From bb3ddf58c4ea21d98f7a6e34dfd75a3abac70907 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Thu, 30 Oct 2025 20:40:31 -0400 Subject: [PATCH] fix: ensure Transport.identity loaded before GATT server starts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change from async deferred loading to synchronous wait before GATT server startup. This ensures the Identity characteristic is created with a valid 16-byte value instead of empty [], preventing BlueZ from rejecting or corrupting the advertisement which caused "0 matching service UUID" discovery failures. The bug: Identity characteristic was being created with value=[] because the GATT server thread started before Transport.identity was loaded from storage (~1s timing window). BlueZ may silently reject advertisements when validating GATT databases with empty READ characteristics. The fix: Block interface startup for up to 30s waiting for Transport.identity (typically available within 0.5-1s), then set it on GATT server BEFORE starting the server thread. Identity characteristic now always has valid 16-byte value when registered with BlueZ. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/RNS/Interfaces/BLEGATTServer.py | 5 +- src/RNS/Interfaces/BLEInterface.py | 79 ++++++++++++++++------------- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/src/RNS/Interfaces/BLEGATTServer.py b/src/RNS/Interfaces/BLEGATTServer.py index e8ec6fe..d49c25f 100644 --- a/src/RNS/Interfaces/BLEGATTServer.py +++ b/src/RNS/Interfaces/BLEGATTServer.py @@ -399,7 +399,10 @@ class BLEGATTServer: flags=['read'], read_callback=self._handle_read_identity ) - self._log(f"Added Identity characteristic: {self.IDENTITY_CHAR_UUID} (READ) - Protocol v2", level="DEBUG") + if identity_value: + self._log(f"Added Identity characteristic: {self.IDENTITY_CHAR_UUID} (READ) with {len(identity_value)} bytes - Protocol v2", level="DEBUG") + else: + self._log(f"Added Identity characteristic: {self.IDENTITY_CHAR_UUID} (READ) with EMPTY value - will be updated when identity loads", level="WARNING") # 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 diff --git a/src/RNS/Interfaces/BLEInterface.py b/src/RNS/Interfaces/BLEInterface.py index 88ac7a9..f8fd355 100644 --- a/src/RNS/Interfaces/BLEInterface.py +++ b/src/RNS/Interfaces/BLEInterface.py @@ -488,6 +488,18 @@ class BLEInterface(Interface): else: RNS.log(f"{self} central mode disabled, skipping peer discovery", RNS.LOG_INFO) + # Protocol v2: Wait for Transport.identity BEFORE starting GATT server + # This ensures the Identity characteristic is created with a valid value, + # preventing BlueZ from rejecting/corrupting the advertisement + if self.gatt_server: + RNS.log(f"{self} Waiting for Transport.identity before starting GATT server...", RNS.LOG_DEBUG) + identity_hash = self._wait_for_transport_identity(timeout=30) + if identity_hash: + self.gatt_server.set_transport_identity(identity_hash) + RNS.log(f"{self} Transport.identity set on GATT server: {identity_hash.hex()}", RNS.LOG_INFO) + else: + RNS.log(f"{self} WARNING: Starting GATT server without identity (Protocol v1 mode)", RNS.LOG_WARNING) + # Start GATT server if peripheral mode is enabled if self.gatt_server: asyncio.run_coroutine_threadsafe(self._start_server(), self.loop) @@ -505,60 +517,55 @@ class BLEInterface(Interface): self.online = True RNS.log(f"{self} started successfully", RNS.LOG_INFO) - # Protocol v2: Load Transport identity asynchronously after startup - # Transport.identity is loaded AFTER interface initialization, so we need to wait for it - if self.gatt_server: - RNS.log(f"{self} Launching deferred Transport.identity loading task", RNS.LOG_DEBUG) - asyncio.run_coroutine_threadsafe(self._load_identity_when_ready(), self.loop) - def _run_async_loop(self): """Run the asyncio event loop in a separate thread.""" self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.loop.run_forever() - async def _load_identity_when_ready(self): + def _wait_for_transport_identity(self, timeout=30): """ - Wait for Transport.identity to be loaded, then set it on the GATT server. + Synchronously wait for Transport.identity to be loaded. - Transport.identity is loaded from storage AFTER interface initialization, - so we need to poll until it becomes available. This is called as a background - task during interface startup. + Called during interface startup BEFORE GATT server starts to ensure + the Identity characteristic can be created with a valid value. - Retries every 1 second for up to 30 seconds. + Uses polling with small delays to avoid blocking too long. + + Args: + timeout: Maximum seconds to wait for identity + + Returns: + 16-byte identity hash or None if timeout/unavailable """ - max_attempts = 30 - retry_interval = 1.0 # seconds + import RNS.Transport as Transport + + start_time = time.time() + attempt = 0 + + while time.time() - start_time < timeout: + attempt += 1 - for attempt in range(1, max_attempts + 1): 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: - # Success! Set identity on GATT server - self.gatt_server.set_transport_identity(identity_hash) - RNS.log(f"{self} ✓ Transport.identity loaded on attempt {attempt}, set on GATT server: {identity_hash.hex()}", RNS.LOG_INFO) - return - else: - RNS.log(f"{self} WARNING: Invalid Transport identity hash size: {len(identity_hash) if identity_hash else 0}", RNS.LOG_WARNING) - return - - # Not available yet, log and retry - if attempt == 1 or attempt % 5 == 0 or attempt == max_attempts: - # Log on first attempt, every 5th attempt, and last attempt - RNS.log(f"{self} Waiting for Transport.identity to load... (attempt {attempt}/{max_attempts})", RNS.LOG_DEBUG) - + elapsed = time.time() - start_time + RNS.log(f"{self} ✓ Transport.identity available after {elapsed:.1f}s (attempt {attempt})", RNS.LOG_INFO) + return identity_hash except Exception as e: - RNS.log(f"{self} Error checking Transport.identity: {e}", RNS.LOG_WARNING) + if attempt == 1: + RNS.log(f"{self} Error checking Transport.identity: {e}", RNS.LOG_DEBUG) - await asyncio.sleep(retry_interval) + # Log progress periodically + if attempt == 1 or attempt % 10 == 0: + RNS.log(f"{self} Waiting for Transport.identity... (attempt {attempt}, {time.time() - start_time:.1f}s)", RNS.LOG_DEBUG) - # Timeout - identity never became available - RNS.log(f"{self} WARNING: Transport.identity not available after {max_attempts}s - GATT server will serve empty identity", RNS.LOG_WARNING) - RNS.log(f"{self} Protocol v2 disabled - falling back to MAC-based peer tracking", RNS.LOG_WARNING) + time.sleep(0.1) # Poll every 100ms + + # Timeout + RNS.log(f"{self} WARNING: Transport.identity not available after {timeout}s", RNS.LOG_WARNING) + return None def _clear_stale_ble_paths(self): """