Make BLE daemon resilient to device swaps and add device-initiated refresh

- Cache invalidation: drop ~/.config/claude-usage-monitor/ble-address and remove
  the dead MAC from bluez on connect failure, so swapping ESP boards no longer
  pins the daemon to a stale factory MAC
- Scan robustness: pick first matching candidate (head -1), sanity-check cached
  content for valid MAC format, handle multiple Claude Controllers in bluez
- 5s tick + 60s poll: inner loop wakes every 5s for fast disconnect detection
  while keeping Anthropic API cadence at one minute
- Refresh-request channel: new GATT characteristic ...0004 NOTIFY, firmware
  fires once in onSubscribe when has_received_data is false; daemon subscribes
  via setsid'd dbus-monitor pipeline (process group cleanup avoids the
  bash-wait-on-job hang we hit on disconnect)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hermann Björgvin Haraldsson
2026-05-09 23:44:39 +00:00
co-authored by Claude Opus 4.7
parent 97a5443601
commit 38d8894016
4 changed files with 136 additions and 15 deletions
+11 -1
View File
@@ -84,4 +84,14 @@ See `~/.claude/projects/.../memory/` files for persistent context (user is an em
## Daemon / host side
Unchanged from original project. Python daemon (`daemon/`) reads OAuth token, polls Anthropic API, sends JSON over BLE GATT. Run with `systemctl --user start claude-usage-daemon`.
Bash daemon (`daemon/claude-usage-daemon.sh`) reads OAuth token, polls Anthropic API, sends JSON over BLE GATT. Run with `systemctl --user start claude-usage-daemon`. The unit file's `ExecStart` is the absolute path to the script — repoint it when switching between the worktree and the main checkout.
**Discovery & resilience:**
- Connects by name (`"Claude Controller"`) on first run, caches resolved MAC at `~/.config/claude-usage-monitor/ble-address`. ESP32 BLE addresses are factory-burned per-chip, so swapping any board invalidates the cache.
- On connect failure: cache is dropped AND device is removed from bluez (`bluetoothctl remove`) so the next scan won't re-pick a dead MAC. Multi-candidate scans pick `head -1` and let the failure cycle converge.
- `POLL_INTERVAL=60`, `TICK=5`. Inner loop wakes every 5s to detect disconnects fast; polls Anthropic when 60s elapsed OR when ESP fires a refresh request.
**GATT characteristics on service `4c41555a-...0001`:**
- `...0002` RX — daemon writes JSON usage payload here.
- `...0003` TX — firmware notifies ack/nack (daemon doesn't subscribe).
- `...0004` REQ — firmware fires `0x01` notify in `onSubscribe` if `has_received_data` is false. Daemon subscribes via `setsid bash -c "stdbuf -oL dbus-monitor … | awk …"`; awk drops a flag file the inner loop picks up. See the `feedback_dbus_monitor_pipe` memory for the three subtle gotchas (pipe buffering, busctl-exits race, `wait` blocking on pipeline jobs).