From b503718bf81cfc5b00e1fe804f156b95ced46d36 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Wed, 5 Nov 2025 23:52:04 -0500 Subject: [PATCH] fix(ble): Remove device name from advertisements to fix packet size limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes "Failed to register advertisement" error (BlueZ error 0x03) caused by device name exceeding 31-byte BLE advertisement packet limit. Changes: - Make device_name optional (default: None) to save advertisement space - Remove auto-generation of long identity-based names (RNS-{32-hex-identity}) - Update driver to handle None device names when creating peripheral - Use full 16-byte identity (32 hex chars) for fragmenter keys to avoid collisions - Update documentation to reflect device name is optional and discovery is UUID-based Discovery is based on service UUID matching only. Identity is obtained from the Identity GATT characteristic after connection, not from device name. Tested on Raspberry Pi Zero W with BlueZ 5.82 - advertisement now registers successfully (ActiveInstances: 1). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- BLE_PROTOCOL_v2.2.md | 134 +++++++++---------- README.md | 6 +- src/RNS/Interfaces/BLEInterface.py | 26 ++-- src/RNS/Interfaces/bluetooth_driver.py | 9 +- src/RNS/Interfaces/linux_bluetooth_driver.py | 27 ++-- 5 files changed, 101 insertions(+), 101 deletions(-) diff --git a/BLE_PROTOCOL_v2.2.md b/BLE_PROTOCOL_v2.2.md index bbdd96c..1033889 100644 --- a/BLE_PROTOCOL_v2.2.md +++ b/BLE_PROTOCOL_v2.2.md @@ -64,10 +64,10 @@ The BLE Reticulum Protocol enables mesh networking over Bluetooth Low Energy (BL - Centrals read peripheral identities via GATT characteristic - Address-based fragmenter keys -### v2.1 (Identity-Based Naming) -- Device names encode identity: `RNS-{32-hex-identity-hash}` -- Bypasses bluezero service UUID bug (name-based discovery fallback) -- Identity mappings stored during discovery +### v2.1 (Identity-Based Naming) - Deprecated +- **Deprecated:** Device names previously encoded identity: `RNS-{32-hex-identity-hash}` +- **Issue:** 36-character names exceeded 31-byte BLE advertisement packet limit +- **Replaced in v2.2+:** Device names now optional (default: omitted) ### v2.2 (Current - Identity Handshake) - **Identity handshake:** Centrals send 16-byte identity to peripherals @@ -89,31 +89,28 @@ All Reticulum BLE devices advertise this service UUID to enable discovery. ### Device Naming Convention -**Format:** +**Device names are optional** and configurable via the `device_name` parameter in the BLE interface configuration. The default is `None` (no device name in advertisement). + +**Rationale:** +- BLE advertisements have a **31-byte packet size limit** +- Including the 128-bit service UUID (18 bytes) and flags (3 bytes) leaves only ~10 bytes +- Device names compete for limited advertisement space +- **Discovery is based on service UUID matching only** (device name is not used for peer discovery) +- **Identity is obtained from the Identity GATT characteristic** after connection, not from the device name + +**Recommended:** +- **Omit device name** (default: `None`) to maximize advertisement reliability +- If a name is needed for debugging, keep it very short (max 8 characters) + - Example: `"RNS"`, `"Node1"`, etc. + +**Configuration:** +```ini +[[BLE Interface]] + type = BLEInterface + enabled = True + # device_name = None # Default: no device name (recommended) + # device_name = RNS # Optional: short name for debugging ``` -RNS-{32-hex-characters} -``` - -**Example:** -``` -RNS-680069b61fa51cde5a751ed2396ce46d -``` - -Where `680069b61fa51cde5a751ed2396ce46d` is derived from the device's Reticulum identity: -- Take `RNS.Identity.full_hash(identity)` (cryptographic hash) -- Extract first 16 bytes: `[:16]` -- Convert to hexadecimal: `.hex()` → 32 hex characters -- Result: Device name contains 32-character identity fingerprint - -### Why Embed Identity in Name? - -The bluezero GATT server library (used for peripheral mode) has a known bug where service UUIDs are not properly exposed in BLE advertisements when queried via Bleak scanners. Clients see `service_uuids=[]` even though the service is registered. - -**Workaround:** -By embedding the identity in the device name, scanners can: -1. Match by service UUID (preferred, when it works) -2. Fall back to name pattern matching: `^RNS-[0-9a-f]{32}$` -3. Extract identity directly from the name, bypassing GATT characteristic reads ### Advertisement Interval @@ -316,41 +313,37 @@ BLE devices can **rotate MAC addresses** for privacy reasons. If fragmenters/rea ### Solution: Identity-Based Keys -All peer-specific data structures (fragmenters, reassemblers, interfaces) are keyed by a **16-character hex string derived from the peer's identity hash**. +All peer-specific data structures (fragmenters, reassemblers, interfaces) are keyed by a **32-character hex string representing the full 16-byte peer identity**. ### Key Computation ```python def _get_fragmenter_key(self, peer_identity, peer_address): """ - Compute fragmenter/reassembler dictionary key using identity hash. + Compute fragmenter/reassembler dictionary key using full identity. Args: peer_identity: 16-byte identity hash peer_address: BLE MAC address (unused in v2.2, kept for compatibility) Returns: - 16-character hex string (e.g., "680069b61fa51cde") + 32-character hex string representing full 16-byte identity """ - return RNS.Identity.full_hash(peer_identity)[:16].hex()[:16] + return peer_identity.hex() ``` -**Key Derivation Steps:** -1. `RNS.Identity.full_hash(peer_identity)` - Compute cryptographic hash -2. `[:16]` - Take first 16 bytes -3. `.hex()` - Convert to 32 hex characters -4. `[:16]` - Take first 16 hex characters (representing 8 bytes) -5. Result: 16-character hex string used as dictionary key +**Key Derivation:** +- Uses the **full 16-byte peer identity** directly as hex string (32 characters) +- Avoids collision risk that would exist with shortened keys +- Example: `"680069b61fa51cde5a751ed2396ce46d"` (32 hex chars = 16 bytes) **Example:** ```python -peer_identity = bytes.fromhex("680069b61fa51cde5a751ed2396ce46d") # 16 bytes from device name +peer_identity = bytes.fromhex("680069b61fa51cde5a751ed2396ce46d") # 16 bytes from Identity characteristic frag_key = _get_fragmenter_key(peer_identity, "B8:27:EB:10:28:CD") -# Result: "680069b61fa51cde" (16 hex chars, first half of hash) +# Result: "680069b61fa51cde5a751ed2396ce46d" (32 hex chars, full identity) ``` -**Note:** The fragmenter key (16 hex chars) is shorter than the device name identity (32 hex chars) for efficiency, but both are derived from the same identity hash. - ### Identity Mapping Tables Two dictionaries maintain bidirectional identity ↔ address mappings: @@ -361,30 +354,30 @@ self.address_to_identity = { "B8:27:EB:10:28:CD": b'\x68\x00\x69\xb6\x1f\xa5\x1c\xde...', } -# 16-char identity hash → MAC address +# Full 32-char identity hash → MAC address self.identity_to_address = { - "680069b61fa51cde": "B8:27:EB:10:28:CD", + "680069b61fa51cde5a751ed2396ce46d": "B8:27:EB:10:28:CD", } ``` ### Dictionary Structures ```python -# Fragmenters (keyed by identity hash) +# Fragmenters (keyed by full 32-char identity hash) self.fragmenters = { - "680069b61fa51cde": BLEFragmenter(mtu=517), - "a1b2c3d4e5f6g7h8": BLEFragmenter(mtu=23), + "680069b61fa51cde5a751ed2396ce46d": BLEFragmenter(mtu=517), + "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6": BLEFragmenter(mtu=23), } -# Reassemblers (keyed by identity hash) +# Reassemblers (keyed by full 32-char identity hash) self.reassemblers = { - "680069b61fa51cde": BLEReassembler(timeout=30.0), - "a1b2c3d4e5f6g7h8": BLEReassembler(timeout=30.0), + "680069b61fa51cde5a751ed2396ce46d": BLEReassembler(timeout=30.0), + "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6": BLEReassembler(timeout=30.0), } -# Peer interfaces (keyed by identity hash) +# Peer interfaces (keyed by full 32-char identity hash) self.spawned_interfaces = { - "680069b61fa51cde": BLEPeerInterface(...), + "680069b61fa51cde5a751ed2396ce46d": BLEPeerInterface(...), } ``` @@ -525,12 +518,10 @@ Device A (Lower MAC) Device B (Higher MAC) | | | 1. Start scanning (0.5-2s) | 1. Start advertising | | - Service UUID - | | - Device name: RNS-{identity} + | | - Device name (optional) | | | 2. Discover Device B | - | - Match by service UUID or name | - | - Extract identity from name | - | - Store in address_to_identity | + | - Match by service UUID | | | | 3. MAC sorting check | | my_mac < peer_mac → I connect | @@ -572,14 +563,12 @@ Device A (Lower MAC) Device B (Higher MAC) 1. **Scan for BLE devices** (0.5-2.0 seconds depending on power mode) 2. **Match peers:** - - Primary: Check `service_uuids` for Reticulum UUID - - Fallback: Check device name matches `^RNS-[0-9a-f]{32}$` -3. **Extract identity:** - - Parse 32 hex chars from device name - - Convert to 16-byte identity - - Store in `address_to_identity[peer_address] = identity` -4. **Score peers** by RSSI, history, recency -5. **Select best peer** for connection + - Check `service_uuids` for Reticulum service UUID + - Device name is not used for matching (optional/omitted) +3. **Score peers** by RSSI, history, recency +4. **Select best peer** for connection + +**Note:** Identity is obtained from the Identity GATT characteristic after connection, not from the device name or during discovery. ### Connection Phase (Device A → Device B) @@ -1382,8 +1371,7 @@ sequenceDiagram end end - BLE->>BLE: Generate identity-based device name - Note over BLE: Format: RNS-{32-hex-identity-hash}
Example: RNS-680069b61fa51cde5a751ed2396ce46d + Note over BLE: Device name is optional (default: None)
to fit in 31-byte BLE advertisement packet BLE->>Driver: set_identity(identity_16_bytes) Driver-->>BLE: Identity set @@ -1440,7 +1428,7 @@ sequenceDiagram Note over Scanner: Scan cycle (every 5s) Scanner->>Scanner: Start BLE scan - Peer-->>Scanner: Advertisement
Service: 37145b00-...
Name: RNS-680069b61fa51cde...
RSSI: -45 dBm + Peer-->>Scanner: Advertisement
Service: 37145b00-...
Name: (optional/omitted)
RSSI: -45 dBm Scanner->>BLE: on_device_discovered(address, rssi, name, service_uuids) @@ -1583,7 +1571,7 @@ sequenceDiagram Peripheral->>Peripheral: Extract central's identity Peripheral->>Peripheral: Compute identity hash - Note over Peripheral: hash = RNS.Identity.full_hash(identity)[:16].hex()[:16]
Steps: hash → first 16 bytes → hex → first 16 chars
Example: "680069b61fa51cde" + Note over Peripheral: hash = identity.hex()
Uses full 16-byte identity as 32 hex chars
Example: "680069b61fa51cde5a751ed2396ce46d" Peripheral->>Peripheral: Store bidirectional mappings Note over Peripheral: address_to_identity[central_addr] = identity_16_bytes
identity_to_address[identity_hash] = central_addr @@ -1626,10 +1614,10 @@ sequenceDiagram **Central Side:** ```python address_to_identity["B8:27:EB:A8:A7:22"] = b'\x68\x00\x69\xb6...' # From discovery -identity_to_address["680069b61fa51cde"] = "B8:27:EB:A8:A7:22" -fragmenters["680069b61fa51cde"] = BLEFragmenter(mtu=517) -reassemblers["680069b61fa51cde"] = BLEReassembler() -spawned_interfaces["680069b61fa51cde"] = BLEPeerInterface(...) +identity_to_address["680069b61fa51cde5a751ed2396ce46d"] = "B8:27:EB:A8:A7:22" +fragmenters["680069b61fa51cde5a751ed2396ce46d"] = BLEFragmenter(mtu=517) +reassemblers["680069b61fa51cde5a751ed2396ce46d"] = BLEReassembler() +spawned_interfaces["680069b61fa51cde5a751ed2396ce46d"] = BLEPeerInterface(...) ``` **Peripheral Side:** @@ -1667,7 +1655,7 @@ sequenceDiagram Note over Transport: 233-byte announce packet
Contains: identity, public key, hops, etc. BLE_If->>BLE_If: Look up fragmenter by identity hash - Note over BLE_If: Key: "680069b61fa51cde" + Note over BLE_If: Key: "680069b61fa51cde5a751ed2396ce46d" BLE_If->>Frag: fragment_packet(data, mtu=23) activate Frag diff --git a/README.md b/README.md index 99efd5a..c3a303a 100644 --- a/README.md +++ b/README.md @@ -159,8 +159,8 @@ Add the BLE interface to your Reticulum configuration (`~/.reticulum/config`): type = BLEInterface enabled = yes - # Optional: customize device name - # device_name = My-Reticulum-Node + # Optional: set short device name (max 8 chars recommended, default: none) + # device_name = RNS ``` For detailed configuration options, see [`examples/config_example.toml`](examples/config_example.toml). @@ -195,7 +195,7 @@ The BLE interface supports extensive configuration options. See [`examples/confi ### Key Configuration Options -- **`device_name`**: Advertised device name (auto-generated if not specified) +- **`device_name`**: Optional BLE device name (default: none, keep short if used, max 8 chars recommended) - **`service_uuid`**: BLE service UUID (must match on all devices) - **`enable_peripheral`**: Accept incoming connections (default: yes) - **`enable_central`**: Scan and connect to peers (default: yes) diff --git a/src/RNS/Interfaces/BLEInterface.py b/src/RNS/Interfaces/BLEInterface.py index f37dacb..fa97b67 100644 --- a/src/RNS/Interfaces/BLEInterface.py +++ b/src/RNS/Interfaces/BLEInterface.py @@ -288,10 +288,11 @@ class BLEInterface(Interface): # BLE configuration self.service_uuid = c.get("service_uuid", BLEInterface.SERVICE_UUID) - # Device name will be set to identity-based name after Transport.identity is available - # Format: RNS-{identity_hash} where identity_hash is first 16 hex chars of Transport.identity - # This enables reliable discovery even when bluezero doesn't expose service UUIDs to Bleak - self.device_name = c.get("device_name", None) # Will be auto-generated from identity if None + # Device name for BLE advertising (optional, configurable via config file) + # Default is None (no device name) to save advertisement packet space (31-byte limit). + # Discovery is based on service UUID only. Identity is obtained from the Identity + # characteristic after connection. If set, keep it short (max 8 chars recommended). + self.device_name = c.get("device_name", None) self.discovery_interval = float(c.get("discovery_interval", BLEInterface.DISCOVERY_INTERVAL)) self.max_peers = int(c.get("max_connections", BLEInterface.MAX_PEERS)) self.min_rssi = int(c.get("min_rssi", BLEInterface.MIN_RSSI)) @@ -487,19 +488,16 @@ class BLEInterface(Interface): elapsed = time.time() - start_time RNS.log(f"{self} Transport.identity available after {elapsed:.1f}s", RNS.LOG_INFO) - # Generate identity-based device name if not configured - if self.device_name is None: - identity_str = identity_hash.hex() # Full 16 bytes as 32 hex chars - self.device_name = f"RNS-{identity_str}" - RNS.log(f"{self} Auto-generated identity-based device name: {self.device_name}", RNS.LOG_INFO) - # Set identity on driver self.driver.set_identity(identity_hash) # Start advertising try: self.driver.start_advertising(self.device_name, identity_hash) - RNS.log(f"{self} Started advertising as {self.device_name}", RNS.LOG_INFO) + if self.device_name: + RNS.log(f"{self} Started advertising as {self.device_name}", RNS.LOG_INFO) + else: + RNS.log(f"{self} Started advertising (no device name)", RNS.LOG_INFO) except Exception as e: RNS.log(f"{self} Failed to start advertising: {e}", RNS.LOG_ERROR) @@ -1138,16 +1136,16 @@ class BLEInterface(Interface): def _get_fragmenter_key(self, peer_identity, peer_address): """ - Compute fragmenter/reassembler dictionary key using identity hash. + Compute fragmenter/reassembler dictionary key using full identity hash. Args: peer_identity: 16-byte peer identity peer_address: BLE MAC address (unused, kept for compatibility) Returns: - str: Identity hash (16 hex chars) + str: Full 16-byte identity as 32 hex characters """ - return RNS.Identity.full_hash(peer_identity)[:16].hex()[:16] + return peer_identity.hex() def _compute_identity_hash(self, peer_identity): """ diff --git a/src/RNS/Interfaces/bluetooth_driver.py b/src/RNS/Interfaces/bluetooth_driver.py index 2274025..0cdffec 100644 --- a/src/RNS/Interfaces/bluetooth_driver.py +++ b/src/RNS/Interfaces/bluetooth_driver.py @@ -107,10 +107,15 @@ class BLEDriverInterface(ABC): pass @abstractmethod - def start_advertising(self, device_name: str, identity: bytes): + def start_advertising(self, device_name: Optional[str], identity: bytes): """ - Starts advertising the configured service UUID and the given device name. + Starts advertising the configured service UUID and optionally a device name. The identity parameter is used to populate the Identity characteristic. + + Args: + device_name: Optional device name to include in advertisement (None to omit). + Keep short (max 8 chars) to fit in 31-byte BLE advertisement packet. + identity: 16-byte identity hash for the Identity characteristic. """ pass diff --git a/src/RNS/Interfaces/linux_bluetooth_driver.py b/src/RNS/Interfaces/linux_bluetooth_driver.py index 3e74989..dcbf4e1 100644 --- a/src/RNS/Interfaces/linux_bluetooth_driver.py +++ b/src/RNS/Interfaces/linux_bluetooth_driver.py @@ -622,7 +622,7 @@ class LinuxBluetoothDriver(BLEDriverInterface): # Advertising (Peripheral Mode) # ======================================================================== - def start_advertising(self, device_name: str, identity: bytes): + def start_advertising(self, device_name: Optional[str], identity: bytes): """Start advertising as a BLE peripheral.""" if not self._running: self._log("Cannot start advertising: driver not running", "ERROR") @@ -638,7 +638,10 @@ class LinuxBluetoothDriver(BLEDriverInterface): self._log("Already advertising", "DEBUG") return - self._log(f"Starting BLE advertising as '{device_name}'...") + if device_name: + self._log(f"Starting BLE advertising as '{device_name}'...") + else: + self._log("Starting BLE advertising (no device name)...") # Set identity self.set_identity(identity) @@ -1263,7 +1266,7 @@ class BluezeroGATTServer: self._log(f"Identity set: {identity_bytes.hex()}") - def start(self, device_name: str): + def start(self, device_name: Optional[str]): """Start GATT server and advertising.""" if self.running: self._log("Server already running", "WARNING") @@ -1273,7 +1276,10 @@ class BluezeroGATTServer: if not self.identity_bytes: raise RuntimeError("Identity must be set before starting GATT server. Call set_identity() first.") - self._log(f"Starting GATT server with device name '{device_name}'...") + if device_name: + self._log(f"Starting GATT server with device name '{device_name}'...") + else: + self._log("Starting GATT server (no device name)...") # Reset events self.stop_event.clear() @@ -1362,11 +1368,14 @@ class BluezeroGATTServer: adapter_address = local_adapter.address self._log(f"Using adapter: {adapter_address}", "DEBUG") - # Create peripheral - self.peripheral_obj = peripheral.Peripheral( - adapter_address, - local_name=device_name - ) + # Create peripheral (omit local_name if None to save advertisement packet space) + if device_name: + self.peripheral_obj = peripheral.Peripheral( + adapter_address, + local_name=device_name + ) + else: + self.peripheral_obj = peripheral.Peripheral(adapter_address) # Add service self.peripheral_obj.add_service(