* fix(ble): Increase D-Bus monitoring intervals to prevent HCI errors
The D-Bus monitoring threads were polling too frequently (0.5s and 30s),
causing HCI command collisions on BCM43xx single-radio chips. These chips
cannot handle concurrent BLE operations, and the frequent D-Bus activity
was interfering with scan/advertise cycles.
Changes:
- Increase D-Bus disconnect monitor interval from 0.5s to 5s
- Increase stale connection poll interval from 30s to 120s
This eliminates HCI errors (Opcode 0x2005/0x2006) while preserving
disconnect detection functionality with slightly higher latency.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(ble): Convert D-Bus monitoring to event-driven approach
Replace polling-based D-Bus monitoring with true event-driven pattern:
1. D-Bus monitor thread:
- Use asyncio.Event instead of periodic sleep
- Store loop reference for thread-safe shutdown
- Use call_soon_threadsafe to wake loop on stop
2. Stale poll thread:
- Replace busy-wait loop (240 x 0.5s) with single Event.wait()
- Increase interval from 120s to 300s (safety net only)
- Immediate response to stop signal
Benefits:
- Zero CPU usage while waiting (no periodic wakeups)
- Immediate shutdown response (ms instead of 5s)
- Cleaner, simpler code
- Maintains disconnect detection via D-Bus signals
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test(ble): Add comprehensive unit tests for HCI error fixes
Add 26 new unit tests for the event-driven D-Bus monitoring fixes
that eliminated HCI errors on BCM43xx single-radio chips.
Test coverage:
- TestEventDrivenDBusMonitor: Tests asyncio.Event usage, immediate
wake response, call_soon_threadsafe cross-thread signaling
- TestStalePollImprovements: Tests threading.Event.wait() usage,
300s interval, immediate stop response
- TestStopShutdownBehavior: Tests stop() async signaling, RuntimeError
handling, shutdown latency improvement
- TestIntegrationScenarios: Tests full lifecycle, multiple stop calls
- TestCodeVerification: Verifies actual source patterns match expected
All 26 tests pass without requiring pytest-asyncio plugin.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(tests): Use threading.RLock instead of asyncio.Lock in test fixtures
In Python 3.8/3.9, asyncio.Lock() requires a running event loop. When
test_hci_error_fixes.py runs first (alphabetically) and uses asyncio.run(),
it closes the event loop after each test. Subsequent test fixtures that
create asyncio.Lock() then fail with "no current event loop" errors.
Since these are mock fixtures that don't need async semantics, use
threading.RLock() instead.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(tests): Replace all asyncio.Lock() with threading.RLock() in test mocks
asyncio.Lock() requires a running event loop in Python 3.8/3.9. When
test files using asyncio.run() execute first, the event loop is closed,
causing subsequent test fixtures to fail when creating asyncio.Lock().
Fixed in:
- test_peripheral_disconnect_cleanup.py (mock_gatt_server fixture)
- test_bluez_state_cleanup.py (mock_driver fixture)
- test_ble_peer_interface.py (create_mock_peer_interface helper)
- conftest.py (create_mock_interface helper)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: torlando-tech <torlando-tech@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The _cleanup_stale_interface() method was cleaning up identity_to_address
but not the reverse mapping address_to_identity. This caused:
- Memory leak: stale entries accumulate over time
- Inconsistent state: bidirectional mappings become out of sync
This fix matches the cleanup pattern in _disconnected_callback() which
properly cleans up both directions of the mapping.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
pip extracts package metadata from the wheel filename, which must follow
PEP 427 format: {package}-{version}-{pythontag}-{abi}-{platform}.whl
The previous filename (dbus_fast-armv6l-$$.whl) caused pip to fail with
"Invalid wheel filename (wrong number of parts)" on 32-bit ARM systems.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Resolved conflicts:
- BLEInterface.py, test_v2_2_mac_sorting.py, CLAUDE.md: Keep release
(HW_MTU fix, pending_mtu, MAC rotation recovery/bypass, corrected tests)
- install.sh, README.md: Keep main (JustWorksRepairing auto-configuration)
- CHANGELOG.md: Consolidated detailed notes from main into [0.2.2] section
- Added FUNDING.yml from main
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add TestMACRotationBypassesSorting class with 4 tests for the MAC rotation fix
- Fix MockInterface to properly mock RNS.Interfaces.Interface module
- Add get_config_obj() to MockInterface for BLEInterface initialization
- Fix inverted test expectations in test_sequential_mac_addresses and
test_mac_sorting_with_multiple_peers (lower MAC initiates connection)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
After detecting MAC rotation and cleaning up the stale interface,
immediately add the peer to scored_peers and continue, bypassing
the MAC sorting check. This ensures reconnection always happens
after MAC rotation regardless of which device has the higher MAC.
Bug: After MAC rotation, peer interface wasn't recreated because
the code fell through to MAC sorting check which could skip the
peer if local MAC > peer MAC.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Enable JustWorksRepairing = always for automatic BLE pairing
- Disable GATT caching (Cache = no) to prevent Service Changed
subscription pairing prompts on Android 15+ devices
- Track config changes and only restart BlueZ when needed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Reorder operations in handle_peripheral_data() to create
fragmenter/reassembler BEFORE spawning peer interface. This
prevents data from being dropped during the brief window when
the interface exists but the reassembler doesn't.
Also adds unit tests to verify the fix and prevent regression.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Reorder operations in handle_peripheral_data() to create
fragmenter/reassembler BEFORE spawning peer interface. This
prevents data from being dropped during the brief window when
the interface exists but the reassembler doesn't.
Also adds unit tests to verify the fix and prevent regression.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
When a peer reconnects from a new MAC address (common with Android MAC
rotation, also possible on Linux after adapter reset), the old interface
state may become stale. This change adds:
1. _cleanup_stale_interface() method:
- Detaches old interface
- Cleans up identity mappings
- Removes fragmenter/reassembler for old address
- Clears pending MTU for old address
2. MAC rotation detection in _select_peers_to_connect():
- If interface exists for identity but old address is not in peers
(connection dead), clean up stale state and allow reconnection
- If old connection still active, skip as before (no change)
This is defensive code that handles edge cases like:
- Android MAC rotation (~15 min intervals)
- Linux adapter reset while peer state is tracked
- Connection timeout without proper disconnect callback
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Two critical fixes ported from Android testing:
1. HW_MTU instance attribute fix:
- Base Interface.__init__() sets self.HW_MTU = None
- BLEInterface.HW_MTU = 500 is a CLASS attribute, not instance
- After super().__init__(), self.HW_MTU is None (instance shadows class)
- BLEPeerInterface copies: self.HW_MTU = parent.HW_MTU (gets None)
- When HW_MTU is None, Transport.py truncates packet.data incorrectly
- Result: Link establishment fails with mismatched link_id
- Fix: Explicitly set self.HW_MTU = BLEInterface.HW_MTU after super().__init__()
2. MTU/identity race condition fix:
- MTU negotiation can complete before identity is received
- Previously: Warning logged, fragmenter not created
- Now: Store pending MTU, create fragmenter when identity arrives
- Adds pending_mtu dict to track deferred MTU values
- _device_connected_callback checks for pending MTU after storing identity
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update CHANGELOG.md with release notes for v0.2.2
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Removed .github/workflows/deploy.yaml which was a byte-for-byte duplicate
of deploy.yml. Both workflows were executing independently, causing:
- Duplicate deployments to Raspberry Pi devices
- Potential race conditions from concurrent SSH sessions
- Unnecessary resource usage
Kept deploy.yml as it uses the more standard extension in the GitHub
Actions ecosystem.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Removed .github/workflows/deploy.yaml which was a byte-for-byte duplicate
of deploy.yml. Both workflows were executing independently, causing:
- Duplicate deployments to Raspberry Pi devices
- Potential race conditions from concurrent SSH sessions
- Unnecessary resource usage
Kept deploy.yml as it uses the more standard extension in the GitHub
Actions ecosystem.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Adds Step 5C to install.sh that automatically configures BlueZ's
JustWorksRepairing setting to "always" in /etc/bluetooth/main.conf.
This enables automatic pairing for peer-initiated BLE connections,
which is required for zero-touch mesh networking.
Changes:
- install.sh: Add Step 5C to configure JustWorksRepairing
- Checks current setting and only modifies if needed
- Handles both root and non-root execution
- Restarts bluetooth service to apply changes
- Container-friendly with non-fatal restart operations
- README.md: Document JustWorksRepairing in manual installation steps
- README.md: Add troubleshooting section for JustWorksRepairing warning
Fixes#13🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Enhances the installation script to automatically detect and handle
pipx installations of RNS, eliminating the need for manual dependency
injection in most cases.
Changes to install.sh:
- Add pipx detection logic that checks for RNS in pipx paths
- Verify pipx command availability and RNS listing
- Install build dependencies (build-essential, python3-dev, libdbus-dev)
for Debian/Ubuntu and (base-devel, gobject-introspection) for Arch
- Implement automated pipx inject for all BLE dependencies
(bleak, bluezero, dbus-python)
- Add progress messages for long-running dbus-python compilation
- Verify all dependencies after injection
- Use correct Python executable for setcap based on install mode
Changes to README.md:
- Update Option A description to mention pipx detection
- Add note to Option C that install.sh now handles pipx automatically
- Keep manual instructions for troubleshooting/fallback
Benefits:
- Consistent "one-command installation" experience for all users
- Reduces user errors from manual pipx injection
- Provides clear error messages with recovery instructions
- Maintains manual documentation as fallback
Related to #11🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Enhances the installation script to automatically detect and handle
pipx installations of RNS, eliminating the need for manual dependency
injection in most cases.
Changes to install.sh:
- Add pipx detection logic that checks for RNS in pipx paths
- Verify pipx command availability and RNS listing
- Install build dependencies (build-essential, python3-dev, libdbus-dev)
for Debian/Ubuntu and (base-devel, gobject-introspection) for Arch
- Implement automated pipx inject for all BLE dependencies
(bleak, bluezero, dbus-python)
- Add progress messages for long-running dbus-python compilation
- Verify all dependencies after injection
- Use correct Python executable for setcap based on install mode
Changes to README.md:
- Update Option A description to mention pipx detection
- Add note to Option C that install.sh now handles pipx automatically
- Keep manual instructions for troubleshooting/fallback
Benefits:
- Consistent "one-command installation" experience for all users
- Reduces user errors from manual pipx injection
- Provides clear error messages with recovery instructions
- Maintains manual documentation as fallback
Related to #11🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes#11
When RNS is installed via pipx, the BLE interface cannot access
system-installed packages like python-dbus due to pipx's isolated
virtual environments. This commit adds comprehensive documentation
for pipx users.
Changes:
- Add new "Option C: pipx Installation" section with step-by-step
instructions for installing BLE dependencies via pipx inject
- Include system dependency installation for both Arch and Debian/Ubuntu
- Document how to find and grant capabilities to pipx Python executable
- Add BlueZ experimental mode configuration steps
- Explain why pipx requires special handling
- Add troubleshooting entry for ModuleNotFoundError issues with pipx
The documented solution uses `pipx inject rns bleak==1.1.1 bluezero dbus-python`
to install BLE dependencies into the isolated RNS environment.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes#11
When RNS is installed via pipx, the BLE interface cannot access
system-installed packages like python-dbus due to pipx's isolated
virtual environments. This commit adds comprehensive documentation
for pipx users.
Changes:
- Add new "Option C: pipx Installation" section with step-by-step
instructions for installing BLE dependencies via pipx inject
- Include system dependency installation for both Arch and Debian/Ubuntu
- Document how to find and grant capabilities to pipx Python executable
- Add BlueZ experimental mode configuration steps
- Explain why pipx requires special handling
- Add troubleshooting entry for ModuleNotFoundError issues with pipx
The documented solution uses `pipx inject rns bleak==1.1.1 bluezero dbus-python`
to install BLE dependencies into the isolated RNS environment.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes integration test failures where TestRealWorldScenario tests
couldn't access the mock_driver fixture.
The mock_driver fixture was defined inside TestPeripheralDisconnectCleanup
class, making it unavailable to TestRealWorldScenario class. This caused
pytest fixture lookup errors:
- test_both_monitoring_mechanisms_detect_disconnect_idempotent
- test_polling_catches_missed_dbus_signal
Solution: Move mock_driver to module level (outside class) so all test
classes can access it as a shared fixture.
All integration tests now pass locally.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes integration test failures where TestRealWorldScenario tests
couldn't access the mock_driver fixture.
The mock_driver fixture was defined inside TestPeripheralDisconnectCleanup
class, making it unavailable to TestRealWorldScenario class. This caused
pytest fixture lookup errors:
- test_both_monitoring_mechanisms_detect_disconnect_idempotent
- test_polling_catches_missed_dbus_signal
Solution: Move mock_driver to module level (outside class) so all test
classes can access it as a shared fixture.
All integration tests now pass locally.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes installer failures in container environments due to missing sudo command.
The BlueZ LE-only mode configuration section was attempting to modify
/etc/bluetooth/main.conf using sudo, even in container environments where:
1. Bluetooth hardware is not available
2. sudo is often not installed (containers run as root)
3. BlueZ configuration is not applicable
Now detects container environments using is_container() and skips the
LE-only mode configuration entirely, consistent with the Bluetooth
adapter power state checks.
This prevents "sudo: command not found" errors in Debian/Ubuntu CI containers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes installer failures in container environments due to missing sudo command.
The BlueZ LE-only mode configuration section was attempting to modify
/etc/bluetooth/main.conf using sudo, even in container environments where:
1. Bluetooth hardware is not available
2. sudo is often not installed (containers run as root)
3. BlueZ configuration is not applicable
Now detects container environments using is_container() and skips the
LE-only mode configuration entirely, consistent with the Bluetooth
adapter power state checks.
This prevents "sudo: command not found" errors in Debian/Ubuntu CI containers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes three CI failures identified in workflow run #19395416465:
1. **Missing threading import** (test_peripheral_disconnect_cleanup.py)
- Added missing `import threading` to fix NameError during test setup
- Tests use threading.RLock() but import was missing
2. **Timing race condition** (test_stale_connection_polling.py)
- Increased sleep from 0.15s to 1.5s in test_polling_interval_30_seconds
- Test expects 2 polling cycles at 0.6s each, was timing out in CI
3. **Container-aware Bluetooth checks** (install.sh)
- Added is_container() helper to detect Docker/container environments
- Skip Bluetooth adapter power checks in containers (no hardware access)
- Prevents false failures from bluetoothctl crashes in CI environments
All changes are test/installer infrastructure only - no production code changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes three CI failures identified in workflow run #19395416465:
1. **Missing threading import** (test_peripheral_disconnect_cleanup.py)
- Added missing `import threading` to fix NameError during test setup
- Tests use threading.RLock() but import was missing
2. **Timing race condition** (test_stale_connection_polling.py)
- Increased sleep from 0.15s to 1.5s in test_polling_interval_30_seconds
- Test expects 2 polling cycles at 0.6s each, was timing out in CI
3. **Container-aware Bluetooth checks** (install.sh)
- Added is_container() helper to detect Docker/container environments
- Skip Bluetooth adapter power checks in containers (no hardware access)
- Prevents false failures from bluetoothctl crashes in CI environments
All changes are test/installer infrastructure only - no production code changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This workflow automates the deployment of code to multiple Raspberry Pi devices and validates the BLE interface after deployment. It includes setup, deployment, validation, and summary steps.
This workflow automates the deployment of code to multiple Raspberry Pi devices and validates the BLE interface after deployment. It includes setup, deployment, validation, and summary steps.