/* * Arduino code for a Bluetooth version of the Chorder. * @author: clc@clcworld.net * additional code by: priestdo@budgardr.org * tinyusb adaptation by zachlash with gen AI assist * This version tested on pi pico button board * * This is new arduino code based on/inspired by the SpiffChorder * which can be found at http://symlink.dk/projects/spiffchorder/ * * changes Feb. 2025 * - Replaced BLE HID device with TinyUSB * - Removed Bluefruit and Battery specific functions * - Modified sendRawKeyDn(), SendRawKeyUp(), and sendControlKey() to use USB with same function calls * * Forked from FeatherChorder (BLE) at https://github.com/clc/chorder/commit/82546f74d6e33fa860c4a671e82515283c2588f9 * * Kept in sync with all changes made to BLE version since original fork. * See this section in FeatherChorder.ino for additional changes. * * Last mucked with on: 2025/03/26 */ #include #include #include "Adafruit_TinyUSB.h" // Define unique report IDs #define REPORT_ID_KEYBOARD 1 #define REPORT_ID_CONSUMER_CONTROL 2 // USB HID Object Adafruit_USBD_HID usb_hid; // HID report descriptor: Includes both Keyboard and Consumer Control (Media) keys uint8_t const desc_hid_report[] = { TUD_HID_REPORT_DESC_KEYBOARD( HID_REPORT_ID(1) ), // Keyboard TUD_HID_REPORT_DESC_CONSUMER( HID_REPORT_ID(2) ) // Media keys (Consumer Control) }; // Setup USB HID void setupTinyUsbHid(){ if (!TinyUSBDevice.isInitialized()) { TinyUSBDevice.begin(0); } usb_hid.setBootProtocol(HID_ITF_PROTOCOL_KEYBOARD); usb_hid.setPollInterval(2); usb_hid.setReportDescriptor(desc_hid_report, sizeof(desc_hid_report)); usb_hid.setStringDescriptor("TinyUSB Keyboard & Media"); usb_hid.begin(); if (TinyUSBDevice.mounted()) { TinyUSBDevice.detach(); delay(10); TinyUSBDevice.attach(); } } #include "KeyCodes.h" #include "ChordMappings.h" /*=============================================== =========== APPLICATION SETTINGS VERBOSE_MODE If set to 'true' enables debug output, 'false' attempts to suppresses most serial output. -----------------------------------------------------------------------*/ #define VERBOSE_MODE true //============================================================= // a small helper used in setup() // ctb void error(const __FlashStringHelper*err) { if ( VERBOSE_MODE ) Serial.println(err); while (1); } //============================================================= // ctb class Button { byte _pin; // The button's I/O pin, as an Arduino pin number. public: Button(byte pin) : _pin(pin) { pinMode(pin, INPUT_PULLUP); // Make pin an input and activate pullup. } bool isDown() const { // TODO: this assumes we're using analog pins! //return analogRead(_pin) < 0x100; return (digitalRead(_pin) == LOW); } }; //============================================================= // board and connectivity specific // power regulator enable control pin const int EnPin = 5; // Pin numbers for the chording keyboard switches, using the Arduino numbering. static const Button switch_pins[7] = { Button(2), // Pinky Button(3), // Ring Button(4), // Middle Button(5), // Index Button(6), // Near Thumb Button(7), // Center Thumb Button(8), // Far Thumb }; //================================================== //ctb // A few timing constants const int HalfSec = 500; // for a half second delay // It seems it can happen that there are times when sending 3 raw // keys in a macro, the keys can arrive in the wrong order. So this was added // 2025-02-23 to see if it prevents that. Adjust as needed. // A long name for a short thing. The time between raw key sends in a macro . const int InterstitialDelay = 50; // for a 5/100 sec. delay (4/100 was not enough) // note - key debounce timing constants are not in this section. //====END=CONSTANTS=====================END=CONSTANTS============= //=====SETUP============================SETUP==================== // board specific messages void setup(void) { // Ensure software power reset pin in high pinMode(EnPin, OUTPUT); // Make pin an output, digitalWrite(EnPin, HIGH); // and activate pullup. delay(500); if ( VERBOSE_MODE ) Serial.begin(115200); if ( VERBOSE_MODE ) Serial.println(F("Adafruit TinyUSB HID Chorder")); if ( VERBOSE_MODE ) Serial.println(F("---------------------------------------")); /* Initialise the module */ if ( VERBOSE_MODE ) Serial.print(F("Initialising the TinyUSB HID device: ")); if ( VERBOSE_MODE ) Serial.println( F("OK!") ); setupTinyUsbHid(); } // used by processReading() // ctb enum State { PRESSING, RELEASING, }; State state = RELEASING; byte lastKeyState = 0; // used by sendKey() // ctb enum Mode { ALPHA, NUMSYM, FUNCTION }; bool isNumsymLocked = false; keymap_t latchMods = 0x00; // currently latched modKeys keymap_t modKeys = 0x00; // current modifyers ( L/Rshift,L/Ralt, L/Rctrl, L/Rgui ) Mode mode = ALPHA; // used by processREADING and loop byte previousStableReading = 0; byte currentStableReading = 0; long lastDebounceTime = 0; // the last time the output pin was toggled long debounceDelay = 10; // the debounce time; increase if the output flickers //=====RESET=====================RESET========================== // ctb void reset(){ mode = ALPHA; latchMods=0x00; modKeys = 0x00; isNumsymLocked = false; sendRawKeyUp(); } //=====SEND KEY====================SEND KEY======================== // mostly common but some differences in switch statement // used by processReading() void sendKey(byte keyState){ keymap_t theKey; // Determine the key based on the current mode's keymap if (mode == ALPHA) { theKey = keymap_default[keyState]; } else if (mode == NUMSYM) { theKey = keymap_numsym[keyState]; } else { theKey = keymap_function[keyState]; } switch (theKey) { // Handle mode switching - return immediately after the mode has changed // Handle basic mode switching case LATCH: if ( latchMods == 0x00 ) { // latch was not set, so set it current modKeys latchMods = modKeys; } else { latchMods = 0x00; // latch was set, so clear it. } break; case MODE_NUM: if (mode == NUMSYM) { mode = ALPHA; } else { mode = NUMSYM; } return; case MODE_FUNC: if (mode == FUNCTION) { mode = ALPHA; } else { mode = FUNCTION; } return; case MODE_RESET: reset(); return; case MODE_MRESET: reset(); digitalWrite(EnPin, LOW); // turn off 3.3v regulator enable. return; // something with a battery only case BAT_LVL: Serial.print("BLE only battery level chord"); return; case MODE_FRESET: Serial.print("BLE only factory reset chord"); return; // back to common code // Handle mode locks case MODE_NUMLCK: if (isNumsymLocked){ isNumsymLocked = false; mode = ALPHA; } else { isNumsymLocked = true; mode = NUMSYM; } return; // Handle modifier keys toggling case MOD_LCTRL: modKeys = modKeys ^ 0x01; return; case MOD_LSHIFT: modKeys = modKeys ^ 0x02; return; case MOD_LALT: modKeys = modKeys ^ 0x04; return; case MOD_LGUI: modKeys = modKeys ^ 0x08; return; case MOD_RCTRL: modKeys = modKeys ^ 0x10; return; case MOD_RSHIFT: modKeys = modKeys ^ 0x20; return; case MOD_RALT: modKeys = modKeys ^ 0x40; return; case MOD_RGUI: modKeys = modKeys ^ 0x80; return; // Handle special keys case MULTI_NumShift: if (mode == NUMSYM) { mode = ALPHA; } else { mode = NUMSYM; } modKeys = modKeys ^ 0x02; return; case MULTI_CtlAlt: modKeys = modKeys ^ 0x01; modKeys = modKeys ^ 0x04; return; /* Everything after this sends actual keys to the system; break rather than return since we want to reset the modifiers after these keys are sent. */ case MACRO_000: sendRawKey(0x00, ENUMKEY_0); sendRawKey(0x00, ENUMKEY_0); sendRawKey(0x00, ENUMKEY_0); break; case MACRO_00: sendRawKey(0x00, ENUMKEY_0); sendRawKey(0x00, ENUMKEY_0); break; case MACRO_quotes: sendRawKey(0x02, 0x34); delay(InterstitialDelay); sendRawKey(0x02, 0x34); delay(InterstitialDelay); sendRawKey(0x00, 0x50); break; case MACRO_parens: sendRawKey(0x02, ENUMKEY_9); delay(InterstitialDelay); sendRawKey(0x02, ENUMKEY_0); delay(InterstitialDelay); sendRawKey(0x00, 0x50); break; case MACRO_dollar: sendRawKey(0x02, 0x21); break; case MACRO_percent: sendRawKey(0x02, 0x22); break; case MACRO_ampersand: sendRawKey(0x02, 0x24); break; case MACRO_asterisk: sendRawKey(0x02, 0x25); break; case MACRO_question: sendRawKey(0x02, 0x38); break; case MACRO_plus: sendRawKey(0x02, 0x2E); break; case MACRO_openparen: sendRawKey(0x02, ENUMKEY_9); break; case MACRO_closeparen: sendRawKey(0x02, ENUMKEY_0); break; case MACRO_opencurly: sendRawKey(0x02, 0x2F); break; case MACRO_closecurly: sendRawKey(0x02, 0x30); break; case MACRO_1 : // er sendRawKey(modKeys, ENUMKEY_E); delay(InterstitialDelay); sendRawKey(latchMods, ENUMKEY_R); break; case MACRO_2: // th sendRawKey(modKeys, ENUMKEY_T); delay(InterstitialDelay); sendRawKey(latchMods, ENUMKEY_H); break; case MACRO_3: // an sendRawKey(modKeys, ENUMKEY_A); delay(InterstitialDelay); sendRawKey(latchMods, ENUMKEY_N); break; case MACRO_4: // in sendRawKey(modKeys, ENUMKEY_I); delay(InterstitialDelay); sendRawKey(latchMods, ENUMKEY_N); break; // macro test is a long string to confirm length of interstitial delay case MACRO_TEST: sendRawKey (modKeys, ENUMKEY_A); delay(InterstitialDelay); sendRawKey (modKeys, ENUMKEY_B); delay(InterstitialDelay); sendRawKey (modKeys, ENUMKEY_C); delay(InterstitialDelay); sendRawKey (modKeys, ENUMKEY_D); delay(InterstitialDelay); sendRawKey (modKeys, ENUMKEY_E); delay(InterstitialDelay); sendRawKey (modKeys, ENUMKEY_F); delay(InterstitialDelay); sendRawKey (modKeys, ENUMKEY_G); delay(InterstitialDelay); sendRawKey (modKeys, ENUMKEY_H); break; case MACRO_SHIFTDN: modKeys = MOD_LSHIFT; sendRawKeyDn(0x02, 0x00); break; // Handle Android specific keys case ANDROID_search: sendRawKey(0x04, 0x2C); break; case ANDROID_home: sendRawKey(0x04, 0x29); break; case ANDROID_menu: sendRawKey(0x10, 0x29); break; case ANDROID_back: sendRawKey(0x00, 0x29); break; case ANDROID_dpadcenter: sendRawKey(0x00, 0x5D); break; case MEDIA_playpause: sendControlKey("PLAYPAUSE"); break; case MEDIA_stop: sendControlKey("MEDIASTOP"); break; case MEDIA_next: sendControlKey("MEDIANEXT"); break; case MEDIA_previous: sendControlKey("MEDIAPREVIOUS"); break; case MEDIA_volup: sendControlKey("VOLUME+,500"); break; case MEDIA_voldn: sendControlKey("VOLUME-,500"); break; // Send the key default: sendRawKey(modKeys, theKey); break; } modKeys = latchMods; //set modKeys to any currently latched mods mode = ALPHA; // Reset the modKeys and mode based on locks if (isNumsymLocked){ mode = NUMSYM; } } //======SEND RAW KEY====================SEND RAW KEY================ // ctb // used in sendKey() // // new sendRawKey to make sure all is working as expected // before trying to impliment seporate watching for key down and key up // to allow host side key repeat // void sendRawKey(char modKey, char rawKey){ sendRawKeyDn(modKey, rawKey); sendRawKeyUp(); } //======SEND RAW KEY DOWN===============SEND RAW KEY DOWN============ // connectivity specific - This is for USB // used in sendRawKey() // void sendRawKeyDn(uint8_t modKey, uint8_t rawKey){ uint8_t keycode[6] = {0}; keycode[0] = rawKey; usb_hid.keyboardReport(1, modKey, keycode); delay(10); } //======SEND RAW KEY UP==============SEND RAW KEY UP================== // used in sendRawKey() // void sendRawKeyUp(){ usb_hid.keyboardRelease(1); delay(10); } //======SEND CONTROL KEY============SEND CONTROL KEY================== // connectivity specific - This is for USB // used in sendKey() // void sendControlKey(String cntrlName) { uint16_t mediaKey = 0; uint16_t holdTime = 10; // Default hold time in milliseconds // Extract the hold time if included (e.g., "VOLUME+,500") int commaIndex = cntrlName.indexOf(','); if (commaIndex > 0) { holdTime = cntrlName.substring(commaIndex + 1).toInt(); cntrlName = cntrlName.substring(0, commaIndex); } // Map the string to the appropriate HID Consumer Control keycode if (cntrlName == "PLAYPAUSE") mediaKey = HID_USAGE_CONSUMER_PLAY_PAUSE; else if (cntrlName == "MEDIASTOP") mediaKey = HID_USAGE_CONSUMER_STOP; else if (cntrlName == "MEDIANEXT") mediaKey = HID_USAGE_CONSUMER_SCAN_NEXT; else if (cntrlName == "MEDIAPREVIOUS") mediaKey = HID_USAGE_CONSUMER_SCAN_PREVIOUS; else if (cntrlName == "VOLUME+") mediaKey = HID_USAGE_CONSUMER_VOLUME_INCREMENT; else if (cntrlName == "VOLUME-") mediaKey = HID_USAGE_CONSUMER_VOLUME_DECREMENT; else return; // Invalid input, do nothing // Send media key usb_hid.sendReport(2, &mediaKey, sizeof(mediaKey)); delay(holdTime); // Release the media key uint16_t release = 0; usb_hid.sendReport(2, &release, sizeof(release)); } //=====PROCESS READING==================PROCESS READING=============== // ctb // used in loop() // // check if was pressing chord and now releasing, change to releasing, // then send the key // if chord was not pressing and now is, change to pressing // void processReading(){ switch (state) { case PRESSING: if (previousStableReading & ~currentStableReading) { state = RELEASING; sendKey(previousStableReading); } break; case RELEASING: if (currentStableReading & ~previousStableReading) { state = PRESSING; } break; } } //========LOOP=========================LOOP================== // ctb void loop() { // Build the current key state. byte keyState = 0, mask = 1; for (int i = 0; i < 7; i++) { if (switch_pins[i].isDown()) keyState |= mask; mask <<= 1; } if (lastKeyState != keyState) { lastDebounceTime = millis(); } if ((millis() - lastDebounceTime) > debounceDelay) { // whatever the reading is at, it's been there for longer // than the debounce delay, so take it as the actual current state: currentStableReading = keyState; } if (previousStableReading != currentStableReading) { processReading(); previousStableReading = currentStableReading; } lastKeyState = keyState; }