From a0f361a3f4338f4eb8a7fbf436bf9d03ebe02a29 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Wed, 29 Oct 2025 00:10:57 -0400 Subject: [PATCH 1/9] feat: add Bluetooth adapter power state handling and auto-power-on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the common "Not Powered" error where Bluetooth adapters are powered off, preventing BLE operations. This issue is particularly common on Raspberry Pi after boot or system updates. Changes: 1. README.md - Added comprehensive troubleshooting section - New section: "Bluetooth adapter not powered" - Documented symptoms, causes, and 4 solution methods - Instructions for automatic power-on at boot - Cross-referenced from other sections 2. BLEInterface.py - Enhanced error handling in _discover_peers() - Detect "Not Powered" errors specifically - Show clear, actionable error messages instead of stack traces - Provide direct solution commands - Link to troubleshooting documentation - Gracefully handle error without crashing 3. install.sh - Automatic power state checking - New "Step 5B: Bluetooth Adapter Power State" section - Check if adapter is powered using `bluetoothctl show` - Automatically power on adapter if needed - Verify operation succeeded - Provide troubleshooting steps if power-on fails - Check for rfkill blocks 4. examples/bluetooth-power-on.service - New systemd service - Ensures Bluetooth is powered on at boot - Optional but recommended for production - Includes installation instructions in README 5. examples/config_example.toml - Added troubleshooting entry #7 - Documents power state issue in config comments - Cross-references systemd service example - Notes that installer (v1.x+) handles this automatically Impact: - Users get clear guidance instead of cryptic stack traces - Installer automatically fixes the issue during setup - Reduces support burden for common power state errors - Enables automatic recovery at boot via systemd service Fixes: "Not Powered" / "No powered Bluetooth adapters found" errors Tested on: Raspberry Pi Zero 2 W with Raspberry Pi OS Lite 64-bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 44 +++++++++++++++++++++++++++++ examples/bluetooth-power-on.service | 12 ++++++++ examples/config_example.toml | 9 ++++++ install.sh | 39 +++++++++++++++++++++++++ src/RNS/Interfaces/BLEInterface.py | 20 +++++++++++-- 5 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 examples/bluetooth-power-on.service diff --git a/README.md b/README.md index 51ca23a..99efd5a 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,50 @@ These errors occur when BlueZ attempts Classic Bluetooth (BR/EDR) connections in **Solution:** Enable BlueZ experimental mode (see Installation → Manual Installation → step 4). If you used the automated installer, re-run it without `--skip-experimental`. +### Bluetooth adapter not powered / "No powered Bluetooth adapters found" +The Bluetooth adapter exists but is powered off, preventing BLE operations. + +**Symptoms:** +- Error: `dbus.exceptions.DBusException: org.bluez.Error.Failed: Not Powered` +- Error: `BleakError: No powered Bluetooth adapters found.` +- BLE interface fails to start or discover peers +- GATT server startup fails immediately + +**Cause:** +The Bluetooth adapter is in a powered-off state. This commonly happens on Raspberry Pi after boot or system updates. + +**Solution:** +Power on the Bluetooth adapter: + +```bash +# Option 1: Using bluetoothctl (recommended) +bluetoothctl power on + +# Option 2: If adapter is RF-blocked +sudo rfkill unblock bluetooth + +# Option 3: Using hciconfig (older systems) +sudo hciconfig hci0 up + +# Verify adapter is powered: +bluetoothctl show +# Should display "Powered: yes" +``` + +**Automatic power-on at boot:** +Ensure Bluetooth service is enabled and starts at boot: + +```bash +# Enable Bluetooth service +sudo systemctl enable bluetooth +sudo systemctl start bluetooth + +# For persistent power-on, create a systemd service: +# See examples/bluetooth-power-on.service +``` + +The automated installer (v1.x+) automatically checks and powers on the Bluetooth adapter during installation. + ## Architecture The BLE interface consists of four main components: diff --git a/examples/bluetooth-power-on.service b/examples/bluetooth-power-on.service new file mode 100644 index 0000000..a350f75 --- /dev/null +++ b/examples/bluetooth-power-on.service @@ -0,0 +1,12 @@ +[Unit] +Description=Ensure Bluetooth adapter is powered on at boot +After=bluetooth.service +Requires=bluetooth.service + +[Service] +Type=oneshot +ExecStart=/bin/sh -c 'echo -e "power on\nquit" | /usr/bin/bluetoothctl' +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target diff --git a/examples/config_example.toml b/examples/config_example.toml index 823fd75..a5bf0ef 100644 --- a/examples/config_example.toml +++ b/examples/config_example.toml @@ -266,6 +266,15 @@ power_mode = balanced # - If you used install.sh, it should have enabled this automatically # - Verify: ps aux | grep bluetoothd (should show -E flag) # +# 7. Bluetooth adapter not powered (No powered Bluetooth adapters found): +# IMPORTANT: BLE interface won't work if adapter is powered off! +# - Symptoms: "Not Powered" errors, interface fails to start, no peer discovery +# - Solution: bluetoothctl power on +# - Verify: bluetoothctl show (should display "Powered: yes") +# - For automatic power-on at boot: see examples/bluetooth-power-on.service +# - If blocked: sudo rfkill unblock bluetooth +# - The installer (v1.x+) automatically powers on the adapter +# # For more troubleshooting, see README.md # ============================================================================ diff --git a/install.sh b/install.sh index f3a3520..3ebf450 100755 --- a/install.sh +++ b/install.sh @@ -618,6 +618,45 @@ EOF fi fi +# Step 5B: Bluetooth Adapter Power State +print_header "Bluetooth Adapter Power State" + +if command -v bluetoothctl &> /dev/null; then + print_info "Checking Bluetooth adapter power state..." + + # Check if adapter is powered + if bluetoothctl show | grep -q "Powered: yes"; then + print_success "Bluetooth adapter is powered on" + else + print_warning "Bluetooth adapter is not powered" + print_info "Powering on Bluetooth adapter..." + + # Power on the adapter + echo -e "power on\nquit" | bluetoothctl > /dev/null 2>&1 + + # Verify it worked + sleep 1 + if bluetoothctl show | grep -q "Powered: yes"; then + print_success "Bluetooth adapter powered on successfully" + else + print_error "Failed to power on Bluetooth adapter" + echo + print_info "Troubleshooting steps:" + echo " 1. Check if adapter is blocked: sudo rfkill list bluetooth" + echo " 2. Unblock if needed: sudo rfkill unblock bluetooth" + echo " 3. Try manually: bluetoothctl power on" + echo " 4. Verify adapter exists: bluetoothctl list" + echo + print_warning "BLE interface may not work until adapter is powered on" + fi + fi +else + print_warning "bluetoothctl not available, cannot check adapter power state" + print_info "Ensure Bluetooth adapter is powered on before running rnsd" +fi + +echo + # Step 6: Configuration print_header "Configuration" diff --git a/src/RNS/Interfaces/BLEInterface.py b/src/RNS/Interfaces/BLEInterface.py index d5672d1..604042c 100644 --- a/src/RNS/Interfaces/BLEInterface.py +++ b/src/RNS/Interfaces/BLEInterface.py @@ -862,9 +862,23 @@ class BLEInterface(Interface): RNS.log(f"{self} scanning for peers (scan_time={scan_time:.1f}s)...", RNS.LOG_EXTREME) scanner = BleakScanner(detection_callback=detection_callback) - await scanner.start() - await asyncio.sleep(scan_time) - await scanner.stop() + try: + await scanner.start() + await asyncio.sleep(scan_time) + await scanner.stop() + except Exception as e: + error_msg = str(e) + # Check for "Not Powered" or similar adapter power issues + if "No powered Bluetooth adapters" in error_msg or "Not Powered" in error_msg: + RNS.log(f"{self} Bluetooth adapter is not powered!", RNS.LOG_ERROR) + RNS.log(f"{self} Solution: Run 'bluetoothctl power on' or 'sudo rfkill unblock bluetooth'", RNS.LOG_ERROR) + RNS.log(f"{self} See troubleshooting: https://github.com/torlando-tech/ble-reticulum#bluetooth-adapter-not-powered", RNS.LOG_ERROR) + # Don't raise, just return - the discovery loop will retry + self.scanning = False + return + else: + # Re-raise other errors + raise # Get local adapter address if we don't have it yet (for connection direction preference) if self.local_address is None: From d46ee840b026f07f3cb64471433e3955a3e0c9f2 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Wed, 29 Oct 2025 00:23:33 -0400 Subject: [PATCH 2/9] fix: add automatic rfkill unblocking to installer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automatically detect and unblock soft-blocked Bluetooth adapters during installation, preventing "Failed to set power on" errors on Raspberry Pi and other systems where rfkill may block the adapter by default. Changes: - Add rfkill soft-block detection in install.sh Step 5B - Automatically run 'sudo rfkill unblock bluetooth' when needed - Verify unblock succeeded before attempting power-on - Provide clear feedback during unblock process Fixes the root cause of power-on failures when adapter is soft-blocked, which was discovered during testing on Raspberry Pi Zero 2 W. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- install.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/install.sh b/install.sh index 3ebf450..e5563db 100755 --- a/install.sh +++ b/install.sh @@ -624,6 +624,24 @@ print_header "Bluetooth Adapter Power State" if command -v bluetoothctl &> /dev/null; then print_info "Checking Bluetooth adapter power state..." + # Check for rfkill blocks first (must be unblocked before power-on works) + if command -v rfkill &> /dev/null; then + if rfkill list bluetooth | grep -q "Soft blocked: yes"; then + print_warning "Bluetooth adapter is soft-blocked by rfkill" + print_info "Unblocking Bluetooth adapter..." + sudo rfkill unblock bluetooth + sleep 1 + + # Verify unblock succeeded + if rfkill list bluetooth | grep -q "Soft blocked: yes"; then + print_error "Failed to unblock Bluetooth adapter" + print_warning "You may need to check hardware switch or BIOS settings" + else + print_success "Bluetooth adapter unblocked successfully" + fi + fi + fi + # Check if adapter is powered if bluetoothctl show | grep -q "Powered: yes"; then print_success "Bluetooth adapter is powered on" From 9410630561da73eaf2fb8517cb90de3fe2c28fd5 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Wed, 29 Oct 2025 09:53:30 -0400 Subject: [PATCH 3/9] fix spacing for copy paste --- install.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/install.sh b/install.sh index e5563db..a43459c 100755 --- a/install.sh +++ b/install.sh @@ -687,13 +687,13 @@ echo " File: $CONFIG_FILE" echo echo " Add this section (copy-paste ready):" echo -echo " [[BLE Interface]]" -echo " type = BLEInterface" -echo " enabled = yes" +echo " [[BLE Interface]]" +echo " type = BLEInterface" +echo " enabled = yes" echo -echo " # Enable both modes for mesh" -echo " enable_peripheral = yes" -echo " enable_central = yes" +echo " # Enable both modes for mesh" +echo " enable_peripheral = yes" +echo " enable_central = yes" echo echo "2. See examples/config_example.toml for all configuration options" echo From eebdeb3907b1a74d7f9a10a9e28266fc87169dbf Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Wed, 29 Oct 2025 10:02:51 -0400 Subject: [PATCH 4/9] fix: add root detection for setcap and rfkill commands in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The installer was failing in CI Docker containers with "sudo: command not found" because Docker containers run as root and don't have sudo installed. Added root detection (checking $EUID) before setcap and rfkill commands, following the same pattern used elsewhere in the script. This allows the installer to work correctly in both scenarios: - User systems: Uses sudo when needed (non-root users) - Docker/CI containers: Runs commands directly as root (no sudo required) Fixes the CI failure at install.sh:458 where setcap was unconditionally using sudo, and prevents future failures at the rfkill command. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- install.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index a43459c..e2823cb 100755 --- a/install.sh +++ b/install.sh @@ -455,7 +455,12 @@ else # Grant capabilities if we have a valid path if [ -f "$PYTHON_PATH" ] && [ ! -L "$PYTHON_PATH" ]; then print_info "Granting capabilities to: $PYTHON_PATH" - sudo setcap 'cap_net_raw,cap_net_admin+eip' "$PYTHON_PATH" + # Use sudo only if not running as root (Docker containers run as root without sudo) + if [ "$EUID" -eq 0 ]; then + setcap 'cap_net_raw,cap_net_admin+eip' "$PYTHON_PATH" + else + sudo setcap 'cap_net_raw,cap_net_admin+eip' "$PYTHON_PATH" + fi if [ $? -eq 0 ]; then print_success "Bluetooth permissions granted successfully" @@ -629,7 +634,12 @@ if command -v bluetoothctl &> /dev/null; then if rfkill list bluetooth | grep -q "Soft blocked: yes"; then print_warning "Bluetooth adapter is soft-blocked by rfkill" print_info "Unblocking Bluetooth adapter..." - sudo rfkill unblock bluetooth + # Use sudo only if not running as root (Docker containers run as root without sudo) + if [ "$EUID" -eq 0 ]; then + rfkill unblock bluetooth + else + sudo rfkill unblock bluetooth + fi sleep 1 # Verify unblock succeeded From 53a38652f0b551f494c493d870d2dacf859eff41 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Wed, 29 Oct 2025 10:07:40 -0400 Subject: [PATCH 5/9] fix: make bluetoothctl commands non-fatal in container environments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bluetoothctl command was causing CI failures with abort signal (exit 134) in Docker containers where D-Bus is not running or Bluetooth hardware is not available. This is expected behavior in container/CI environments. Changes: - Added `|| true` to bluetoothctl power on command to prevent script failure - Added `2>/dev/null` to bluetoothctl show commands to suppress D-Bus errors - Added clarifying comment about container/CI environment behavior This allows the installer to complete successfully in CI while still attempting to configure Bluetooth on real systems where the hardware is available. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- install.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index e2823cb..353970e 100755 --- a/install.sh +++ b/install.sh @@ -653,18 +653,18 @@ if command -v bluetoothctl &> /dev/null; then fi # Check if adapter is powered - if bluetoothctl show | grep -q "Powered: yes"; then + if bluetoothctl show 2>/dev/null | grep -q "Powered: yes"; then print_success "Bluetooth adapter is powered on" else print_warning "Bluetooth adapter is not powered" print_info "Powering on Bluetooth adapter..." - # Power on the adapter - echo -e "power on\nquit" | bluetoothctl > /dev/null 2>&1 + # Power on the adapter (non-fatal in container/CI environments where D-Bus may not be running) + echo -e "power on\nquit" | bluetoothctl > /dev/null 2>&1 || true # Verify it worked sleep 1 - if bluetoothctl show | grep -q "Powered: yes"; then + if bluetoothctl show 2>/dev/null | grep -q "Powered: yes"; then print_success "Bluetooth adapter powered on successfully" else print_error "Failed to power on Bluetooth adapter" From bc839fba9333ca69bc7c195ca31b1ae2f8afa16c Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Wed, 29 Oct 2025 10:13:55 -0400 Subject: [PATCH 6/9] fix: skip setcap when running as root to avoid container issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The setcap command was causing "Operation not permitted" errors when trying to run Python in Docker containers after capabilities were applied. This is because setcap can cause security restrictions that are incompatible with container environments. Root users don't need capabilities anyway - they already have full permissions to access Bluetooth hardware. This change: - Detects when running as root (EUID == 0) - Skips the entire setcap process for root users - Adds informative messages explaining why it's being skipped - Simplifies the code by removing nested root checks This allows the installer tests to pass in CI while still properly granting capabilities for non-root users on real systems. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- install.sh | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/install.sh b/install.sh index 353970e..588351a 100755 --- a/install.sh +++ b/install.sh @@ -427,7 +427,11 @@ else fi fi - if command -v setcap &> /dev/null; then + # Skip setcap when running as root (e.g., in containers) - root already has all permissions + if [ "$EUID" -eq 0 ]; then + print_info "Running as root - skipping capability grant (not needed)" + print_info "Root user already has all required Bluetooth permissions" + elif command -v setcap &> /dev/null; then # Get python3 path PYTHON_PATH=$(which python3) print_info "Detected Python at: $PYTHON_PATH" @@ -455,12 +459,7 @@ else # Grant capabilities if we have a valid path if [ -f "$PYTHON_PATH" ] && [ ! -L "$PYTHON_PATH" ]; then print_info "Granting capabilities to: $PYTHON_PATH" - # Use sudo only if not running as root (Docker containers run as root without sudo) - if [ "$EUID" -eq 0 ]; then - setcap 'cap_net_raw,cap_net_admin+eip' "$PYTHON_PATH" - else - sudo setcap 'cap_net_raw,cap_net_admin+eip' "$PYTHON_PATH" - fi + sudo setcap 'cap_net_raw,cap_net_admin+eip' "$PYTHON_PATH" if [ $? -eq 0 ]; then print_success "Bluetooth permissions granted successfully" From 5feb5a9af143f084c204f9e8df6d4d8e2d6e5d23 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Wed, 29 Oct 2025 10:18:45 -0400 Subject: [PATCH 7/9] ci: temporarily disable unit and integration tests Disabling unit and integration test jobs to save GitHub Actions minutes while troubleshooting installer test failures in Docker containers. This reduces CI runtime from ~4-5 minutes to ~1 minute per run. To re-enable: remove the 'if: false' lines from unit-tests and integration-tests jobs in .github/workflows/test.yml --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2553f89..e48b4b6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,7 @@ jobs: unit-tests: name: Unit Tests runs-on: ubuntu-latest + if: false # Temporarily disabled to save CI minutes while troubleshooting installer tests strategy: matrix: @@ -61,6 +62,7 @@ jobs: integration-tests: name: Integration Tests runs-on: ubuntu-latest + if: false # Temporarily disabled to save CI minutes while troubleshooting installer tests strategy: matrix: From 5f47e995bca36bc9715ca08a3dc815d78ca6e888 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Wed, 29 Oct 2025 10:25:01 -0400 Subject: [PATCH 8/9] fix: save repo root path in test_installer.sh for later cd The test script was failing because it changed directories during execution (to /tmp/test-config/interfaces) and then tried to use a relative path to navigate back to the repo root, which failed. Fix: Save the absolute path to repo root at the beginning and reuse it when needed instead of calculating it relative to the current directory. Fixes the error: 'cd: tests/..: No such file or directory' --- tests/test_installer.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_installer.sh b/tests/test_installer.sh index 316c744..a361145 100755 --- a/tests/test_installer.sh +++ b/tests/test_installer.sh @@ -48,6 +48,7 @@ check_package() { echo "Running install.sh (self-contained installer)..." # Navigate to repository root (script is in tests/ directory) cd "$(dirname "$0")/.." +REPO_ROOT="$(pwd)" chmod +x install.sh mkdir -p /tmp/test-config @@ -184,7 +185,7 @@ echo "" # Test --skip-experimental flag echo "Testing --skip-experimental flag..." -cd "$(dirname "$0")/.." +cd "$REPO_ROOT" # Run with --skip-experimental to verify it doesn't fail ./install.sh --config /tmp/test-config-skip --skip-experimental > /tmp/skip-test.log 2>&1 < Date: Wed, 29 Oct 2025 10:27:16 -0400 Subject: [PATCH 9/9] fix: make systemctl commands non-fatal in container environments The systemctl commands (daemon-reload, restart bluetooth) were causing CI failures in Docker containers where systemd is not running as PID 1. Changes: - Added '|| true' and '2>/dev/null' to systemctl commands to make them non-fatal - Updated verification logic to detect container environments (/.dockerenv) - Added appropriate messages for container environments - Systemd override file is still created, but service restart is skipped This allows the installer to complete successfully in containers while still properly configuring BlueZ experimental mode on real systems with systemd. --- install.sh | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/install.sh b/install.sh index 588351a..73f94b6 100755 --- a/install.sh +++ b/install.sh @@ -587,8 +587,9 @@ else ExecStart= ExecStart=$BLUETOOTHD_PATH -E EOF - systemctl daemon-reload - systemctl restart bluetooth + # Non-fatal in container/CI environments where systemd isn't running + systemctl daemon-reload 2>/dev/null || true + systemctl restart bluetooth 2>/dev/null || true else # Not root - use sudo sudo mkdir -p /etc/systemd/system/bluetooth.service.d @@ -597,12 +598,13 @@ EOF ExecStart= ExecStart=$BLUETOOTHD_PATH -E EOF - sudo systemctl daemon-reload - sudo systemctl restart bluetooth + # Non-fatal in container/CI environments where systemd isn't running + sudo systemctl daemon-reload 2>/dev/null || true + sudo systemctl restart bluetooth 2>/dev/null || true fi - # Verify bluetooth service is running - if systemctl is-active --quiet bluetooth; then + # Verify bluetooth service is running (skip in container environments) + if systemctl is-active --quiet bluetooth 2>/dev/null; then # Double-check that -E flag is actually set if ps aux | grep bluetoothd | grep -q -- "-E"; then print_success "BlueZ experimental mode enabled successfully" @@ -610,10 +612,14 @@ EOF print_warning "Bluetooth service restarted but -E flag not detected" print_info "You may need to manually verify: ps aux | grep bluetoothd" fi - else + elif command -v systemctl &> /dev/null && [ ! -f /.dockerenv ]; then + # Only show error if systemctl exists and we're not in a container print_error "Bluetooth service failed to start" print_warning "Check status with: sudo systemctl status bluetooth" echo + else + # Container environment or systemd not available + print_info "Systemd override created (service restart skipped in container environment)" fi echo fi