Gate 2E passes on jp
Some checks failed
Tests / Detect Changes (push) Has been cancelled
Tests / Installer Test (Raspberry Pi OS - ARM) (push) Has been cancelled
Tests / Installer Test (Raspberry Pi OS - ARM)-1 (push) Has been cancelled
Tests / Unit Tests (push) Has been cancelled
Tests / Unit Tests-1 (push) Has been cancelled
Tests / Unit Tests-2 (push) Has been cancelled
Tests / Unit Tests-3 (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Integration Tests-1 (push) Has been cancelled
Tests / Integration Tests-2 (push) Has been cancelled
Tests / Integration Tests-3 (push) Has been cancelled
Tests / Installer Test (Fresh System) (push) Has been cancelled
Tests / Installer Test (Fresh System)-1 (push) Has been cancelled
Tests / Installer Test (Fresh System)-2 (push) Has been cancelled
Tests / Installer Test (Fresh System)-3 (push) Has been cancelled
Tests / Installer Test (Fresh System)-4 (push) Has been cancelled

This commit is contained in:
John Poole 2026-05-18 16:40:05 -07:00
commit 8589f97f49
6 changed files with 880 additions and 1 deletions

View file

@ -0,0 +1,135 @@
# Gate 2E: BLEPeerSessionManager Optional Integration
Date: 2026-05-18 16:28 America/Los_Angeles
Scope: optional `BLEPeerSessionManager` integration behind `BLE_RETICULUM_SESSION_BACKEND`.
## Summary
Gate 2E adds an optional C++ session-manager path for `BLEInterface._handle_identity_handshake`.
`BLEInterface.py` was modified in this gate. The existing Python handshake implementation was preserved as `_handle_identity_handshake_python`, and `_handle_identity_handshake` is now a dispatcher.
No live BLE field testing was performed.
## Backend Flag
New environment variable:
```text
BLE_RETICULUM_SESSION_BACKEND=auto|cpp|python
```
Behavior:
- `python`: always use the existing Python handshake logic.
- `cpp`: require `ble_protocol_core_cpp.BLEPeerSessionManager`; fail clearly if unavailable.
- `auto`: prefer C++ session manager if importable, otherwise use Python.
This is separate from:
```text
BLE_RETICULUM_FRAGMENTATION_BACKEND
```
The fragmentation backend was not conflated with session backend selection.
## Modified Source
- `src/ble_reticulum/BLESessionBackend.py`
- `src/ble_reticulum/BLEInterface.py`
`handle_peripheral_data` was not modified.
The C++ manager remains a decision engine only. Python still performs:
- `RNS.log`
- `driver.disconnect`
- Python fragmenter/reassembler creation
- `_spawn_peer_interface`
- `address_to_identity` / `identity_to_address` mirroring
- `address_to_interface` mirroring
- `_pending_identity_connections` cleanup
- `_last_real_data` updates
## Tests Added
Added:
- `migration/tests/test_ble_session_backend_integration.py`
Coverage:
- default/auto backend selects C++ when available;
- auto falls back to Python when C++ is unavailable;
- Python backend forces Python even when C++ is available;
- C++ backend fails clearly when unavailable;
- Python backend still uses existing Python handshake behavior;
- C++ backend covers Gate 2D equivalence cases through the `BLEInterface._handle_identity_handshake` dispatcher:
- non-16-byte payload;
- new identity accepted;
- duplicate same identity consumed;
- duplicate mismatched identity consumed with warning intent;
- duplicate active elsewhere disconnects current address;
- stale/pending detach replacement accepted;
- zombie old connection accepted and old peer disconnect requested;
- MTU fallback to 23;
- MTU provided honored;
- pending identity removed;
- existing spawned interface path updates address.
## Verification
Backend shim regression:
```sh
pytest -q migration/tests/test_fragmentation_backend_shim.py
```
Result:
```text
9 passed, 2 warnings in 0.59s
```
Gate 2C / Gate 2D / Gate 2E regression:
```sh
PYTHONPATH=migration/protocol_core pytest -q migration/tests/test_ble_peer_session_manager_pybind.py migration/tests/test_ble_peer_session_manager_python_equivalence.py migration/tests/test_ble_session_backend_integration.py
```
Result:
```text
39 passed, 1 skipped, 2 warnings in 0.51s
```
Migration regression set:
```sh
PYTHONPATH=migration/protocol_core pytest -q migration/tests/test_fragmentation_cpp_equivalence.py migration/tests/test_identity_helpers_cpp_equivalence.py migration/tests/test_ble_peer_session_manager_pybind.py migration/tests/test_ble_peer_session_manager_python_equivalence.py migration/tests/test_ble_session_backend_integration.py
```
Result:
```text
87 passed, 2 skipped, 2 warnings in 0.77s
```
The pybind11 extension was already current for Gate 2E; no rebuild was needed in this gate.
## Notes
The default backend selector follows the Gate 2E requested `auto` behavior: prefer C++ when available and fall back to Python when unavailable. Operators can force the reference Python path with:
```sh
BLE_RETICULUM_SESSION_BACKEND=python
```
## SQL
Companion SQL:
`migration/sql/mark_gate2e_protocol_session_optional_integration_20260518_1628.sql`
The SQL marks `_handle_identity_handshake` as `OPTIONAL_INTEGRATION` for phase `2_ble_protocol_session_manager`, keeps tag `GLUE`, keeps `cpp_candidate=1`, and does not mark field acceptance.

View file

@ -0,0 +1,92 @@
BEGIN TRANSACTION;
INSERT INTO symbols (
source_file,
symbol_name,
symbol_type,
class_name,
line_number,
tag,
phase,
status,
cpp_candidate,
confidence,
rationale,
callers,
callees,
notes,
first_seen_at,
updated_at
)
VALUES (
'src/ble_reticulum/BLEInterface.py',
'_handle_identity_handshake',
'method',
'BLEInterface',
1202,
'GLUE',
'2_ble_protocol_session_manager',
'OPTIONAL_INTEGRATION',
1,
'high',
'Gate 2E added optional BLEPeerSessionManager integration behind BLE_RETICULUM_SESSION_BACKEND. The existing Python handshake path remains available and the C++ manager remains a decision engine with Python adapter side effects.',
'_data_received_callback',
'_handle_identity_handshake_python; _handle_identity_handshake_cpp; _compute_identity_hash; _check_duplicate_identity; driver.disconnect; driver.get_peer_mtu; _get_fragmenter_key; BLEFragmenter; BLEReassembler; _spawn_peer_interface',
'Gate 2E optional integration tests passed. BLEInterface.py was modified, handle_peripheral_data was not modified, no live BLE field tests were performed, and no Phase 1 FIELD_ACCEPTED rows were changed.',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
ON CONFLICT(source_file, class_name, symbol_name, line_number) DO UPDATE SET
tag = 'GLUE',
phase = '2_ble_protocol_session_manager',
status = 'OPTIONAL_INTEGRATION',
cpp_candidate = 1,
confidence = 'high',
rationale = excluded.rationale,
callers = excluded.callers,
callees = excluded.callees,
notes = excluded.notes,
updated_at = CURRENT_TIMESTAMP;
INSERT INTO reviews (
symbol_id,
reviewed_at,
reviewer,
old_tag,
new_tag,
old_status,
new_status,
note
)
SELECT
symbol_id,
CURRENT_TIMESTAMP,
'Codex',
'GLUE',
'GLUE',
'PYTHON_EQUIVALENT',
'OPTIONAL_INTEGRATION',
'Gate 2E added optional BLEPeerSessionManager integration behind BLE_RETICULUM_SESSION_BACKEND. Regression results: backend shim 9 passed; Gate 2C/2D/2E 39 passed, 1 skipped; migration set 87 passed, 2 skipped. No FIELD_ACCEPTED status assigned.'
FROM symbols
WHERE source_file = 'src/ble_reticulum/BLEInterface.py'
AND class_name = 'BLEInterface'
AND symbol_name = '_handle_identity_handshake'
AND line_number = 1202;
SELECT
symbol_id,
source_file,
class_name,
symbol_name,
tag,
phase,
status,
cpp_candidate,
updated_at
FROM symbols
WHERE source_file = 'src/ble_reticulum/BLEInterface.py'
AND class_name = 'BLEInterface'
AND symbol_name = '_handle_identity_handshake'
AND line_number = 1202;
COMMIT;

View file

@ -117,7 +117,7 @@ class FakePeerInterface:
class Harness:
_handle_identity_handshake = BLEInterface._handle_identity_handshake
_handle_identity_handshake = BLEInterface._handle_identity_handshake_python
_check_duplicate_identity = BLEInterface._check_duplicate_identity
_compute_identity_hash = BLEInterface._compute_identity_hash
_get_fragmenter_key = BLEInterface._get_fragmenter_key

View file

@ -0,0 +1,360 @@
import json
import os
import subprocess
import sys
import types
import pytest
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
SRC_DIR = os.path.join(REPO_ROOT, "src")
CPP_BUILD_DIR = os.path.join(REPO_ROOT, "migration", "protocol_core")
sys.path.insert(0, SRC_DIR)
sys.path.insert(0, CPP_BUILD_DIR)
cpp = pytest.importorskip("ble_protocol_core_cpp")
def install_fake_rns():
rns = types.ModuleType("RNS")
rns.log = lambda *args, **kwargs: None
rns.LOG_EXTREME = 0
rns.LOG_DEBUG = 1
rns.LOG_INFO = 2
rns.LOG_NOTICE = 2
rns.LOG_WARNING = 3
rns.LOG_ERROR = 4
rns.LOG_CRITICAL = 5
rns.Reticulum = types.SimpleNamespace(configdir=None)
rns.Transport = types.SimpleNamespace(interfaces=[])
sys.modules["RNS"] = rns
transport = types.ModuleType("RNS.Transport")
transport.interfaces = []
sys.modules["RNS.Transport"] = transport
sys.modules["RNS.Interfaces"] = types.ModuleType("RNS.Interfaces")
interface_mod = types.ModuleType("RNS.Interfaces.Interface")
class Interface:
MODE_FULL = 0
interface_mod.Interface = Interface
sys.modules["RNS.Interfaces.Interface"] = interface_mod
install_fake_rns()
import ble_reticulum.BLEInterface as ble_interface_module
from ble_reticulum.BLEInterface import BLEInterface
class FakeRNS:
LOG_DEBUG = 1
LOG_INFO = 2
LOG_NOTICE = 2
LOG_WARNING = 3
LOG_ERROR = 4
logs = []
@staticmethod
def log(message, level):
FakeRNS.logs.append((level, message))
class FakeDriver:
def __init__(self, mtu=None, connected_peers=None):
self.mtu = mtu
self.connected_peers = set(connected_peers or [])
self.disconnect_calls = []
def disconnect(self, address):
self.disconnect_calls.append(address)
def get_peer_mtu(self, address):
return self.mtu
class FakeFragmenter:
instances = []
def __init__(self, mtu=185):
self.mtu = mtu
FakeFragmenter.instances.append(self)
class FakeReassembler:
instances = []
def __init__(self, timeout=None):
self.timeout = timeout
FakeReassembler.instances.append(self)
class FakeLock:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
class FakePeerInterface:
def __init__(self, address):
self.peer_address = address
class Harness:
_handle_identity_handshake = BLEInterface._handle_identity_handshake
_handle_identity_handshake_cpp = BLEInterface._handle_identity_handshake_cpp
_handle_identity_handshake_python = BLEInterface._handle_identity_handshake_python
_get_session_backend = BLEInterface._get_session_backend
_get_session_manager = BLEInterface._get_session_manager
_build_session_snapshot = BLEInterface._build_session_snapshot
_check_duplicate_identity = BLEInterface._check_duplicate_identity
_compute_identity_hash = BLEInterface._compute_identity_hash
_get_fragmenter_key = BLEInterface._get_fragmenter_key
def __init__(self, backend="cpp", mtu=None, connected_peers=None):
self._session_backend = backend
self._session_manager = cpp.BLEPeerSessionManager() if backend == "cpp" else None
self.driver = FakeDriver(mtu=mtu, connected_peers=connected_peers)
self.address_to_identity = {}
self.identity_to_address = {}
self.address_to_interface = {}
self.spawned_interfaces = {}
self.peers = {}
self.fragmenters = {}
self.reassemblers = {}
self.frag_lock = FakeLock()
self._pending_identity_connections = {}
self._pending_identity_timeout = 30
self._pending_detach = {}
self._last_real_data = {}
self._zombie_timeout = 45.0
self.cleanup_calls = []
self.spawn_calls = []
def _cleanup_stale_address(self, identity_hash, old_address):
self.cleanup_calls.append((identity_hash, old_address))
self.address_to_identity.pop(old_address, None)
self.address_to_interface.pop(old_address, None)
self.peers.pop(old_address, None)
def _spawn_peer_interface(
self,
address,
name,
peer_identity,
client=None,
mtu=None,
connection_type="central",
):
identity_hash = self._compute_identity_hash(peer_identity)
peer = FakePeerInterface(address)
self.spawned_interfaces[identity_hash] = peer
self.address_to_interface[address] = peer
self.spawn_calls.append(
{
"address": address,
"peer_identity": peer_identity,
"mtu": mtu,
"connection_type": connection_type,
}
)
return peer
def __str__(self):
return "HarnessBLEInterface"
@pytest.fixture(autouse=True)
def patch_bleinterface_globals(monkeypatch):
FakeRNS.logs = []
FakeFragmenter.instances = []
FakeReassembler.instances = []
monkeypatch.setattr(ble_interface_module, "RNS", FakeRNS)
monkeypatch.setattr(ble_interface_module, "BLEFragmenter", FakeFragmenter)
monkeypatch.setattr(ble_interface_module, "BLEReassembler", FakeReassembler)
def identity(base):
return bytes((base + i) & 0xFF for i in range(16))
def run_backend_probe(requested=None, include_cpp=True):
env = os.environ.copy()
if requested is None:
env.pop("BLE_RETICULUM_SESSION_BACKEND", None)
else:
env["BLE_RETICULUM_SESSION_BACKEND"] = requested
pythonpath = [SRC_DIR]
if include_cpp:
pythonpath.append(CPP_BUILD_DIR)
env["PYTHONPATH"] = os.pathsep.join(pythonpath)
completed = subprocess.run(
[
sys.executable,
"-c",
"import json, ble_reticulum.BLESessionBackend as b; "
"print(json.dumps({'backend': b.BACKEND, 'requested': b.REQUESTED_BACKEND}))",
],
cwd=REPO_ROOT,
env=env,
text=True,
capture_output=True,
)
return completed
def test_default_auto_backend_selects_cpp_when_available():
completed = run_backend_probe(requested=None, include_cpp=True)
assert completed.returncode == 0, completed.stderr
assert json.loads(completed.stdout)["backend"] == "cpp"
def test_auto_backend_falls_back_to_python_when_cpp_unavailable():
completed = run_backend_probe(requested="auto", include_cpp=False)
assert completed.returncode == 0, completed.stderr
assert json.loads(completed.stdout)["backend"] == "python"
def test_python_backend_forces_python_even_when_cpp_available():
completed = run_backend_probe(requested="python", include_cpp=True)
assert completed.returncode == 0, completed.stderr
assert json.loads(completed.stdout)["backend"] == "python"
def test_cpp_backend_fails_clearly_when_unavailable():
completed = run_backend_probe(requested="cpp", include_cpp=False)
assert completed.returncode != 0
assert "C++ BLE session backend is not available" in completed.stderr
def test_python_backend_still_uses_existing_python_behavior():
harness = Harness(backend="python", mtu=185)
peer_identity = identity(0x10)
result = harness._handle_identity_handshake("AA:01", peer_identity)
assert result is True
assert harness.address_to_identity["AA:01"] == peer_identity
frag_key = harness._get_fragmenter_key(peer_identity, "AA:01")
assert harness.fragmenters[frag_key].mtu == 185
def test_cpp_backend_non_16_byte_payload_returns_false():
harness = Harness(backend="cpp")
assert harness._handle_identity_handshake("AA:02", b"abc") is False
def test_cpp_backend_new_identity_accepted():
harness = Harness(backend="cpp", mtu=185)
harness._pending_identity_connections["AA:03"] = 1.0
peer_identity = identity(0x20)
result = harness._handle_identity_handshake("AA:03", peer_identity)
assert result is True
assert harness.address_to_identity["AA:03"] == peer_identity
assert "AA:03" not in harness._pending_identity_connections
assert harness.spawn_calls
frag_key = harness._get_fragmenter_key(peer_identity, "AA:03")
assert harness.fragmenters[frag_key].mtu == 185
def test_cpp_backend_duplicate_same_identity_consumed():
harness = Harness(backend="cpp")
peer_identity = identity(0x30)
harness.address_to_identity["AA:04"] = peer_identity
assert harness._handle_identity_handshake("AA:04", peer_identity) is True
def test_cpp_backend_duplicate_mismatch_consumed_with_warning():
harness = Harness(backend="cpp")
harness.address_to_identity["AA:05"] = identity(0x40)
assert harness._handle_identity_handshake("AA:05", identity(0x50)) is True
assert FakeRNS.logs
def test_cpp_backend_duplicate_active_elsewhere_disconnects_current():
old_address = "AA:06"
current_address = "AA:07"
peer_identity = identity(0x60)
harness = Harness(backend="cpp", connected_peers={old_address})
identity_hash = harness._compute_identity_hash(peer_identity)
harness.identity_to_address[identity_hash] = old_address
harness.peers[old_address] = object()
assert harness._handle_identity_handshake(current_address, peer_identity) is True
assert harness.driver.disconnect_calls == [current_address]
def test_cpp_backend_stale_pending_detach_replacement_accepted():
old_address = "AA:08"
current_address = "AA:09"
peer_identity = identity(0x70)
harness = Harness(backend="cpp")
identity_hash = harness._compute_identity_hash(peer_identity)
harness.identity_to_address[identity_hash] = old_address
harness.address_to_identity[old_address] = peer_identity
harness._pending_detach[identity_hash] = 1.0
assert harness._handle_identity_handshake(current_address, peer_identity) is True
assert harness.cleanup_calls == [(identity_hash, old_address)]
assert harness.address_to_identity[current_address] == peer_identity
def test_cpp_backend_zombie_old_connection_disconnects_old():
old_address = "AA:0A"
current_address = "AA:0B"
peer_identity = identity(0x80)
harness = Harness(backend="cpp", connected_peers={old_address})
identity_hash = harness._compute_identity_hash(peer_identity)
harness.identity_to_address[identity_hash] = old_address
harness.peers[old_address] = object()
harness.address_to_identity[old_address] = peer_identity
harness._last_real_data[identity_hash] = 1.0
assert harness._handle_identity_handshake(current_address, peer_identity) is True
assert old_address in harness.driver.disconnect_calls
assert harness.cleanup_calls == [(identity_hash, old_address)]
def test_cpp_backend_mtu_fallback_to_23():
harness = Harness(backend="cpp", mtu=None)
peer_identity = identity(0x90)
assert harness._handle_identity_handshake("AA:0C", peer_identity) is True
frag_key = harness._get_fragmenter_key(peer_identity, "AA:0C")
assert harness.fragmenters[frag_key].mtu == 23
def test_cpp_backend_mtu_provided_is_honored():
harness = Harness(backend="cpp", mtu=247)
peer_identity = identity(0xA0)
assert harness._handle_identity_handshake("AA:0D", peer_identity) is True
frag_key = harness._get_fragmenter_key(peer_identity, "AA:0D")
assert harness.fragmenters[frag_key].mtu == 247
def test_cpp_backend_pending_identity_removed():
harness = Harness(backend="cpp")
harness._pending_identity_connections["AA:0E"] = 1.0
assert harness._handle_identity_handshake("AA:0E", identity(0xB0)) is True
assert "AA:0E" not in harness._pending_identity_connections
def test_cpp_backend_existing_spawned_interface_updates_address():
old_address = "AA:0F"
current_address = "AA:10"
peer_identity = identity(0xC0)
harness = Harness(backend="cpp")
identity_hash = harness._compute_identity_hash(peer_identity)
existing = FakePeerInterface(old_address)
harness.spawned_interfaces[identity_hash] = existing
harness.identity_to_address[identity_hash] = old_address
harness._pending_detach[identity_hash] = 1.0
assert harness._handle_identity_handshake(current_address, peer_identity) is True
assert existing.peer_address == current_address
assert harness.address_to_interface[current_address] is existing

View file

@ -98,6 +98,41 @@ except ImportError as e:
BLE_FRAGMENTATION_BACKEND = "python-direct"
from ble_reticulum.BLEFragmentation import BLEFragmenter, BLEReassembler
# Import optional session backend selector.
try:
from BLESessionBackend import (
BACKEND as BLE_SESSION_BACKEND,
BLEPeerSessionManager,
ConnectionId as BLESessionConnectionId,
ConnectionSnapshot as BLESessionConnectionSnapshot,
InputDecision as BLESessionInputDecision,
LocalRole as BLESessionLocalRole,
SessionActionType as BLESessionActionType,
)
except ImportError as e:
if "C++ BLE session backend is not available" in str(e):
raise
try:
from ble_reticulum.BLESessionBackend import (
BACKEND as BLE_SESSION_BACKEND,
BLEPeerSessionManager,
ConnectionId as BLESessionConnectionId,
ConnectionSnapshot as BLESessionConnectionSnapshot,
InputDecision as BLESessionInputDecision,
LocalRole as BLESessionLocalRole,
SessionActionType as BLESessionActionType,
)
except ImportError as e:
if "C++ BLE session backend is not available" in str(e):
raise
BLE_SESSION_BACKEND = "python-direct"
BLEPeerSessionManager = None
BLESessionConnectionId = None
BLESessionConnectionSnapshot = None
BLESessionInputDecision = None
BLESessionLocalRole = None
BLESessionActionType = None
# Import GATT server for peripheral mode
try:
from BLEGATTServer import BLEGATTServer
@ -412,6 +447,13 @@ class BLEInterface(Interface):
# This handles BLE link degradation where keepalives work but data doesn't.
self._last_real_data = {}
self._zombie_timeout = 30.0 # seconds - connection is zombie if no real data for this long
self._session_backend = BLE_SESSION_BACKEND
self._session_manager = None
if self._session_backend == "cpp" and BLEPeerSessionManager is not None:
self._session_manager = BLEPeerSessionManager(
self._pending_identity_timeout,
self._zombie_timeout,
)
# Fragmentation
self.fragmenters = {} # address -> BLEFragmenter (per MTU)
@ -477,6 +519,7 @@ class BLEInterface(Interface):
f"reassembler={BLEReassembler.__module__}.{BLEReassembler.__name__})",
RNS.LOG_NOTICE
)
RNS.log(f"{self} session backend: {self._session_backend}", RNS.LOG_NOTICE)
if os.environ.get("BLE_RETICULUM_FRAGMENTATION_BACKEND_REPORT", "").lower() in ["1", "yes", "true", "on"]:
print(
f"{self} fragmentation backend: {BLE_FRAGMENTATION_BACKEND} "
@ -1199,7 +1242,152 @@ class BLEInterface(Interface):
self.address_to_interface[address] = existing_if
RNS.log(f"{self} updated peer interface address for MAC rotation: {old_addr} -> {address}", RNS.LOG_DEBUG)
def _get_session_backend(self):
return getattr(self, "_session_backend", BLE_SESSION_BACKEND)
def _get_session_manager(self):
manager = getattr(self, "_session_manager", None)
if manager is None:
if BLEPeerSessionManager is None:
raise ImportError(
"C++ BLE session backend is not available. Build or install "
"ble_protocol_core_cpp, or set BLE_RETICULUM_SESSION_BACKEND=python."
)
manager = BLEPeerSessionManager(
getattr(self, "_pending_identity_timeout", 30),
getattr(self, "_zombie_timeout", 30),
)
self._session_manager = manager
return manager
def _build_session_snapshot(self, address: str, data: bytes):
snapshot = BLESessionConnectionSnapshot()
snapshot.current = BLESessionConnectionId(address)
snapshot.local_role = BLESessionLocalRole.Peripheral
peer_identity = self.address_to_identity.get(address)
if peer_identity:
snapshot.set_known_identity(peer_identity)
if len(data) == 16:
identity_hash = self._compute_identity_hash(bytes(data))
existing_address = self.identity_to_address.get(identity_hash)
if existing_address is not None:
snapshot.existing_address_for_identity = existing_address
snapshot.identity_has_pending_detach = identity_hash in self._pending_detach
snapshot.existing_address_connected = existing_address in self.driver.connected_peers
snapshot.existing_address_in_peer_table = existing_address in self.peers
last_real_data = self._last_real_data.get(identity_hash, 0)
snapshot.existing_last_real_data = last_real_data
snapshot.existing_connection_is_zombie = (
last_real_data > 0 and time.time() - last_real_data > self._zombie_timeout
)
try:
mtu = self.driver.get_peer_mtu(address)
except Exception:
mtu = None
if mtu:
snapshot.negotiated_mtu = int(mtu)
return snapshot
def _handle_identity_handshake_cpp(self, address: str, data: bytes) -> bool:
try:
manager = self._get_session_manager()
snapshot = self._build_session_snapshot(address, data)
result = manager.handle_identity_handshake(snapshot, bytes(data), time.time())
if result.decision == BLESessionInputDecision.PassToReassembler:
return False
if result.decision == BLESessionInputDecision.ConsumedDuplicateSameIdentity:
RNS.log(f"{self} received duplicate identity handshake from {address} (already known via callback)", RNS.LOG_DEBUG)
return True
if result.decision == BLESessionInputDecision.ConsumedDuplicateMismatchedIdentity:
RNS.log(f"{self} received 16-byte data from {address} that differs from known identity, consuming as handshake", RNS.LOG_WARNING)
return True
if result.decision == BLESessionInputDecision.RejectedDuplicateIdentity:
RNS.log(
f"{self} duplicate identity rejected for {address} in peripheral mode (MAC rotation)",
RNS.LOG_WARNING
)
try:
self.driver.disconnect(address)
except Exception as e:
RNS.log(f"{self} failed to disconnect duplicate {address}: {e}", RNS.LOG_DEBUG)
return True
if result.decision != BLESessionInputDecision.AcceptedNewIdentity:
return True
central_identity = result.peer_identity
if central_identity is None:
central_identity = bytes(data)
identity_hash = result.identity_key or self._compute_identity_hash(central_identity)
for action in result.actions:
if action.type == BLESessionActionType.CleanupOldAddress and action.old_address:
self._cleanup_stale_address(identity_hash, action.old_address)
elif action.type == BLESessionActionType.DisconnectOldPeer and action.old_address:
try:
self.driver.disconnect(action.old_address)
except Exception as e:
RNS.log(f"{self} failed to disconnect zombie {action.old_address}: {e}", RNS.LOG_DEBUG)
self.address_to_identity[address] = central_identity
self.identity_to_address[identity_hash] = address
RNS.log(f"{self} received identity handshake from {address}: {identity_hash}", RNS.LOG_INFO)
mtu = result.mtu or 23
frag_key = result.fragmenter_key or self._get_fragmenter_key(central_identity, address)
with self.frag_lock:
self.fragmenters[frag_key] = BLEFragmenter(mtu=mtu)
if frag_key not in self.reassemblers:
self.reassemblers[frag_key] = BLEReassembler()
if identity_hash not in self.spawned_interfaces:
peer_name = f"Central-{address[-8:]}"
connection_type = "peripheral"
self._spawn_peer_interface(
address=address,
name=peer_name,
peer_identity=central_identity,
mtu=mtu,
connection_type=connection_type
)
else:
existing_if = self.spawned_interfaces[identity_hash]
old_addr = existing_if.peer_address
if old_addr != address:
existing_if.peer_address = address
self.address_to_interface[address] = existing_if
RNS.log(f"{self} updated peer interface address for MAC rotation: {old_addr} -> {address}", RNS.LOG_DEBUG)
RNS.log(f"{self} identity handshake complete for {address}", RNS.LOG_INFO)
self._last_real_data[identity_hash] = time.time()
if address in self._pending_identity_connections:
del self._pending_identity_connections[address]
return True
except Exception as e:
RNS.log(f"{self} failed to process identity handshake via C++ session backend from {address}: {e}", RNS.LOG_ERROR)
return True
def _handle_identity_handshake(self, address: str, data: bytes) -> bool:
if self._get_session_backend() == "cpp":
return self._handle_identity_handshake_cpp(address, data)
return self._handle_identity_handshake_python(address, data)
def _handle_identity_handshake_python(self, address: str, data: bytes) -> bool:
"""
Handle identity handshake from central device (peripheral role only).

View file

@ -0,0 +1,104 @@
import os
BACKEND_ENV_VAR = "BLE_RETICULUM_SESSION_BACKEND"
DEFAULT_BACKEND = "auto"
VALID_BACKENDS = {"auto", "cpp", "python"}
def _requested_backend():
value = os.environ.get(BACKEND_ENV_VAR, DEFAULT_BACKEND).strip().lower()
if value not in VALID_BACKENDS:
raise ValueError(
f"Invalid {BACKEND_ENV_VAR}={value!r}; expected 'auto', 'cpp', or 'python'"
)
return value
def _load_cpp_backend():
try:
from ble_protocol_core_cpp import (
BLEPeerSessionManager,
ConnectionId,
ConnectionSnapshot,
InputDecision,
LocalRole,
SessionActionType,
)
except ImportError as e:
raise ImportError(
"C++ BLE session backend is not available. Build or install "
"ble_protocol_core_cpp, or set BLE_RETICULUM_SESSION_BACKEND=python."
) from e
return (
"cpp",
BLEPeerSessionManager,
ConnectionId,
ConnectionSnapshot,
InputDecision,
LocalRole,
SessionActionType,
)
def _load_python_backend():
return ("python", None, None, None, None, None, None)
REQUESTED_BACKEND = _requested_backend()
if REQUESTED_BACKEND == "python":
(
BACKEND,
BLEPeerSessionManager,
ConnectionId,
ConnectionSnapshot,
InputDecision,
LocalRole,
SessionActionType,
) = _load_python_backend()
elif REQUESTED_BACKEND == "cpp":
(
BACKEND,
BLEPeerSessionManager,
ConnectionId,
ConnectionSnapshot,
InputDecision,
LocalRole,
SessionActionType,
) = _load_cpp_backend()
else:
try:
(
BACKEND,
BLEPeerSessionManager,
ConnectionId,
ConnectionSnapshot,
InputDecision,
LocalRole,
SessionActionType,
) = _load_cpp_backend()
except ImportError:
(
BACKEND,
BLEPeerSessionManager,
ConnectionId,
ConnectionSnapshot,
InputDecision,
LocalRole,
SessionActionType,
) = _load_python_backend()
__all__ = [
"BACKEND",
"BACKEND_ENV_VAR",
"REQUESTED_BACKEND",
"BLEPeerSessionManager",
"ConnectionId",
"ConnectionSnapshot",
"InputDecision",
"LocalRole",
"SessionActionType",
]