Unverified Commit f6d13d2b authored by Earle F. Philhower, III's avatar Earle F. Philhower, III Committed by GitHub

Bluetooth Master HID and musical keyboard example (#2195)

Adds BluetoothHIDMaster and HIDKeyStream which let the PicoW connect to
and use Bluetooth Classic HID devices like keyboards and mice.

An example that lets the PicoW use a BT keyboard as a piano is
included and shows the use of the new classes.
parent f7865839
// 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 <BluetoothHIDMaster.h>
#include <PWMAudio.h>
// We need the inverse map, borrow from the Keyboard library
#include <HID_Keyboard.h>
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();
hid.connectKeyboard();
// 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.connectKeyboard();
}
}
#######################################
# Syntax Coloring Map
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
BluetoothHIDMaster KEYWORD1
HIDKeyStream KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
begin KEYWORD2
end KEYWORD2
scan KEYWORD2
scanAsyncDone KEYWORD2
scanAsyncResult KEYWORD2
connectKeyboard KEYWORD2
connectMouse KEYWORD2
hidConnected KEYWORD2
onMouseMove KEYWORD2
onMouseButton KEYWORD2
onKeyDown KEYWORD2
onKeyUp KEYWORD2
onConsumerKeyDown KEYWORD2
onConsumerKeyUp KEYWORD2
# BTDeviceInfo
deviceClass KEYWORD2
address KEYWORD2
addressString KEYWORD2
rssi KEYWORD2
name KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################
name=BluetoothHIDMaster
version=1.0
author=Earle F. Philhower, III <earlephilhower@yahoo.com>
maintainer=Earle F. Philhower, III <earlephilhower@yahoo.com>
sentence=Classic Bluetooth HID (Keyboard/Mouse/Joystick) master mode
paragraph=Classic Bluetooth HID (Keyboard/Mouse/Joystick) master mode
category=Communication
url=http://github.com/earlephilhower/arduino-pico
architectures=rp2040
dot_a_linkage=true
depends=BluetoothHCI
/*
Bluetooth HCI packet handler class
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
// Based off of the BlueKitchen HID master demo
/*
Copyright (C) 2023 BlueKitchen GmbH
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holders nor the names of
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
4. Any redistribution, use, or modification is done solely for
personal benefit and not for any commercial purpose or for
monetary gain.
THIS SOFTWARE IS PROVIDED BY BLUEKITCHEN GMBH AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BLUEKITCHEN
GMBH OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Please inquire about commercial licensing options at
contact@bluekitchen-gmbh.com
*/
#include <Arduino.h>
#include "btstack.h"
#include <list>
#include <memory>
#include <BluetoothLock.h>
#include "BluetoothHIDMaster.h"
#define CCALLBACKNAME _BTHIDCB
#include <ctocppcallback.h>
#define PACKETHANDLERCB(class, cbFcn) \
(CCALLBACKNAME<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__>::func = std::bind(&class::cbFcn, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), \
static_cast<btstack_packet_handler_t>(CCALLBACKNAME<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__ - 1>::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));
// Initialize L2CAP
l2cap_init();
// Initialize LE Security Manager. Needed for cross-transport key derivation
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);
// 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);
_hci.install();
_running = true;
_hci.begin();
}
void BluetoothHIDMaster::end() {
BluetoothLock b;
_hci.uninstall();
_running = false;
}
bool BluetoothHIDMaster::running() {
return _hci.running() && _hidConnected;
}
bool BluetoothHIDMaster::hciRunning() {
return _hci.running();
}
void BluetoothHIDMaster::onMouseMove(void (*cb)(void *, int, int, int), void *cbData) {
_mouseMoveCB = cb;
_mouseMoveData = cbData;
}
void BluetoothHIDMaster::onMouseButton(void (*cb)(void *, int, bool), void *cbData) {
_mouseButtonCB = cb;
_mouseButtonData = cbData;
}
void BluetoothHIDMaster::onKeyDown(void (*cb)(void *, int), void *cbData) {
_keyDownCB = cb;
_keyDownData = cbData;
}
void BluetoothHIDMaster::onKeyUp(void (*cb)(void *, int), void *cbData) {
_keyUpCB = cb;
_keyUpData = cbData;
}
void BluetoothHIDMaster::onConsumerKeyDown(void (*cb)(void *, int), void *cbData) {
_consumerKeyDownCB = cb;
_consumerKeyDownData = cbData;
}
void BluetoothHIDMaster::onConsumerKeyUp(void (*cb)(void *, int), void *cbData) {
_consumerKeyUpCB = cb;
_consumerKeyUpData = cbData;
}
std::list<BTDeviceInfo> BluetoothHIDMaster::scan(uint32_t mask, int scanTimeSec, bool async) {
return _hci.scan(mask, scanTimeSec, async);
}
bool BluetoothHIDMaster::scanAsyncDone() {
return _hci.scanAsyncDone();
}
std::list<BTDeviceInfo> BluetoothHIDMaster::scanAsyncResult() {
return _hci.scanAsyncResult();
}
bool BluetoothHIDMaster::connected() {
return _hidConnected && _hid_host_descriptor_available;
}
bool BluetoothHIDMaster::connect(const uint8_t *addr) {
if (!_running) {
return false;
}
while (!_hci.running()) {
delay(10);
}
uint8_t a[6];
memcpy(a, addr, sizeof(a));
return ERROR_CODE_SUCCESS == hid_host_connect(a, HID_PROTOCOL_MODE_REPORT, &_hid_host_cid);
}
bool BluetoothHIDMaster::connectCOD(uint32_t cod) {
if (!_running) {
return false;
}
while (!_hci.running()) {
delay(10);
}
uint8_t a[6];
clearPairing();
auto l = scan(cod);
for (auto e : l) {
DEBUGV("Scan connecting %s at %s ... ", e.name(), e.addressString());
memcpy(a, e.address(), sizeof(a));
if (ERROR_CODE_SUCCESS == hid_host_connect(a, HID_PROTOCOL_MODE_REPORT, &_hid_host_cid)) {
DEBUGV("Connection established\n");
return true;
}
DEBUGV("Failed\n");
}
return false;
}
bool BluetoothHIDMaster::connectKeyboard() {
return connectCOD(0x2540);
}
bool BluetoothHIDMaster::connectMouse() {
return connectCOD(0x2580);
}
bool BluetoothHIDMaster::disconnect() {
BluetoothLock b;
if (connected()) {
hid_host_disconnect(_hid_host_cid);
}
if (!_running || !connected()) {
return false;
}
_hid_host_descriptor_available = false;
return true;
}
void BluetoothHIDMaster::clearPairing() {
BluetoothLock b;
if (connected()) {
hid_host_disconnect(_hid_host_cid);
}
gap_delete_all_link_keys();
_hid_host_descriptor_available = false;
}
void BluetoothHIDMaster::hid_host_handle_interrupt_report(btstack_hid_parser_t * parser) {
uint8_t new_keys[NUM_KEYS];
uint8_t tosend[NUM_KEYS];
int tosendcnt = 0;
memset(new_keys, 0, sizeof(new_keys));
int new_keys_count = 0;
uint16_t new_consumer_key = 0;
uint8_t newMB = 0;
bool noMB = false;
bool updCons = false;
bool updKey = false;
bool updMB = false;
bool updMouse = false;
int dx = 0;
int dy = 0;
int dwheel = 0;
while (btstack_hid_parser_has_more(parser)) {
uint16_t usage_page;
uint16_t usage;
int32_t value;
btstack_hid_parser_get_field(parser, &usage_page, &usage, &value);
if (usage_page == 0x01) {
updMouse = true;
if (usage == 0x30) {
dx = value;
} else if (usage == 0x31) {
dy = value;
} else if (usage == 0x38) {
dwheel = value;
}
} else if (usage_page == 0x09) {
updMB = true;
if (usage == 0) {
noMB = true;
}
if (!noMB && value && (usage > 0) && (usage < 9)) {
newMB |= 1 << (usage - 1);
}
} else if (usage_page == 0x0c) {
updCons = true;
if (value) {
new_consumer_key = usage;
// check if usage was used last time (and ignore in that case)
if (usage == last_consumer_key) {
usage = 0;
}
if (usage == 0) {
continue;
}
if (last_consumer_key) {
if (_consumerKeyUpCB) {
_consumerKeyUpCB(_consumerKeyUpData, last_consumer_key);
}
}
if (_consumerKeyDownCB) {
_consumerKeyDownCB(_consumerKeyDownData, usage);
}
} else if (last_consumer_key == usage) {
if (_consumerKeyUpCB) {
_consumerKeyUpCB(_consumerKeyUpData, last_consumer_key);
}
}
} else if (usage_page == 0x07) {
updKey = true;
if (value) {
new_keys[new_keys_count++] = usage;
// check if usage was used last time (and ignore in that case)
int i;
for (i = 0; i < NUM_KEYS; i++) {
if (usage == last_keys[i]) {
usage = 0;
}
}
if (usage == 0) {
continue;
}
tosend[tosendcnt++] = usage;
}
}
}
if (updKey) {
bool found;
for (int i = 0; i < NUM_KEYS; i++) {
found = false;
for (int j = 0; j < NUM_KEYS; j++) {
if (last_keys[i] == new_keys[j]) {
found = true;
break;
}
}
if (!found && last_keys[i] && _keyUpCB) {
_keyUpCB(_keyUpData, last_keys[i]);
}
}
for (int i = 0; _keyDownCB && i < tosendcnt; i++) {
_keyDownCB(_keyDownData, tosend[i]);
}
memcpy(last_keys, new_keys, NUM_KEYS);
}
if (updCons) {
last_consumer_key = new_consumer_key;
}
if (updMB) {
if (lastMB != newMB) {
for (int i = 0; i < 8; i++) {
int mask = 1 << i;
if ((lastMB & mask) && !(newMB & mask) && _mouseButtonCB) {
_mouseButtonCB(_mouseButtonData, i, false);
} else if (!(lastMB & mask) && (newMB & mask) && _mouseButtonCB) {
_mouseButtonCB(_mouseButtonData, i, true);
}
}
lastMB = newMB;
}
}
if (updMouse && _mouseMoveCB) {
_mouseMoveCB(_mouseMoveData, dx, dy, dwheel);
}
}
void BluetoothHIDMaster::hid_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
(void)channel;
(void)size;
if ((packet_type != HCI_EVENT_PACKET) || (hci_event_packet_get_type(packet) != HCI_EVENT_HID_META)) {
return;
}
uint8_t status;
switch (hci_event_hid_meta_get_subevent_code(packet)) {
case HID_SUBEVENT_INCOMING_CONNECTION:
// There is an incoming connection: we can accept it or decline it.
// The hid_host_report_mode in the hid_host_accept_connection function
// allows the application to request a protocol mode.
// For available protocol modes, see hid_protocol_mode_t in btstack_hid.h file.
hid_host_accept_connection(hid_subevent_incoming_connection_get_hid_cid(packet), HID_PROTOCOL_MODE_REPORT);
break;
case HID_SUBEVENT_CONNECTION_OPENED:
// The status field of this event indicates if the control and interrupt
// connections were opened successfully.
status = hid_subevent_connection_opened_get_status(packet);
if (status != ERROR_CODE_SUCCESS) {
DEBUGV("Connection failed, status 0x%02x\n", status);
_hidConnected = false;
_hid_host_cid = 0;
return;
}
_hidConnected = true;
_hid_host_descriptor_available = false;
_hid_host_cid = hid_subevent_connection_opened_get_hid_cid(packet);
DEBUGV("HID Host connected.\n");
break;
case HID_SUBEVENT_DESCRIPTOR_AVAILABLE:
// This event will follows HID_SUBEVENT_CONNECTION_OPENED event.
// For incoming connections, i.e. HID Device initiating the connection,
// the HID_SUBEVENT_DESCRIPTOR_AVAILABLE is delayed, and some HID
// reports may be received via HID_SUBEVENT_REPORT event. It is up to
// the application if these reports should be buffered or ignored until
// the HID descriptor is available.
status = hid_subevent_descriptor_available_get_status(packet);
if (status == ERROR_CODE_SUCCESS) {
_hid_host_descriptor_available = true;
} else {
DEBUGV("Cannot handle input report, HID Descriptor is not available, status 0x%02x\n", status);
}
break;
case HID_SUBEVENT_REPORT:
// Handle input report.
if (_hid_host_descriptor_available) {
uint16_t report_len = hid_subevent_report_get_report_len(packet);
const uint8_t *report = hid_subevent_report_get_report(packet);
// check if HID Input Report
if ((report_len < 1) || (*report != 0xa1)) {
break;
}
report++;
report_len--;;
btstack_hid_parser_t parser;
btstack_hid_parser_init(&parser, hid_descriptor_storage_get_descriptor_data(_hid_host_cid), hid_descriptor_storage_get_descriptor_len(_hid_host_cid), HID_REPORT_TYPE_INPUT, report, report_len);
hid_host_handle_interrupt_report(&parser);
}
break;
case HID_SUBEVENT_SET_PROTOCOL_RESPONSE:
// For incoming connections, the library will set the protocol mode of the
// HID Device as requested in the call to hid_host_accept_connection. The event
// reports the result. For connections initiated by calling hid_host_connect,
// this event will occur only if the established report mode is boot mode.
status = hid_subevent_set_protocol_response_get_handshake_status(packet);
if (status != HID_HANDSHAKE_PARAM_TYPE_SUCCESSFUL) {
DEBUGV("Error set protocol, status 0x%02x\n", status);
break;
}
switch ((hid_protocol_mode_t)hid_subevent_set_protocol_response_get_protocol_mode(packet)) {
case HID_PROTOCOL_MODE_BOOT:
DEBUGV("Protocol mode set: BOOT.\n");
break;
case HID_PROTOCOL_MODE_REPORT:
DEBUGV("Protocol mode set: REPORT.\n");
break;
default:
DEBUGV("Unknown protocol mode.\n");
break;
}
break;
case HID_SUBEVENT_CONNECTION_CLOSED:
// The connection was closed.
_hidConnected = false;
_hid_host_cid = 0;
_hid_host_descriptor_available = false;
DEBUGV("HID Host disconnected.\n");
break;
default:
break;
}
}
// Simplified US Keyboard with Shift modifier
#define CHAR_ILLEGAL 0xff
#define CHAR_RETURN '\n'
#define CHAR_ESCAPE 27
#define CHAR_TAB '\t'
#define CHAR_BACKSPACE 0x7f
/**
English (US)
*/
static const uint8_t keytable_us_none [] = {
CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 0-3 */
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', /* 4-13 */
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', /* 14-23 */
'u', 'v', 'w', 'x', 'y', 'z', /* 24-29 */
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', /* 30-39 */
CHAR_RETURN, CHAR_ESCAPE, CHAR_BACKSPACE, CHAR_TAB, ' ', /* 40-44 */
'-', '=', '[', ']', '\\', CHAR_ILLEGAL, ';', '\'', 0x60, ',', /* 45-54 */
'.', '/', CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 55-60 */
CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 61-64 */
CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 65-68 */
CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 69-72 */
CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 73-76 */
CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 77-80 */
CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 81-84 */
'*', '-', '+', '\n', '1', '2', '3', '4', '5', /* 85-97 */
'6', '7', '8', '9', '0', '.', 0xa7, /* 97-100 */
};
static const uint8_t keytable_us_shift[] = {
CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 0-3 */
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', /* 4-13 */
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', /* 14-23 */
'U', 'V', 'W', 'X', 'Y', 'Z', /* 24-29 */
'!', '@', '#', '$', '%', '^', '&', '*', '(', ')', /* 30-39 */
CHAR_RETURN, CHAR_ESCAPE, CHAR_BACKSPACE, CHAR_TAB, ' ', /* 40-44 */
'_', '+', '{', '}', '|', CHAR_ILLEGAL, ':', '"', 0x7E, '<', /* 45-54 */
'>', '?', CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 55-60 */
CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 61-64 */
CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 65-68 */
CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 69-72 */
CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 73-76 */
CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 77-80 */
CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, CHAR_ILLEGAL, /* 81-84 */
'*', '-', '+', '\n', '1', '2', '3', '4', '5', /* 85-97 */
'6', '7', '8', '9', '0', '.', 0xb1, /* 97-100 */
};
bool HIDKeyStream::setFIFOSize(size_t size) {
if (!size || _running) {
return false;
}
_fifoSize = size + 1; // Always 1 unused entry
return true;
}
HIDKeyStream::HIDKeyStream() {
}
HIDKeyStream::~HIDKeyStream() {
end();
}
void HIDKeyStream::begin() {
if (_running) {
end();
}
_queue = new uint8_t[_fifoSize];
_writer = 0;
_reader = 0;
_lshift = false;
_rshift = false;
_holding = false;
_running = true;
}
void HIDKeyStream::end() {
if (!_running) {
return;
}
_running = false;
delete[] _queue;
}
int HIDKeyStream::peek() {
if (!_running) {
return -1;
}
if (_writer != _reader) {
return _queue[_reader];
}
return -1;
}
int HIDKeyStream::read() {
if (!_running) {
return -1;
}
if (_writer != _reader) {
auto ret = _queue[_reader];
asm volatile("" ::: "memory"); // Ensure the value is read before advancing
auto next_reader = (_reader + 1) % _fifoSize;
asm volatile("" ::: "memory"); // Ensure the reader value is only written once, correctly
_reader = next_reader;
return ret;
}
return -1;
}
int HIDKeyStream::available() {
if (!_running) {
return 0;
}
return (_fifoSize + _writer - _reader) % _fifoSize;
}
int HIDKeyStream::availableForWrite() {
return 2 * _fifoSize - available() - 1; // Every 2 write = 1 read buffer insertion
}
void HIDKeyStream::flush() {
// We always send blocking
}
size_t HIDKeyStream::write(uint8_t c) {
if (!availableForWrite()) {
return 0;
}
if (_holding) {
_holding = false;
bool state = (bool)c;
if (_heldKey == 0xe1) {
_lshift = state;
return 1;
} else if (_heldKey == 0xe6) {
_rshift = state;
return 1;
} else if (state) {
auto ascii = (_lshift || _rshift) ? keytable_us_shift[_heldKey] : keytable_us_none[_heldKey];
if (ascii != CHAR_ILLEGAL) {
auto next_writer = _writer + 1;
if (next_writer == _fifoSize) {
next_writer = 0;
}
if (next_writer != _reader) {
_queue[_writer] = ascii;
asm volatile("" ::: "memory"); // Ensure the queue is written before the written count advances
_writer = next_writer;
}
}
return 1;
} else {
return 1;
}
} else {
if ((c < sizeof(keytable_us_shift)) || (c == 0xe1) || (c == 0xe6)) {
_holding = true;
_heldKey = c;
}
return 1;
}
}
size_t HIDKeyStream::write(const uint8_t *p, size_t len) {
if (!_running || !len) {
return 0;
}
size_t cnt = 0;
for (size_t i = 0; i < len; i++) {
if (!write(p[len])) {
return cnt;
}
cnt++;
}
return cnt;
}
HIDKeyStream::operator bool() {
return _running;
}
/*
Bluetooth HID Master class, can connect to keyboards, mice, and joypads
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include <Arduino.h>
#include <list>
#include <memory>
#include <BluetoothHCI.h>
#include <btstack.h>
// Write raw key up/down events, read ASCII chars out
class HIDKeyStream : public Stream {
public:
HIDKeyStream();
~HIDKeyStream();
bool setFIFOSize(size_t size);
void begin();
void end();
virtual int peek() override;
virtual int read() override;
virtual int available() override;
virtual int availableForWrite() override;
virtual void flush() override;
virtual size_t write(uint8_t c) override;
virtual size_t write(const uint8_t *p, size_t len) override;
using Print::write;
operator bool();
private:
bool _lshift = false;
bool _rshift = false;
bool _running = false;
bool _holding = false;
uint8_t _heldKey;
// Lockless, IRQ-handled circular queue
uint32_t _writer;
uint32_t _reader;
size_t _fifoSize = 32;
uint8_t *_queue;
};
class BluetoothHIDMaster {
public:
void begin();
bool connected();
void end();
bool hciRunning();
bool running();
static const uint32_t keyboard_cod = 0x2540;
static const uint32_t mouse_cod = 0x2540;
static const uint32_t any_cod = 0;
std::list<BTDeviceInfo> scan(uint32_t mask, int scanTimeSec = 5, bool async = false);
bool scanAsyncDone();
std::list<BTDeviceInfo> scanAsyncResult();
bool connect(const uint8_t *addr);
bool connectKeyboard();
bool connectMouse();
bool disconnect();
void clearPairing();
void onMouseMove(void (*)(void *, int, int, int), void *cbData = nullptr);
void onMouseButton(void (*)(void *, int, bool), void *cbData = nullptr);
void onKeyDown(void (*)(void *, int), void *cbData = nullptr);
void onKeyUp(void (*)(void *, int), void *cbData = nullptr);
void onConsumerKeyDown(void (*)(void *, int), void *cbData = nullptr);
void onConsumerKeyUp(void (*)(void *, int), void *cbData = nullptr);
private:
bool connectCOD(uint32_t cod);
BluetoothHCI _hci;
void hid_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
uint8_t lastMB = 0;
enum { NUM_KEYS = 6 };
uint8_t last_keys[NUM_KEYS] = { 0 };
uint16_t last_consumer_key = 0;
void hid_host_handle_interrupt_report(btstack_hid_parser_t * parser);
bool _running = false;
volatile bool _hidConnected = false;
uint16_t _hid_host_cid = 0;
bool _hid_host_descriptor_available = false;
uint8_t _hid_descriptor_storage[300];
void (*_mouseMoveCB)(void *, int, int, int) = nullptr;
void *_mouseMoveData;
void (*_mouseButtonCB)(void *, int, bool) = nullptr;
void *_mouseButtonData;
void (*_keyDownCB)(void *, int) = nullptr;
void *_keyDownData;
void (*_keyUpCB)(void *, int) = nullptr;
void *_keyUpData;
void (*_consumerKeyDownCB)(void *, int) = nullptr;
void *_consumerKeyDownData;
void (*_consumerKeyUpCB)(void *, int) = nullptr;
void *_consumerKeyUpData;
};
...@@ -15,7 +15,8 @@ for dir in ./cores/rp2040 ./libraries/EEPROM ./libraries/I2S ./libraries/SingleF ...@@ -15,7 +15,8 @@ for dir in ./cores/rp2040 ./libraries/EEPROM ./libraries/I2S ./libraries/SingleF
./libraries/JoystickBLE ./libraries/KeyboardBLE ./libraries/MouseBLE \ ./libraries/JoystickBLE ./libraries/KeyboardBLE ./libraries/MouseBLE \
./libraries/lwIP_w5500 ./libraries/lwIP_w5100 ./libraries/lwIP_enc28j60 \ ./libraries/lwIP_w5500 ./libraries/lwIP_w5100 ./libraries/lwIP_enc28j60 \
./libraries/SPISlave ./libraries/lwIP_ESPHost ./libraries/FatFS\ ./libraries/SPISlave ./libraries/lwIP_ESPHost ./libraries/FatFS\
./libraries/FatFSUSB ./libraries/BluetoothAudio ./libraries/BluetoothHCI; do ./libraries/FatFSUSB ./libraries/BluetoothAudio ./libraries/BluetoothHCI \
./libraries/BluetoothHIDMaster; do
find $dir -type f \( -name "*.c" -o -name "*.h" -o -name "*.cpp" \) -a \! -path '*api*' -exec astyle --suffix=none --options=./tests/astyle_core.conf \{\} \; find $dir -type f \( -name "*.c" -o -name "*.h" -o -name "*.cpp" \) -a \! -path '*api*' -exec astyle --suffix=none --options=./tests/astyle_core.conf \{\} \;
find $dir -type f -name "*.ino" -exec astyle --suffix=none --options=./tests/astyle_examples.conf \{\} \; find $dir -type f -name "*.ino" -exec astyle --suffix=none --options=./tests/astyle_examples.conf \{\} \;
done done
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment