feat: add Identity characteristic for stable peer tracking across MAC rotations
Implements BLE Protocol v2 with Transport identity GATT characteristic to solve Android MAC address rotation issues. Adds IDENTITY_CHAR_UUID (00000004-...) that serves the 16-byte RNS.Transport.identity.hash, enabling reliable bidirectional mesh connectivity with Android devices whose BLE MAC addresses rotate every ~15 minutes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
fffb9b188d
commit
3878ac2f70
2 changed files with 106 additions and 0 deletions
|
|
@ -65,6 +65,9 @@ class BLEGATTServer:
|
|||
# TX Characteristic: We notify on this (centrals receive)
|
||||
TX_CHAR_UUID = "00000003-5824-4f48-9e1a-3b3e8f0c1234"
|
||||
|
||||
# Identity Characteristic: Centrals read this to get stable node identity (Protocol v2)
|
||||
IDENTITY_CHAR_UUID = "00000004-5824-4f48-9e1a-3b3e8f0c1234"
|
||||
|
||||
def __init__(self, interface, device_name: str = "Reticulum-Node", agent_capability: str = "NoInputNoOutput"):
|
||||
"""
|
||||
Initialize BLE GATT Server
|
||||
|
|
@ -88,6 +91,9 @@ class BLEGATTServer:
|
|||
self.tx_characteristic = None
|
||||
self.rx_characteristic = None
|
||||
|
||||
# Identity (Protocol v2)
|
||||
self.identity_hash = None # 16-byte Transport identity hash
|
||||
|
||||
# BLE agent for automatic pairing
|
||||
self.ble_agent = None
|
||||
|
||||
|
|
@ -208,6 +214,33 @@ class BLEGATTServer:
|
|||
|
||||
return value # bluezero expects us to return the value
|
||||
|
||||
def _handle_read_identity(self, options):
|
||||
"""
|
||||
Handle read request for Identity characteristic (bluezero callback)
|
||||
|
||||
Called when a central reads the Identity characteristic.
|
||||
Returns the 16-byte Transport identity hash.
|
||||
|
||||
Args:
|
||||
options: D-Bus options dict (may contain 'device' address)
|
||||
|
||||
Returns:
|
||||
list of ints: The 16-byte identity hash as a list of integers
|
||||
"""
|
||||
# Extract central address from options
|
||||
central_address = options.get("device", "unknown")
|
||||
if central_address and central_address != "unknown":
|
||||
central_address = central_address.split("/")[-1].replace("_", ":")
|
||||
|
||||
if self.identity_hash is None:
|
||||
self._log(f">>> READ REQUEST for Identity from {central_address}: Identity not available yet", level="WARNING")
|
||||
return [] # Return empty if not available
|
||||
|
||||
# Convert bytes to list of ints for bluezero
|
||||
identity_list = list(self.identity_hash)
|
||||
self._log(f">>> READ REQUEST for Identity from {central_address}: Serving {len(identity_list)} bytes", level="INFO")
|
||||
return identity_list
|
||||
|
||||
def _handle_central_connected(self, central_address: str, mtu: Optional[int] = None):
|
||||
"""
|
||||
Handle new central connection
|
||||
|
|
@ -355,6 +388,19 @@ class BLEGATTServer:
|
|||
)
|
||||
self._log(f"Added TX characteristic: {self.TX_CHAR_UUID} (READ, NOTIFY)", level="DEBUG")
|
||||
|
||||
# Add Identity characteristic (read to get stable node identity - Protocol v2)
|
||||
identity_value = list(self.identity_hash) if self.identity_hash else []
|
||||
self.peripheral_obj.add_characteristic(
|
||||
srv_id=1,
|
||||
chr_id=3,
|
||||
uuid=self.IDENTITY_CHAR_UUID,
|
||||
value=identity_value,
|
||||
notifying=False,
|
||||
flags=['read'],
|
||||
read_callback=self._handle_read_identity
|
||||
)
|
||||
self._log(f"Added Identity characteristic: {self.IDENTITY_CHAR_UUID} (READ) - Protocol v2", level="DEBUG")
|
||||
|
||||
# Find and save TX characteristic for later notification sends
|
||||
# Characteristics are stored in order added: chr_id=1 (RX) is index 0, chr_id=2 (TX) is index 1
|
||||
if len(self.peripheral_obj.characteristics) >= 2:
|
||||
|
|
@ -438,6 +484,25 @@ class BLEGATTServer:
|
|||
self.running = False
|
||||
raise
|
||||
|
||||
def set_transport_identity(self, identity_hash: bytes):
|
||||
"""
|
||||
Set the Transport identity hash for BLE Protocol v2.
|
||||
|
||||
This should be called after RNS.Transport is initialized and before
|
||||
starting the GATT server (or early during startup).
|
||||
|
||||
Args:
|
||||
identity_hash: 16-byte Reticulum Transport identity hash
|
||||
"""
|
||||
if not isinstance(identity_hash, bytes):
|
||||
raise TypeError(f"identity_hash must be bytes, got {type(identity_hash)}")
|
||||
|
||||
if len(identity_hash) != 16:
|
||||
raise ValueError(f"identity_hash must be 16 bytes, got {len(identity_hash)}")
|
||||
|
||||
self.identity_hash = identity_hash
|
||||
self._log(f"Transport identity set: {identity_hash.hex()}", level="INFO")
|
||||
|
||||
async def stop(self):
|
||||
"""
|
||||
Stop the GATT server and advertising
|
||||
|
|
|
|||
|
|
@ -298,6 +298,7 @@ class BLEInterface(Interface):
|
|||
SERVICE_UUID = "00000001-5824-4f48-9e1a-3b3e8f0c1234" # Custom Reticulum BLE service
|
||||
CHARACTERISTIC_RX_UUID = "00000002-5824-4f48-9e1a-3b3e8f0c1234" # RX characteristic
|
||||
CHARACTERISTIC_TX_UUID = "00000003-5824-4f48-9e1a-3b3e8f0c1234" # TX characteristic
|
||||
CHARACTERISTIC_IDENTITY_UUID = "00000004-5824-4f48-9e1a-3b3e8f0c1234" # Identity characteristic (Protocol v2)
|
||||
|
||||
# Discovery and connection settings
|
||||
DISCOVERY_INTERVAL = 5.0 # seconds between discovery scans
|
||||
|
|
@ -501,6 +502,22 @@ class BLEInterface(Interface):
|
|||
# TODO: Remove when upstream Transport.py is fixed (see session notes)
|
||||
self._clear_stale_ble_paths()
|
||||
|
||||
# Protocol v2: Set Transport identity on GATT server for stable peer tracking
|
||||
if self.gatt_server:
|
||||
try:
|
||||
import RNS.Transport as Transport
|
||||
if hasattr(Transport, 'identity') and Transport.identity:
|
||||
identity_hash = Transport.identity.hash
|
||||
if identity_hash and len(identity_hash) == 16:
|
||||
self.gatt_server.set_transport_identity(identity_hash)
|
||||
RNS.log(f"{self} Set Transport identity on GATT server: {identity_hash.hex()}", RNS.LOG_INFO)
|
||||
else:
|
||||
RNS.log(f"{self} WARNING: Invalid Transport identity hash size: {len(identity_hash) if identity_hash else 0}", RNS.LOG_WARNING)
|
||||
else:
|
||||
RNS.log(f"{self} WARNING: Transport.identity not available yet", RNS.LOG_WARNING)
|
||||
except Exception as e:
|
||||
RNS.log(f"{self} Error setting Transport identity: {e}", RNS.LOG_ERROR)
|
||||
|
||||
self.online = True
|
||||
RNS.log(f"{self} started successfully", RNS.LOG_INFO)
|
||||
|
||||
|
|
@ -1363,6 +1380,30 @@ class BLEInterface(Interface):
|
|||
except Exception as e:
|
||||
RNS.log(f"{self} service discovery failed: {type(e).__name__}: {e} (will retry)", RNS.LOG_WARNING)
|
||||
|
||||
# Read Identity characteristic (Protocol v2) if available
|
||||
peer_identity_hash = None
|
||||
if reticulum_service:
|
||||
try:
|
||||
identity_char = None
|
||||
for char in reticulum_service.characteristics:
|
||||
if char.uuid.lower() == BLEInterface.CHARACTERISTIC_IDENTITY_UUID.lower():
|
||||
identity_char = char
|
||||
break
|
||||
|
||||
if identity_char:
|
||||
RNS.log(f"{self} reading Identity characteristic from {peer.name}...", RNS.LOG_DEBUG)
|
||||
identity_value = await client.read_gatt_char(identity_char)
|
||||
if identity_value and len(identity_value) == 16:
|
||||
peer_identity_hash = bytes(identity_value).hex()
|
||||
RNS.log(f"{self} received peer identity from {peer.name}: {peer_identity_hash}", RNS.LOG_INFO)
|
||||
else:
|
||||
RNS.log(f"{self} invalid identity size from {peer.name}: {len(identity_value) if identity_value else 0} bytes", RNS.LOG_WARNING)
|
||||
else:
|
||||
RNS.log(f"{self} Identity characteristic not found on {peer.name} (Protocol v1 device)", RNS.LOG_DEBUG)
|
||||
except Exception as e:
|
||||
RNS.log(f"{self} failed to read identity from {peer.name}: {type(e).__name__}: {e}", RNS.LOG_DEBUG)
|
||||
# Continue without identity
|
||||
|
||||
# Get negotiated MTU
|
||||
try:
|
||||
# For BlueZ backend, acquire MTU first to avoid warning
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue