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
This diff is collapsed.
/*
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
./libraries/JoystickBLE ./libraries/KeyboardBLE ./libraries/MouseBLE \
./libraries/lwIP_w5500 ./libraries/lwIP_w5100 ./libraries/lwIP_enc28j60 \
./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 "*.ino" -exec astyle --suffix=none --options=./tests/astyle_examples.conf \{\} \;
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