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
+33 -1
View File
@@ -8,7 +8,8 @@
// Custom GATT UUIDs for data channel
#define SERVICE_UUID "4c41555a-4465-7669-6365-000000000001"
#define RX_CHAR_UUID "4c41555a-4465-7669-6365-000000000002" // host writes here
#define TX_CHAR_UUID "4c41555a-4465-7669-6365-000000000003" // device notifies here
#define TX_CHAR_UUID "4c41555a-4465-7669-6365-000000000003" // device ack/nack notifies
#define REQ_CHAR_UUID "4c41555a-4465-7669-6365-000000000004" // device-initiated refresh request
#define BLE_BUF_SIZE 512
@@ -45,11 +46,13 @@ static NimBLEHIDDevice* hid_dev = nullptr;
static NimBLECharacteristic* input_kbd = nullptr;
static NimBLECharacteristic* tx_char = nullptr;
static NimBLECharacteristic* rx_char = nullptr;
static NimBLECharacteristic* req_char = nullptr;
static ble_state_t state = BLE_STATE_INIT;
static bool need_advertise = false;
static char rx_buf[BLE_BUF_SIZE];
static volatile bool data_ready = false;
static volatile bool has_received_data = false;
static char mac_str[18];
static void start_advertising() {
@@ -86,6 +89,19 @@ class RxCallbacks : public NimBLECharacteristicCallbacks {
memcpy(rx_buf, val.c_str(), len);
rx_buf[len] = '\0';
data_ready = true;
has_received_data = true;
}
};
// When the daemon enables notifications on the refresh char, ask for data
// if we have none yet. Firing on subscribe (not on connect) ensures the
// notification isn't dropped before the daemon's CCCD write completes.
class ReqCallbacks : public NimBLECharacteristicCallbacks {
void onSubscribe(NimBLECharacteristic* chr, NimBLEConnInfo& info, uint16_t subValue) override {
Serial.printf("BLE: req_char onSubscribe subValue=%u has_data=%d\n", subValue, has_received_data ? 1 : 0);
if (subValue != 0 && !has_received_data) {
ble_request_refresh();
}
}
};
@@ -128,6 +144,13 @@ void ble_init(void) {
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
);
req_char = svc->createCharacteristic(
REQ_CHAR_UUID,
NIMBLE_PROPERTY::NOTIFY
);
static ReqCallbacks reqCb;
req_char->setCallbacks(&reqCb);
svc->start();
server->start();
start_advertising();
@@ -186,6 +209,15 @@ void ble_send_nack(void) {
}
}
void ble_request_refresh(void) {
if (state == BLE_STATE_CONNECTED && req_char) {
uint8_t v = 0x01;
req_char->setValue(&v, 1);
req_char->notify();
Serial.println("BLE: refresh requested");
}
}
void ble_keyboard_press(uint8_t key, uint8_t modifier) {
if (state != BLE_STATE_CONNECTED || !input_kbd) return;
// HID report: [modifier, reserved, key1, key2, key3, key4, key5, key6]
+1
View File
@@ -18,6 +18,7 @@ bool ble_has_data(void);
const char* ble_get_data(void);
void ble_send_ack(void);
void ble_send_nack(void);
void ble_request_refresh(void);
// BLE HID keyboard
void ble_keyboard_press(uint8_t key, uint8_t modifier);