From 7c8efa0a63ac56d8c12eac5c4344bf01d858b9b2 Mon Sep 17 00:00:00 2001 From: "Earle F. Philhower, III" Date: Fri, 7 Jun 2024 14:20:58 -0700 Subject: [PATCH] Add BLE support to BluetoothHIDMaster Support Bluetooth BLE keyboard and mice using the same HID master infrastructure as the BT Classic. --- README.md | 2 +- .../BluetoothBLEScanner.ino | 34 +++ libraries/BluetoothHCI/keywords.txt | 4 + libraries/BluetoothHCI/src/BluetoothDevice.h | 16 + libraries/BluetoothHCI/src/BluetoothHCI.cpp | 277 +++++++++++++++++- libraries/BluetoothHCI/src/BluetoothHCI.h | 19 ++ .../KeyboardPianoBLE/KeyboardPianoBLE.ino | 209 +++++++++++++ .../BluetoothHIDMaster/library.properties | 4 +- .../src/BluetoothHIDMaster.cpp | 182 +++++++++++- .../src/BluetoothHIDMaster.h | 15 +- 10 files changed, 740 insertions(+), 22 deletions(-) create mode 100644 libraries/BluetoothHCI/examples/BluetoothBLEScanner/BluetoothBLEScanner.ino create mode 100644 libraries/BluetoothHIDMaster/examples/KeyboardPianoBLE/KeyboardPianoBLE.ino diff --git a/README.md b/README.md index ebfd9ee37..718989dee 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Read the [Contributing Guide](https://github.com/earlephilhower/arduino-pico/blo # Features * Adafruit TinyUSB Arduino (USB mouse, keyboard, flash drive, generic HID, CDC Serial, MIDI, WebUSB, others) * Bluetooth on the PicoW (Classic and BLE) with Keyboard, Mouse, Joystick, and Virtual Serial -* Bluetooth Classic HID master mode (connect to BT keyboard or mouse) +* Bluetooth Classic and BLE HID master mode (connect to BT keyboard or mouse) * Generic Arduino USB Serial, Keyboard, Joystick, and Mouse emulation * WiFi (Pico W, ESP32-based ESPHost, Atmel WINC1500) * Ethernet (Wired W5500, W5100, ENC28J60) diff --git a/libraries/BluetoothHCI/examples/BluetoothBLEScanner/BluetoothBLEScanner.ino b/libraries/BluetoothHCI/examples/BluetoothBLEScanner/BluetoothBLEScanner.ino new file mode 100644 index 000000000..3e56d5ab6 --- /dev/null +++ b/libraries/BluetoothHCI/examples/BluetoothBLEScanner/BluetoothBLEScanner.ino @@ -0,0 +1,34 @@ +#include + +BluetoothHCI hci; + +void BTBasicSetup() { + l2cap_init(); + gatt_client_init(); + sm_init(); + sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT); + gap_set_default_link_policy_settings(LM_LINK_POLICY_ENABLE_SNIFF_MODE | LM_LINK_POLICY_ENABLE_ROLE_SWITCH); + hci_set_master_slave_policy(HCI_ROLE_MASTER); + hci_set_inquiry_mode(INQUIRY_MODE_RSSI_AND_EIR); + + hci.setBLEName("Pico BLE Scanner"); + hci.install(); + hci.begin(); +} + +void setup() { + delay(5000); + BTBasicSetup(); +} + +void loop() { + Serial.printf("BEGIN BLE SCAN @%lu ...", millis()); + auto l = hci.scanBLE(BluetoothHCI::any_cod); + Serial.printf("END BLE SCAN @%lu\n\n", millis()); + Serial.printf("%-8s | %-17s | %-4s | %s\n", "Class", "Address", "RSSI", "Name"); + Serial.printf("%-8s | %-17s | %-4s | %s\n", "--------", "-----------------", "----", "----------------"); + for (auto e : l) { + Serial.printf("%08lx | %17s | %4d | %s\n", e.deviceClass(), e.addressString(), e.rssi(), e.name()); + } + Serial.printf("\n\n\n"); +} diff --git a/libraries/BluetoothHCI/keywords.txt b/libraries/BluetoothHCI/keywords.txt index 7291df779..5c767ffde 100644 --- a/libraries/BluetoothHCI/keywords.txt +++ b/libraries/BluetoothHCI/keywords.txt @@ -20,9 +20,13 @@ scan KEYWORD2 scanAsyncDone KEYWORD2 scanAsyncResult KEYWORD2 +setName KEYWORD2 +scanBLE KEYWORD2 + # BTDeviceInfo deviceClass KEYWORD2 address KEYWORD2 +addressType KEYWORD2 addressString KEYWORD2 rssi KEYWORD2 name KEYWORD2 diff --git a/libraries/BluetoothHCI/src/BluetoothDevice.h b/libraries/BluetoothHCI/src/BluetoothDevice.h index 5eadedeec..fe700ae0b 100644 --- a/libraries/BluetoothHCI/src/BluetoothDevice.h +++ b/libraries/BluetoothHCI/src/BluetoothDevice.h @@ -27,14 +27,26 @@ class BTDeviceInfo { BTDeviceInfo(uint32_t dc, const uint8_t addr[6], int rssi, const char *name) { _deviceClass = dc; memcpy(_address, addr, sizeof(_address)); + _addressType = -1; sprintf(_addressString, "%02x:%02x:%02x:%02x:%02x:%02x", addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]); _rssi = rssi; _name = strdup(name); } + BTDeviceInfo(uint32_t dc, const uint8_t addr[6], int addressType, int rssi, const char *name, size_t nameLen) { + _deviceClass = dc; + memcpy(_address, addr, sizeof(_address)); + sprintf(_addressString, "%02x:%02x:%02x:%02x:%02x:%02x", addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]); + _addressType = addressType; + _rssi = rssi; + _name = (char *)malloc(nameLen + 1); + memcpy(_name, name, nameLen); + _name[nameLen] = 0; + } // Copy constructor to ensure we deep-copy the string BTDeviceInfo(const BTDeviceInfo &b) { _deviceClass = b._deviceClass; memcpy(_address, b._address, sizeof(_address)); + _addressType = b._addressType; memcpy(_addressString, b._addressString, sizeof(_addressString)); _rssi = b._rssi; _name = strdup(b._name); @@ -57,9 +69,13 @@ class BTDeviceInfo { const char *name() { return _name; } + int addressType() { + return _addressType; + } private: uint32_t _deviceClass; uint8_t _address[6]; + int _addressType; char _addressString[18]; int8_t _rssi; char *_name; diff --git a/libraries/BluetoothHCI/src/BluetoothHCI.cpp b/libraries/BluetoothHCI/src/BluetoothHCI.cpp index 31411c69a..69d93de32 100644 --- a/libraries/BluetoothHCI/src/BluetoothHCI.cpp +++ b/libraries/BluetoothHCI/src/BluetoothHCI.cpp @@ -34,13 +34,58 @@ (_BTHCICB::func = std::bind(&class::cbFcn, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), \ static_cast(_BTHCICB::callback)) +void BluetoothHCI::setBLEName(const char *name) { + if (_att) { + free(_att); + _att = nullptr; + } + _att = (uint8_t *)malloc(1 + 0x0a + 0x0d + 0x08 + strlen(name) + 0x02); + uint8_t *ptr = _att; + const uint8_t head[] = { + // ATT DB Version + 1, + // 0x0001 PRIMARY_SERVICE-GAP_SERVICE + 0x0a, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x28, 0x00, 0x18, + // 0x0002 CHARACTERISTIC-GAP_DEVICE_NAME - READ + 0x0d, 0x00, 0x02, 0x00, 0x02, 0x00, 0x03, 0x28, 0x02, 0x03, 0x00, 0x00, 0x2a + }; + memcpy(ptr, head, sizeof(head)); + ptr += sizeof(head); + *ptr++ = 8 + strlen(name); // len of item + *ptr++ = 0x00; + *ptr++ = 0x02; + *ptr++ = 0x00; + *ptr++ = 0x03; + *ptr++ = 0x00; + *ptr++ = 0x00; + *ptr++ = 0x2a; + memcpy(ptr, name, strlen(name)); + ptr += strlen(name); + // End it + *ptr++ = 0x00; + *ptr++ = 0x00; + + DEBUGV("ATTDB: "); + for (size_t i = 0; i < 1 + 0x0a + 0x0d + 0x08 + strlen(name) + 0x02; i++) { + DEBUGV("%02X ", _att[i]); + } + DEBUGV("\n"); +} void BluetoothHCI::install() { + hci_set_inquiry_mode(INQUIRY_MODE_RSSI_AND_EIR); + // Register for HCI events. hci_event_callback_registration.callback = PACKETHANDLERCB(BluetoothHCI, hci_packet_handler); hci_add_event_handler(&hci_event_callback_registration); + + // BLE set our visible name + if (_att) { + att_server_init(_att, nullptr, nullptr); + } } + void BluetoothHCI::begin() { _running = true; hci_power_control(HCI_POWER_ON); @@ -50,6 +95,8 @@ void BluetoothHCI::uninstall() { BluetoothLock b; hci_remove_event_handler(&hci_event_callback_registration); _running = false; + free(_att); + _att = nullptr; } bool BluetoothHCI::running() { @@ -88,14 +135,209 @@ std::list BluetoothHCI::scan(uint32_t mask, int scanTimeSec, bool return _btdList; } -bool BluetoothHCI::scanAsyncDone() { - return _scanning; -} -std::list BluetoothHCI::scanAsyncResult() { +std::list BluetoothHCI::scanBLE(uint32_t uuid, int scanTimeSec) { + _scanMask = uuid; + _btdList.clear(); + if (!_running) { + return _btdList; + } + _scanning = true; + while (!_hciRunning) { + DEBUGV("HCI::scanBLE(): Waiting for HCI to come up\n"); + delay(10); + } + uint32_t inquiryTime = scanTimeSec * 1000; + if (!inquiryTime) { + inquiryTime = 1000; + } + DEBUGV("HCI::scan(): BLE advertise inquiry start\n"); + // Only need to lock around the inquiry start command, not the wait + { + BluetoothLock b; + gap_set_scan_params(0, 75, 50, 0); // TODO - anything better for these params? + gap_start_scan(); + } + uint32_t scanStart = millis(); + + while (_scanning && ((millis() - scanStart) < inquiryTime)) { + delay(10); + } + DEBUGV("HCI::scanBLE(): inquiry end\n"); + gap_stop_scan(); + return _btdList; } +void BluetoothHCI::parse_advertisement_data(uint8_t *packet) { + bd_addr_t address; + gap_event_advertising_report_get_address(packet, address); + //int event_type = gap_event_advertising_report_get_advertising_event_type(packet); + int address_type = gap_event_advertising_report_get_address_type(packet); + int8_t rssi = gap_event_advertising_report_get_rssi(packet); + uint8_t adv_size = gap_event_advertising_report_get_data_length(packet); + const uint8_t * adv_data = gap_event_advertising_report_get_data(packet); + uint16_t uuid = 0; + const char *nameptr = nullptr; + int namelen = 0; + + ad_context_t context; + uint8_t uuid_128[16]; + for (ad_iterator_init(&context, adv_size, (uint8_t *)adv_data) ; ad_iterator_has_more(&context) ; ad_iterator_next(&context)) { + uint8_t data_type = ad_iterator_get_data_type(&context); + uint8_t size = ad_iterator_get_data_len(&context); + const uint8_t * data = ad_iterator_get_data(&context); + + // if (data_type > 0 && data_type < 0x1B){ + // printf(" %s: ", ad_types[data_type]); + // } + int i; + // Assigned Numbers GAP + + switch (data_type) { + case BLUETOOTH_DATA_TYPE_FLAGS: + // show only first octet, ignore rest + //for (i=0; i<8;i++){ + // if (data[0] & (1<address(), address, sizeof(address))) { + seen = true; + } + } + if (!seen) { + BTDeviceInfo btd(uuid, address, address_type, rssi, nameptr ? nameptr : "", nameptr ? namelen : 0); + _btdList.push_back(btd); + } + } +} + + +static void handle_gatt_client_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) { + UNUSED(packet_type); + UNUSED(channel); + UNUSED(size); + + gatt_client_service_t service; + gatt_client_characteristic_t characteristic; + switch (hci_event_packet_get_type(packet)) { + case GATT_EVENT_SERVICE_QUERY_RESULT: + gatt_event_service_query_result_get_service(packet, &service); + // dump_service(&service); + // services[service_count++] = service; + break; + case GATT_EVENT_CHARACTERISTIC_QUERY_RESULT: + gatt_event_characteristic_query_result_get_characteristic(packet, &characteristic); + // dump_characteristic(&characteristic); + break; + case GATT_EVENT_QUERY_COMPLETE: + // GATT_EVENT_QUERY_COMPLETE of search characteristics + // if (service_index < service_count) { + // service = services[service_index++]; + // printf("\nGATT browser - CHARACTERISTIC for SERVICE %s, [0x%04x-0x%04x]\n", + // uuid128_to_str(service.uuid128), service.start_group_handle, service.end_group_handle); + // gatt_client_discover_characteristics_for_service(handle_gatt_client_event, connection_handle, &service); + // break; + // } + // service_index = 0; + break; + default: + break; + } +} + + void BluetoothHCI::hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) { (void)channel; (void)size; @@ -158,6 +400,33 @@ void BluetoothHCI::hci_packet_handler(uint8_t packet_type, uint16_t channel, uin case GAP_EVENT_INQUIRY_COMPLETE: _scanning = false; break; + + case GAP_EVENT_ADVERTISING_REPORT: + if (_scanning) { + parse_advertisement_data(packet); + } + break; + + case HCI_EVENT_DISCONNECTION_COMPLETE: + _hciConn = HCI_CON_HANDLE_INVALID; + DEBUGV("HCI Disconnected\n"); + break; + + case HCI_EVENT_LE_META: + // wait for connection complete + if (hci_event_le_meta_get_subevent_code(packet) != HCI_SUBEVENT_LE_CONNECTION_COMPLETE) { + break; + } + DEBUGV("HCI Connected\n"); + _hciConn = hci_subevent_le_connection_complete_get_connection_handle(packet); + if (_smPair) { + sm_request_pairing(_hciConn); + } else { + // query primary services - not used yet + gatt_client_discover_primary_services(handle_gatt_client_event, _hciConn); + } + break; + default: break; } diff --git a/libraries/BluetoothHCI/src/BluetoothHCI.h b/libraries/BluetoothHCI/src/BluetoothHCI.h index 26039acfd..dc7554e18 100644 --- a/libraries/BluetoothHCI/src/BluetoothHCI.h +++ b/libraries/BluetoothHCI/src/BluetoothHCI.h @@ -32,6 +32,7 @@ class BluetoothHCI { public: void install(); + void setBLEName(const char *bleMasterName); void begin(); void uninstall(); bool running(); @@ -42,6 +43,18 @@ class BluetoothHCI { bool scanAsyncDone(); std::list scanAsyncResult(); + std::list scanBLE(uint32_t uuid, int scanTimeSec = 5); + + friend class BluetoothHIDMaster; + +protected: + hci_con_handle_t getHCIConn() { + return _hciConn; + } + void setPairOnMeta(bool v) { + _smPair = v; + } + private: void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size); btstack_packet_callback_registration_t hci_event_callback_registration; @@ -50,4 +63,10 @@ class BluetoothHCI { std::list _btdList; volatile bool _scanning = false; bool _running = false; + + // BLE specific + uint8_t *_att = nullptr; + void parse_advertisement_data(uint8_t *packet); + volatile hci_con_handle_t _hciConn = HCI_CON_HANDLE_INVALID; + bool _smPair = false; }; diff --git a/libraries/BluetoothHIDMaster/examples/KeyboardPianoBLE/KeyboardPianoBLE.ino b/libraries/BluetoothHIDMaster/examples/KeyboardPianoBLE/KeyboardPianoBLE.ino new file mode 100644 index 000000000..f3e0576f5 --- /dev/null +++ b/libraries/BluetoothHIDMaster/examples/KeyboardPianoBLE/KeyboardPianoBLE.ino @@ -0,0 +1,209 @@ +// KeyboardPiano example - Released to the public domain in 2024 by Earle F. Philhower, III +// +// Demonstrates using the BluetoothHIDMaster class to use a Bluetooth keyboard as a +// piano keyboard on the PicoW +// +// Hook up a phono plug to GP0 and GP1 (and GND of course...the 1st 3 pins on the PCB) +// Connect wired earbuds up and connect over BT from your keyboard and play some music. + + +#include +#include + +// We need the inverse map, borrow from the Keyboard library +#include +extern const uint8_t KeyboardLayout_en_US[128]; + +BluetoothHIDMaster hid; +PWMAudio pwm; +HIDKeyStream keystream; + + +int16_t sine[1000]; // One complete precalculated sine wave for oscillator use +void precalculateSine() { + for (int i = 0; i < 1000; i++) { + sine[i] = (int16_t)(2000.0 * sin(i * 2 * 3.14159 / 1000.0)); // Only make amplitude ~1/16 max so we can sum up w/o clipping + } +} + +// Simple variable frequency resampling oscillator state +typedef struct { + uint32_t key; // Identifier of which key started this tone + uint32_t pos; // Current sine offset + uint32_t step; // Delta in fixed point 16p16 format +} Oscillator; +Oscillator osc[6]; // Look, ma! 6-note polyphony! + +// Quiet down, now! +void silenceOscillators() { + noInterrupts(); + for (int i = 0; i < 6; i++) { + osc[i].pos = 0; + osc[i].step = 0; + } + interrupts(); +} + +// PWM callback, generates sum of online oscillators +void fill() { + int num_samples = pwm.availableForWrite() / 2; + int16_t buff[32 * 2]; + + while (num_samples > 63) { + // Run in 32 LR sample chunks for speed, less loop overhead + for (int o = 0; o < 32; o++) { + int32_t sum = 0; + for (int i = 0; i < 6; i++) { + if (osc[i].step) { + sum += sine[osc[i].pos >> 16]; + osc[i].pos += osc[i].step; + while (osc[i].pos >= 1000 << 16) { + osc[i].pos -= 1000 << 16; + } + } + } + if (sum > 32767) { + sum = 32767; + } else if (sum < -32767) { + sum = -32767; + } + buff[o * 2] = (int16_t) sum; + buff[o * 2 + 1] = (int16_t) sum; + } + pwm.write((const uint8_t *)buff, sizeof(buff)); + num_samples -= 64; + } +} + +// Mouse callbacks. Could keep track of global mouse position, update a cursor, etc. +void mm(void *cbdata, int dx, int dy, int dw) { + (void) cbdata; + Serial.printf("Mouse: X:%d Y:%d Wheel:%d\n", dx, dy, dw); +} + +// Buttons are sent separately from movement +void mb(void *cbdata, int butt, bool down) { + (void) cbdata; + Serial.printf("Mouse: Button %d %s\n", butt, down ? "DOWN" : "UP"); +} + +// Convert a hertz floating point into a step fixed point 16p16 +inline uint32_t stepForHz(float hz) { + const float stepHz = 1000.0 / 44100.0; + const float step = hz * stepHz; + return (uint32_t)(step * 65536.0); +} + +uint32_t keyStepMap[128]; // The frequency of any raw HID key +void setupKeyStepMap() { + for (int i = 0; i < 128; i++) { + keyStepMap[i] = 0; + } + // Implements the "standard" PC keyboard to piano setup + // https://ux.stackexchange.com/questions/46669/mapping-piano-keys-to-computer-keyboard + keyStepMap[KeyboardLayout_en_US['a']] = stepForHz(261.6256); + keyStepMap[KeyboardLayout_en_US['w']] = stepForHz(277.1826); + keyStepMap[KeyboardLayout_en_US['s']] = stepForHz(293.6648); + keyStepMap[KeyboardLayout_en_US['e']] = stepForHz(311.1270); + keyStepMap[KeyboardLayout_en_US['d']] = stepForHz(329.6276); + keyStepMap[KeyboardLayout_en_US['f']] = stepForHz(349.2282); + keyStepMap[KeyboardLayout_en_US['t']] = stepForHz(369.9944); + keyStepMap[KeyboardLayout_en_US['g']] = stepForHz(391.9954); + keyStepMap[KeyboardLayout_en_US['y']] = stepForHz(415.3047); + keyStepMap[KeyboardLayout_en_US['h']] = stepForHz(440.0000); + keyStepMap[KeyboardLayout_en_US['u']] = stepForHz(466.1638); + keyStepMap[KeyboardLayout_en_US['j']] = stepForHz(493.8833); + keyStepMap[KeyboardLayout_en_US['k']] = stepForHz(523.2511); + keyStepMap[KeyboardLayout_en_US['o']] = stepForHz(554.3653); + keyStepMap[KeyboardLayout_en_US['l']] = stepForHz(587.3295); + keyStepMap[KeyboardLayout_en_US['p']] = stepForHz(622.2540); + keyStepMap[KeyboardLayout_en_US[';']] = stepForHz(659.2551); + keyStepMap[KeyboardLayout_en_US['\'']] = stepForHz(698.4565); +} + +// We get make/break for every key which lets us hold notes while a key is depressed +void kb(void *cbdata, int key) { + bool state = (bool)cbdata; + if (state && key < 128) { + // Starting a new note + for (int i = 0; i < 6; i++) { + if (osc[i].step == 0) { + // This one is free + osc[i].key = key; + osc[i].pos = 0; + osc[i].step = keyStepMap[key]; + break; + } + } + } else { + for (int i = 0; i < 6; i++) { + if (osc[i].key == (uint32_t)key) { + osc[i].step = 0; + break; + } + } + } + // The HIDKeyStream object converts a key and state into ASCII. HID key IDs do not map 1:1 to ASCII! + // Write the key and make/break state, then read 1 ASCII char back out. + keystream.write((uint8_t)key); + keystream.write((uint8_t)state); + Serial.printf("Keyboard: %02x %s = '%c'\n", key, state ? "DOWN" : "UP", state ? keystream.read() : '-'); +} + + +// Consumer keys are the special media keys on most modern keyboards (mute/etc.) +void ckb(void *cbdata, int key) { + bool state = (bool)cbdata; + Serial.printf("Consumer: %02x %s\n", key, state ? "DOWN" : "UP"); +} + + +void setup() { + Serial.begin(); + delay(3000); + + Serial.printf("Starting HID master, put your device in pairing mode now.\n"); + + // Init the sound generator + precalculateSine(); + silenceOscillators(); + setupKeyStepMap(); + + // Setup the HID key to ASCII conversion + keystream.begin(); + + // Init the PWM audio output + pwm.setStereo(true); + pwm.setBuffers(16, 64); + pwm.onTransmit(fill); + pwm.begin(44100); + + // Mouse buttons and movement reported separately + hid.onMouseMove(mm); + hid.onMouseButton(mb); + + // We can use the cbData as a flag to see if we're making or breaking a key + hid.onKeyDown(kb, (void *)true); + hid.onKeyUp(kb, (void *)false); + + // Consumer keys are the special function ones like "mute" or "home" + hid.onConsumerKeyDown(ckb, (void *)true); + hid.onConsumerKeyUp(ckb, (void *)false); + + hid.begin(true); + + hid.connectBLE(); //Keyboard(); + // or hid.connectMouse(); +} + +void loop() { + if (BOOTSEL) { + while (BOOTSEL) { + delay(1); + } + hid.disconnect(); + hid.clearPairing(); + Serial.printf("Restarting HID master, put your device in pairing mode now.\n"); + hid.connectBLE(); //Keyboard(); + } +} diff --git a/libraries/BluetoothHIDMaster/library.properties b/libraries/BluetoothHIDMaster/library.properties index a95062a60..c833bad8f 100644 --- a/libraries/BluetoothHIDMaster/library.properties +++ b/libraries/BluetoothHIDMaster/library.properties @@ -2,8 +2,8 @@ name=BluetoothHIDMaster version=1.0 author=Earle F. Philhower, III maintainer=Earle F. Philhower, III -sentence=Classic Bluetooth HID (Keyboard/Mouse/Joystick) master mode -paragraph=Classic Bluetooth HID (Keyboard/Mouse/Joystick) master mode +sentence=Bluetooth Classic and BLE HID (Keyboard/Mouse/Joystick) master mode +paragraph=Bluetooth Classic and BLE HID (Keyboard/Mouse/Joystick) master mode category=Communication url=http://github.com/earlephilhower/arduino-pico architectures=rp2040 diff --git a/libraries/BluetoothHIDMaster/src/BluetoothHIDMaster.cpp b/libraries/BluetoothHIDMaster/src/BluetoothHIDMaster.cpp index d6ae036d6..ce50abc30 100644 --- a/libraries/BluetoothHIDMaster/src/BluetoothHIDMaster.cpp +++ b/libraries/BluetoothHIDMaster/src/BluetoothHIDMaster.cpp @@ -73,22 +73,41 @@ static_cast(CCALLBACKNAME::callback)) -void BluetoothHIDMaster::begin() { - // Initialize HID Host - hid_host_init(_hid_descriptor_storage, sizeof(_hid_descriptor_storage)); - hid_host_register_packet_handler(PACKETHANDLERCB(BluetoothHIDMaster, hid_packet_handler)); +void BluetoothHIDMaster::begin(bool ble, const char *bleName) { + _ble = ble; + if (!ble) { + // Initialize HID Host + hid_host_init(_hid_descriptor_storage, sizeof(_hid_descriptor_storage)); + hid_host_register_packet_handler(PACKETHANDLERCB(BluetoothHIDMaster, hid_packet_handler)); + } else { + if (bleName) { + _hci.setBLEName(bleName); + } + _hci.setPairOnMeta(true); + } // Initialize L2CAP l2cap_init(); // Initialize LE Security Manager. Needed for cross-transport key derivation + if (ble) { + // register for events from Security Manager + _sm_event_callback_registration.callback = PACKETHANDLERCB(BluetoothHIDMaster, sm_packet_handler); + sm_add_event_handler(&_sm_event_callback_registration); + } sm_init(); - // Allow sniff mode requests by HID device and support role switch - gap_set_default_link_policy_settings(LM_LINK_POLICY_ENABLE_SNIFF_MODE | LM_LINK_POLICY_ENABLE_ROLE_SWITCH); + if (ble) { + gatt_client_init(); + hids_client_init(_hid_descriptor_storage, sizeof(_hid_descriptor_storage)); + } else { + // Allow sniff mode requests by HID device and support role switch + gap_set_default_link_policy_settings(LM_LINK_POLICY_ENABLE_SNIFF_MODE | LM_LINK_POLICY_ENABLE_ROLE_SWITCH); + + // try to become master on incoming connections + hci_set_master_slave_policy(HCI_ROLE_MASTER); + } - // try to become master on incoming connections - hci_set_master_slave_policy(HCI_ROLE_MASTER); // enabled EIR hci_set_inquiry_mode(INQUIRY_MODE_RSSI_AND_EIR); @@ -158,7 +177,7 @@ bool BluetoothHIDMaster::connected() { } bool BluetoothHIDMaster::connect(const uint8_t *addr) { - if (!_running) { + if (!_running || _ble) { return false; } while (!_hci.running()) { @@ -170,7 +189,7 @@ bool BluetoothHIDMaster::connect(const uint8_t *addr) { } bool BluetoothHIDMaster::connectCOD(uint32_t cod) { - if (!_running) { + if (!_running || _ble) { return false; } while (!_hci.running()) { @@ -192,6 +211,39 @@ bool BluetoothHIDMaster::connectCOD(uint32_t cod) { return false; } +bool BluetoothHIDMaster::connectBLE(const uint8_t *addr, int addrType) { + if (!_running || !_ble) { + return false; + } + while (!_hci.running()) { + delay(10); + } + uint8_t a[6]; + memcpy(a, addr, sizeof(a)); + return ERROR_CODE_SUCCESS == gap_connect(a, (bd_addr_type_t)addrType); +} + +bool BluetoothHIDMaster::connectBLE() { + if (!_running || !_ble) { + return false; + } + while (!_hci.running()) { + delay(10); + } + + clearPairing(); + auto l = _hci.scanBLE(0x1812 /* HID */); + for (auto e : l) { + DEBUGV("Scan connecting %s at %s ... ", e.name(), e.addressString()); + if (connectBLE(e.address(), e.addressType())) { + DEBUGV("Connection established\n"); + return true; + } + DEBUGV("Failed\n"); + } + return false; +} + bool BluetoothHIDMaster::connectKeyboard() { return connectCOD(0x2540); } @@ -202,12 +254,14 @@ bool BluetoothHIDMaster::connectMouse() { bool BluetoothHIDMaster::disconnect() { BluetoothLock b; - if (connected()) { - hid_host_disconnect(_hid_host_cid); - } if (!_running || !connected()) { return false; } + if (!_ble && connected()) { + hid_host_disconnect(_hid_host_cid); + } else if (_ble && connected()) { + gap_disconnect(_hci.getHCIConn()); + } _hid_host_descriptor_available = false; return true; } @@ -215,7 +269,11 @@ bool BluetoothHIDMaster::disconnect() { void BluetoothHIDMaster::clearPairing() { BluetoothLock b; if (connected()) { - hid_host_disconnect(_hid_host_cid); + if (_ble) { + gap_disconnect(_hci.getHCIConn()); + } else { + hid_host_disconnect(_hid_host_cid); + } } gap_delete_all_link_keys(); _hid_host_descriptor_available = false; @@ -453,6 +511,102 @@ void BluetoothHIDMaster::hid_packet_handler(uint8_t packet_type, uint16_t channe +void BluetoothHIDMaster::sm_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) { + UNUSED(channel); + UNUSED(size); + + if (packet_type != HCI_EVENT_PACKET) { + return; + } + + switch (hci_event_packet_get_type(packet)) { + case SM_EVENT_JUST_WORKS_REQUEST: + DEBUGV("Just works requested\n"); + sm_just_works_confirm(sm_event_just_works_request_get_handle(packet)); + break; + case SM_EVENT_NUMERIC_COMPARISON_REQUEST: + DEBUGV("Confirming numeric comparison: %" PRIu32 "\n", sm_event_numeric_comparison_request_get_passkey(packet)); + sm_numeric_comparison_confirm(sm_event_passkey_display_number_get_handle(packet)); + break; + case SM_EVENT_PASSKEY_DISPLAY_NUMBER: + DEBUGV("Display Passkey: %" PRIu32 "\n", sm_event_passkey_display_number_get_passkey(packet)); + break; + case SM_EVENT_PAIRING_COMPLETE: + switch (sm_event_pairing_complete_get_status(packet)) { + case ERROR_CODE_SUCCESS: + DEBUGV("Pairing complete, success\n"); + // continue - query primary services + DEBUGV("Search for HID service.\n"); + //app_state = W4_HID_CLIENT_CONNECTED; + hids_client_connect(_hci.getHCIConn(), PACKETHANDLERCB(BluetoothHIDMaster, handle_gatt_client_event), HID_PROTOCOL_MODE_REPORT, &_hid_host_cid); + break; + case ERROR_CODE_CONNECTION_TIMEOUT: + DEBUGV("Pairing failed, timeout\n"); + break; + case ERROR_CODE_REMOTE_USER_TERMINATED_CONNECTION: + DEBUGV("Pairing failed, disconnected\n"); + break; + case ERROR_CODE_AUTHENTICATION_FAILURE: + DEBUGV("Pairing failed, reason = %u\n", sm_event_pairing_complete_get_reason(packet)); + break; + default: + break; + } + break; + default: + break; + } +} + + +void BluetoothHIDMaster::handle_gatt_client_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) { + UNUSED(packet_type); + UNUSED(channel); + UNUSED(size); + + uint8_t status; + int idx; + + if (hci_event_packet_get_type(packet) != HCI_EVENT_GATTSERVICE_META) { + return; + } + + switch (hci_event_gattservice_meta_get_subevent_code(packet)) { + case GATTSERVICE_SUBEVENT_HID_SERVICE_CONNECTED: + status = gattservice_subevent_hid_service_connected_get_status(packet); + switch (status) { + case ERROR_CODE_SUCCESS: + DEBUGV("HID service client connected, found %d services\n", gattservice_subevent_hid_service_connected_get_num_instances(packet)); + _hidConnected = true; + _hid_host_descriptor_available = true; + break; + default: + DEBUGV("HID service client connection failed, status 0x%02x.\n", status); + gap_disconnect(_hci.getHCIConn()); + //handle_outgoing_connection_error(); + break; + } + break; + + case GATTSERVICE_SUBEVENT_HID_REPORT: + idx = gattservice_subevent_hid_report_get_service_index(packet); + btstack_hid_parser_t parser; + btstack_hid_parser_init(&parser, hids_client_descriptor_storage_get_descriptor_data(_hid_host_cid, idx), hids_client_descriptor_storage_get_descriptor_len(_hid_host_cid, idx), HID_REPORT_TYPE_INPUT, gattservice_subevent_hid_report_get_report(packet), gattservice_subevent_hid_report_get_report_len(packet)); + hid_host_handle_interrupt_report(&parser); + //hid_handle_input_report( + // gattservice_subevent_hid_report_get_service_index(packet), + // gattservice_subevent_hid_report_get_report(packet), + // gattservice_subevent_hid_report_get_report_len(packet)); + break; + + default: + break; + } +} + + + + // Simplified US Keyboard with Shift modifier #define CHAR_ILLEGAL 0xff diff --git a/libraries/BluetoothHIDMaster/src/BluetoothHIDMaster.h b/libraries/BluetoothHIDMaster/src/BluetoothHIDMaster.h index 970018321..c797e19b9 100644 --- a/libraries/BluetoothHIDMaster/src/BluetoothHIDMaster.h +++ b/libraries/BluetoothHIDMaster/src/BluetoothHIDMaster.h @@ -1,5 +1,6 @@ /* Bluetooth HID Master class, can connect to keyboards, mice, and joypads + Works with Bluetooth Classic and BLE devices Copyright (c) 2024 Earle F. Philhower, III @@ -66,7 +67,10 @@ class HIDKeyStream : public Stream { class BluetoothHIDMaster { public: - void begin(); + void begin(const char *bleName) { + begin(true, bleName); + } + void begin(bool ble = false, const char *bleName = nullptr); bool connected(); void end(); bool hciRunning(); @@ -82,6 +86,10 @@ class BluetoothHIDMaster { bool connect(const uint8_t *addr); bool connectKeyboard(); bool connectMouse(); + + bool connectBLE(const uint8_t *addr, int addrType); + bool connectBLE(); + bool disconnect(); void clearPairing(); @@ -93,6 +101,7 @@ class BluetoothHIDMaster { void onConsumerKeyUp(void (*)(void *, int), void *cbData = nullptr); private: + bool _ble = false; bool connectCOD(uint32_t cod); BluetoothHCI _hci; void hid_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size); @@ -121,4 +130,8 @@ class BluetoothHIDMaster { void *_consumerKeyDownData; void (*_consumerKeyUpCB)(void *, int) = nullptr; void *_consumerKeyUpData; + + btstack_packet_callback_registration_t _sm_event_callback_registration; + void sm_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size); + void handle_gatt_client_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size); };