Replace USB serial/HID with Bluetooth Low Energy
Move all communication from USB to BLE. The device now advertises as "Claude Controller" and acts as both a BLE HID keyboard (for touch gestures) and a GATT data server (for usage updates from the daemon). - Add NimBLE-Arduino BLE module with custom GATT service + HID keyboard - Add third screen (Bluetooth) with connection status, MAC, reset button - Rewrite daemon in bash using bluetoothctl/busctl for BLE GATT writes - Add screenshot capture via LVGL snapshot over serial - Remove TinyUSB mode — normal pio upload works again - Update README with BLE architecture, screenshots, gesture docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
930055170f
commit
10b8052bcc
@@ -0,0 +1,207 @@
|
||||
#include "ble.h"
|
||||
#include <Arduino.h>
|
||||
#include <NimBLEDevice.h>
|
||||
#include <NimBLEHIDDevice.h>
|
||||
|
||||
#define DEVICE_NAME "Claude Controller"
|
||||
|
||||
// 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 BLE_BUF_SIZE 512
|
||||
|
||||
// HID keyboard report descriptor
|
||||
static const uint8_t HID_REPORT_MAP[] = {
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x06, // Usage (Keyboard)
|
||||
0xA1, 0x01, // Collection (Application)
|
||||
0x85, 0x01, // Report ID (1)
|
||||
0x05, 0x07, // Usage Page (Key Codes)
|
||||
0x19, 0xE0, // Usage Minimum (224) - Left Control
|
||||
0x29, 0xE7, // Usage Maximum (231) - Right GUI
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x01, // Logical Maximum (1)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x95, 0x08, // Report Count (8)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute) - Modifier byte
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x81, 0x01, // Input (Constant) - Reserved byte
|
||||
0x95, 0x06, // Report Count (6)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x65, // Logical Maximum (101)
|
||||
0x05, 0x07, // Usage Page (Key Codes)
|
||||
0x19, 0x00, // Usage Minimum (0)
|
||||
0x29, 0x65, // Usage Maximum (101)
|
||||
0x81, 0x00, // Input (Data, Array) - Key array (6 keys)
|
||||
0xC0, // End Collection
|
||||
};
|
||||
|
||||
static NimBLEServer* server = nullptr;
|
||||
static NimBLEHIDDevice* hid_dev = nullptr;
|
||||
static NimBLECharacteristic* input_kbd = nullptr;
|
||||
static NimBLECharacteristic* tx_char = nullptr;
|
||||
static NimBLECharacteristic* rx_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 char mac_str[18];
|
||||
|
||||
static void start_advertising() {
|
||||
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
|
||||
adv->reset();
|
||||
adv->addServiceUUID(SERVICE_UUID);
|
||||
adv->setAppearance(HID_KEYBOARD);
|
||||
adv->enableScanResponse(true);
|
||||
adv->setName(DEVICE_NAME);
|
||||
bool ok = adv->start();
|
||||
state = BLE_STATE_ADVERTISING;
|
||||
Serial.printf("BLE: advertising start=%s\n", ok ? "OK" : "FAILED");
|
||||
}
|
||||
|
||||
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||
void onConnect(NimBLEServer* s, NimBLEConnInfo& info) override {
|
||||
state = BLE_STATE_CONNECTED;
|
||||
Serial.printf("BLE: connected from %s\n", info.getAddress().toString().c_str());
|
||||
}
|
||||
|
||||
void onDisconnect(NimBLEServer* s, NimBLEConnInfo& info, int reason) override {
|
||||
state = BLE_STATE_DISCONNECTED;
|
||||
need_advertise = true;
|
||||
Serial.printf("BLE: disconnected (reason=%d)\n", reason);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
class RxCallbacks : public NimBLECharacteristicCallbacks {
|
||||
void onWrite(NimBLECharacteristic* chr, NimBLEConnInfo& info) override {
|
||||
std::string val = chr->getValue();
|
||||
size_t len = val.length();
|
||||
if (len >= BLE_BUF_SIZE) len = BLE_BUF_SIZE - 1;
|
||||
memcpy(rx_buf, val.c_str(), len);
|
||||
rx_buf[len] = '\0';
|
||||
data_ready = true;
|
||||
}
|
||||
};
|
||||
|
||||
void ble_init(void) {
|
||||
NimBLEDevice::init(DEVICE_NAME);
|
||||
NimBLEDevice::setSecurityAuth(true, false, true); // bonding, no MITM, SC
|
||||
|
||||
// Format MAC address
|
||||
NimBLEAddress addr = NimBLEDevice::getAddress();
|
||||
snprintf(mac_str, sizeof(mac_str), "%s", addr.toString().c_str());
|
||||
for (int i = 0; mac_str[i]; i++) {
|
||||
if (mac_str[i] >= 'a' && mac_str[i] <= 'f') mac_str[i] -= 32;
|
||||
}
|
||||
|
||||
server = NimBLEDevice::createServer();
|
||||
static ServerCallbacks serverCb;
|
||||
server->setCallbacks(&serverCb);
|
||||
|
||||
// --- HID keyboard service ---
|
||||
hid_dev = new NimBLEHIDDevice(server);
|
||||
hid_dev->setReportMap((uint8_t*)HID_REPORT_MAP, sizeof(HID_REPORT_MAP));
|
||||
hid_dev->setManufacturer("Anthropic");
|
||||
hid_dev->setPnp(0x02, 0x05AC, 0x820A, 0x0210); // BT SIG, generic keyboard
|
||||
hid_dev->setHidInfo(0x00, 0x02); // country=0, flags=normally connectable
|
||||
hid_dev->setBatteryLevel(100);
|
||||
input_kbd = hid_dev->getInputReport(1); // report ID 1
|
||||
|
||||
// --- Custom data service ---
|
||||
NimBLEService* svc = server->createService(SERVICE_UUID);
|
||||
|
||||
rx_char = svc->createCharacteristic(
|
||||
RX_CHAR_UUID,
|
||||
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR
|
||||
);
|
||||
static RxCallbacks rxCb;
|
||||
rx_char->setCallbacks(&rxCb);
|
||||
|
||||
tx_char = svc->createCharacteristic(
|
||||
TX_CHAR_UUID,
|
||||
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
|
||||
);
|
||||
|
||||
svc->start();
|
||||
server->start();
|
||||
start_advertising();
|
||||
|
||||
Serial.printf("BLE: init complete, MAC=%s\n", mac_str);
|
||||
}
|
||||
|
||||
void ble_tick(void) {
|
||||
if (need_advertise) {
|
||||
need_advertise = false;
|
||||
start_advertising();
|
||||
}
|
||||
}
|
||||
|
||||
ble_state_t ble_get_state(void) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const char* ble_get_device_name(void) {
|
||||
return DEVICE_NAME;
|
||||
}
|
||||
|
||||
const char* ble_get_mac_address(void) {
|
||||
return mac_str;
|
||||
}
|
||||
|
||||
void ble_clear_bonds(void) {
|
||||
NimBLEDevice::deleteAllBonds();
|
||||
Serial.println("BLE: bonds cleared");
|
||||
if (state == BLE_STATE_CONNECTED) {
|
||||
server->disconnect(server->getPeerInfo(0).getConnHandle());
|
||||
}
|
||||
need_advertise = true;
|
||||
}
|
||||
|
||||
bool ble_has_data(void) {
|
||||
return data_ready;
|
||||
}
|
||||
|
||||
const char* ble_get_data(void) {
|
||||
data_ready = false;
|
||||
return rx_buf;
|
||||
}
|
||||
|
||||
void ble_send_ack(void) {
|
||||
if (state == BLE_STATE_CONNECTED && tx_char) {
|
||||
tx_char->setValue("{\"ack\":true}");
|
||||
tx_char->notify();
|
||||
}
|
||||
}
|
||||
|
||||
void ble_send_nack(void) {
|
||||
if (state == BLE_STATE_CONNECTED && tx_char) {
|
||||
tx_char->setValue("{\"err\":true}");
|
||||
tx_char->notify();
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
uint8_t report[8] = {modifier, 0, key, 0, 0, 0, 0, 0};
|
||||
input_kbd->setValue(report, sizeof(report));
|
||||
input_kbd->notify();
|
||||
}
|
||||
|
||||
void ble_keyboard_release(void) {
|
||||
if (state != BLE_STATE_CONNECTED || !input_kbd) return;
|
||||
uint8_t report[8] = {0};
|
||||
input_kbd->setValue(report, sizeof(report));
|
||||
input_kbd->notify();
|
||||
}
|
||||
|
||||
bool ble_is_connected(void) {
|
||||
return state == BLE_STATE_CONNECTED;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user