Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Split Bluetooth Keyboard

Split bluetooth keyboard

Just some quick notes on something I built fairly quickly; they're published here in the hope that they might be slightly useful for others, or to help inspire others to build something better.

Parts:

  • 2x Adafruit nRF52 https://www.adafruit.com/product/3406 (I got the Pro version, but you'll need a JLink to use the arduino code in this gist if you get the pro, so the link here is the non-pro version)
  • 72x 1N4148 signal diodes
  • 72x Cherry MX compatible keyswitches
  • 6x 1.25u keycaps for modifiers
  • 66x 1u keycaps
  • 2x lipoly battery with JST connector.
  • hot glue to secure the switches in the top plate
  • 20x M3 spacers and standoffs for the case supports
  • 8x M2.5 (M2 will also work) nuts and bolts to mount the controllers

LHS - Left Hand Side

This is a bluetooth peripheral that scans its matrix and publishes the data via bleuart.

RHS - Right Hand Side

This runs as a dual role device; it runs as a Central device that connects to the LHS to read its matrix via bleuart, and runs a bluetooth HID peripheral to publish the combined matrices of the two halves.

You'll want to pair your device with the RHS only!

Wiring

This uses standard matrix wiring (which I've unhelpfully not shown here); each switch has a diode to prevent ghosting. The diodes are used to form the rows. The columns are shown in matrix.svg.

The rows are connected to the A0-A5 pins on the LHS of the controller. The columns are connected to the pins on the other side. Take care to avoid the special pin 31 which is used to sample the battery voltage.

Case

The included case.svg file is ready for use with ponoko.com. I used a blue matte acrylic in my build.

Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
#include <bluefruit.h>
BLEDis bledis;
BLEUart bleuart;
static const int colPins[] = {16,15,7,11,30,27};
static const int rowPins[] = {2,3,4,5,28,29};
void setup()
{
for (auto &pin: rowPins) {
pinMode(pin, OUTPUT);
digitalWrite(pin, HIGH);
}
for (auto &pin: colPins) {
pinMode(pin, INPUT_PULLUP);
}
Serial.begin(115200);
Bluefruit.begin();
Bluefruit.autoConnLed(false);
Bluefruit.setTxPower(0);
Bluefruit.setName("Scission LHS");
bledis.setManufacturer("Wez Furlong");
bledis.setModel("Handwire1");
bledis.begin();
bleuart.begin();
startAdv();
}
void startAdv(void)
{
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
Bluefruit.Advertising.addAppearance(BLE_APPEARANCE_HID_KEYBOARD);
Bluefruit.Advertising.addService(bleuart);
Bluefruit.ScanResponse.addName();
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
}
struct matrix_t {
uint8_t rows[6];
};
struct matrix_t lastRead = {0,0,0,0,0,0};
struct matrix_t read_matrix() {
matrix_t matrix = {0,0,0,0,0,0};
for (int rowNum = 0; rowNum < 6; ++rowNum) {
digitalWrite(rowPins[rowNum], LOW);
for (int colNum = 0; colNum < 6; ++colNum) {
if (!digitalRead(colPins[colNum])) {
matrix.rows[rowNum] |= 1 << colNum;
}
}
digitalWrite(rowPins[rowNum], HIGH);
}
return matrix;
}
void loop()
{
auto down = read_matrix();
uint8_t report[16];
uint8_t repsize = 0;
for (int rowNum = 0; rowNum < 6; ++rowNum) {
for (int colNum = 0; colNum < 6; ++colNum) {
auto mask = 1 << colNum;
auto current = lastRead.rows[rowNum] & mask;
auto thisScan = down.rows[rowNum] & mask;
if (current != thisScan) {
auto scanCode = (rowNum * 6) + colNum;
if (thisScan) {
scanCode |= 0b10000000;
}
report[repsize++] = scanCode;
}
}
}
if (repsize) {
lastRead = down;
#if 0
Serial.print("repsize=");
Serial.print(repsize);
Serial.print(" ");
for (int i = 0; i < repsize; i++) {
Serial.print(report[i], HEX);
Serial.print(" ");
}
Serial.print("\r\n");
#endif
bleuart.write(report, repsize);
}
// Request CPU to enter low-power mode until an event/interrupt occurs
waitForEvent();
}
void rtos_idle_callback(void)
{
}
<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="full" height="169.27734671666838mm" version="1.1" viewBox="0,0,153.4429738830441,169.27734671666838" width="153.4429738830441mm" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs /><g><polygon fill="red" fill-opacity="0.4" points="38.749370911788134,34.998807100447685 39.82936851487047,54.01816842456518 20.810007190752977,55.09816602764752 19.730009587670633,36.078804703530025 38.749370911788134,34.998807100447685" stroke="red" stroke-width="0.2" /><polygon fill="red" fill-opacity="0.4" points="39.82936851487047,54.01816842456518 40.909366117952814,73.03752974868269 21.890004793835317,74.11752735176502 20.810007190752977,55.09816602764752 39.82936851487047,54.01816842456518" stroke="red" stroke-width="0.2" /><polygon fill="red" fill-opacity="0.4" points="40.909366117952814,73.03752974868269 41.98936372103515,92.05689107280017 22.97000239691766,93.1368886758825 21.890004793835317,74.11752735176502 40.909366117952814,73.03752974868269" stroke="red" stroke-width="0.2" /><polygon fill="red" fill-opacity="0.4" points="41.98936372103515,92.05689107280017 43.069361324117494,111.07625239691767 24.05,112.15625 22.97000239691766,93.1368886758825 41.98936372103515,92.05689107280017" stroke="red" stroke-width="0.2" /><polygon fill="red" fill-opacity="0.4" points="43.1,112.15625 43.1,131.20625 24.05,131.20625 24.05,112.15625 43.1,112.15625" stroke="red" stroke-width="0.2" /><polygon fill="red" fill-opacity="0.4" points="43.1,131.20625 43.1,150.25625000000002 24.05,150.25625000000002 24.05,131.20625 43.1,131.20625" stroke="red" stroke-width="0.2" /><line stroke="red" stroke-width="0.2" x1="29.77968905127055" x2="30.859686654352892" y1="45.0484865640476" y2="64.0678478881651" /><line stroke="red" stroke-width="0.2" x1="30.859686654352892" x2="31.93968425743524" y1="64.0678478881651" y2="83.08720921228262" /><line stroke="red" stroke-width="0.2" x1="31.93968425743524" x2="33.01968186051758" y1="83.08720921228262" y2="102.10657053640008" /><line stroke="red" stroke-width="0.2" x1="33.01968186051758" x2="33.575" y1="102.10657053640008" y2="121.68125000000002" /><line stroke="red" stroke-width="0.2" x1="33.575" x2="33.574999999999996" y1="121.68125000000002" y2="140.73125000000002" /><polygon fill="blue" fill-opacity="0.4" points="62.150000000000006,28.8125 62.150000000000006,47.8625 43.1,47.8625 43.1,28.8125 62.150000000000006,28.8125" stroke="blue" stroke-width="0.2" /><polygon fill="blue" fill-opacity="0.4" points="62.150000000000006,47.862500000000004 62.150000000000006,66.91250000000001 43.1,66.91250000000001 43.1,47.862500000000004 62.150000000000006,47.862500000000004" stroke="blue" stroke-width="0.2" /><polygon fill="blue" fill-opacity="0.4" points="62.150000000000006,66.9125 62.150000000000006,85.9625 43.1,85.9625 43.1,66.9125 62.150000000000006,66.9125" stroke="blue" stroke-width="0.2" /><polygon fill="blue" fill-opacity="0.4" points="62.150000000000006,85.9625 62.150000000000006,105.0125 43.1,105.0125 43.1,85.9625 62.150000000000006,85.9625" stroke="blue" stroke-width="0.2" /><polygon fill="blue" fill-opacity="0.4" points="62.150000000000006,112.15625 62.150000000000006,131.20625 43.1,131.20625 43.1,112.15625 62.150000000000006,112.15625" stroke="blue" stroke-width="0.2" /><polygon fill="blue" fill-opacity="0.4" points="62.150000000000006,131.20625 62.150000000000006,150.25625000000002 43.1,150.25625000000002 43.1,131.20625 62.150000000000006,131.20625" stroke="blue" stroke-width="0.2" /><line stroke="blue" stroke-width="0.2" x1="52.625" x2="52.62500000000001" y1="38.3375" y2="57.3875" /><line stroke="blue" stroke-width="0.2" x1="52.62500000000001" x2="52.62500000000001" y1="57.3875" y2="76.4375" /><line stroke="blue" stroke-width="0.2" x1="52.62500000000001" x2="52.625" y1="76.4375" y2="95.4875" /><line stroke="blue" stroke-width="0.2" x1="52.625" x2="52.625" y1="95.4875" y2="121.68125" /><line stroke="blue" stroke-width="0.2" x1="52.625" x2="52.625" y1="121.68125" y2="140.73125000000002" /><polygon fill="green" fill-opacity="0.4" points="81.2,24.05 81.2,43.1 62.150000000000006,43.1 62.150000000000006,24.05 81.2,24.05" stroke="green" stroke-width="0.2" /><polygon fill="green" fill-opacity="0.4" points="81.2,43.1 81.2,62.150000000000006 62.150000000000006,62.150000000000006 62.150000000000006,43.1 81.2,43.1" stroke="green" stroke-width="0.2" /><polygon fill="green" fill-opacity="0.4" points="81.2,62.150000000000006 81.2,81.2 62.150000000000006,81.2 62.150000000000006,62.150000000000006 81.2,62.150000000000006" stroke="green" stroke-width="0.2" /><polygon fill="green" fill-opacity="0.4" points="81.2,81.2 81.2,100.25 62.150000000000006,100.25 62.150000000000006,81.2 81.2,81.2" stroke="green" stroke-width="0.2" /><polygon fill="green" fill-opacity="0.4" points="81.2,112.15625 81.2,131.20625 62.150000000000006,131.20625 62.150000000000006,112.15625 81.2,112.15625" stroke="green" stroke-width="0.2" /><polygon fill="green" fill-opacity="0.4" points="81.2,131.20625 81.2,150.25625000000002 62.150000000000006,150.25625000000002 62.150000000000006,131.20625 81.2,131.20625" stroke="green" stroke-width="0.2" /><line stroke="green" stroke-width="0.2" x1="71.67500000000001" x2="71.67500000000001" y1="33.575" y2="52.625" /><line stroke="green" stroke-width="0.2" x1="71.67500000000001" x2="71.675" y1="52.625" y2="71.675" /><line stroke="green" stroke-width="0.2" x1="71.675" x2="71.675" y1="71.675" y2="90.725" /><line stroke="green" stroke-width="0.2" x1="71.675" x2="71.675" y1="90.725" y2="121.68124999999999" /><line stroke="green" stroke-width="0.2" x1="71.675" x2="71.675" y1="121.68124999999999" y2="140.73125" /><polygon fill="gray" fill-opacity="0.4" points="105.79743271265227,16.624593460712518 104.71743510956993,35.643954784830015 85.69807378545244,34.56395718174767 86.77807138853477,15.544595857630174 105.79743271265227,16.624593460712518" stroke="gray" stroke-width="0.2" /><polygon fill="gray" fill-opacity="0.4" points="104.71743510956993,35.643954784830015 103.63743750648759,54.66331610894751 84.61807618237009,53.58331850586517 85.69807378545244,34.56395718174767 104.71743510956993,35.643954784830015" stroke="gray" stroke-width="0.2" /><polygon fill="gray" fill-opacity="0.4" points="103.63743750648759,54.663316108947505 102.55743990340524,73.682677433065 83.53807857928774,72.60267982998265 84.61807618237009,53.583318505865165 103.63743750648759,54.663316108947505" stroke="gray" stroke-width="0.2" /><polygon fill="gray" fill-opacity="0.4" points="102.55743990340524,73.682677433065 101.4774423003229,92.70203875718249 82.45808097620541,91.62204115410015 83.53807857928774,72.60267982998265 102.55743990340524,73.682677433065" stroke="gray" stroke-width="0.2" /><polygon fill="gray" fill-opacity="0.4" points="101.4774423003229,92.70203875718249 100.39744469724056,111.72140008129999 81.37808337312306,110.64140247821764 82.45808097620541,91.62204115410015 101.4774423003229,92.70203875718249" stroke="gray" stroke-width="0.2" /><polygon fill="gray" fill-opacity="0.4" points="111.31091883992924,117.61456711664881 105.14779032842547,140.61567585515724 86.7469033376187,135.68517304595423 92.91003184912248,112.68406430744581 111.31091883992924,117.61456711664881" stroke="gray" stroke-width="0.2" /><line stroke="gray" stroke-width="0.2" x1="95.74775324905237" x2="94.66775564597002" y1="25.594275321230093" y2="44.6136366453476" /><line stroke="gray" stroke-width="0.2" x1="94.66775564597002" x2="93.58775804288767" y1="44.6136366453476" y2="63.63299796946507" /><line stroke="gray" stroke-width="0.2" x1="93.58775804288767" x2="92.50776043980534" y1="63.63299796946507" y2="82.65235929358258" /><line stroke="gray" stroke-width="0.2" x1="92.50776043980534" x2="91.42776283672298" y1="82.65235929358258" y2="101.67172061770006" /><line stroke="gray" stroke-width="0.2" x1="91.42776283672298" x2="99.02891108877398" y1="101.67172061770006" y2="126.64987008130151" /><polygon fill="gold" fill-opacity="0.4" points="124.81679403676976,17.704591063794858 123.73679643368743,36.723952387912355 104.71743510956993,35.643954784830015 105.79743271265227,16.624593460712518 124.81679403676976,17.704591063794858" stroke="gold" stroke-width="0.2" /><polygon fill="gold" fill-opacity="0.4" points="123.73679643368743,36.723952387912355 122.65679883060508,55.74331371202985 103.63743750648759,54.66331610894751 104.71743510956993,35.643954784830015 123.73679643368743,36.723952387912355" stroke="gold" stroke-width="0.2" /><polygon fill="gold" fill-opacity="0.4" points="122.65679883060508,55.743313712029845 121.57680122752274,74.76267503614734 102.55743990340524,73.682677433065 103.63743750648759,54.663316108947505 122.65679883060508,55.743313712029845" stroke="gold" stroke-width="0.2" /><polygon fill="gold" fill-opacity="0.4" points="121.57680122752274,74.76267503614734 120.4968036244404,93.78203636026484 101.4774423003229,92.70203875718249 102.55743990340524,73.682677433065 121.57680122752274,74.76267503614734" stroke="gold" stroke-width="0.2" /><polygon fill="gold" fill-opacity="0.4" points="120.4968036244404,93.78203636026484 119.41680602135806,112.80139768438234 100.39744469724056,111.72140008129999 101.4774423003229,92.70203875718249 120.4968036244404,93.78203636026484" stroke="gold" stroke-width="0.2" /><polygon fill="gold" fill-opacity="0.4" points="129.711805830736,122.54506992585185 124.78130302153299,140.9459569166586 106.38041603072624,136.01545410745558 111.31091883992924,117.61456711664881 129.711805830736,122.54506992585185" stroke="gold" stroke-width="0.2" /><line stroke="gold" stroke-width="0.2" x1="114.76711457316986" x2="113.6871169700875" y1="26.674272924312437" y2="45.69363424842993" /><line stroke="gold" stroke-width="0.2" x1="113.6871169700875" x2="112.60711936700517" y1="45.69363424842993" y2="64.71299557254741" /><line stroke="gold" stroke-width="0.2" x1="112.60711936700517" x2="111.52712176392284" y1="64.71299557254741" y2="83.73235689666492" /><line stroke="gold" stroke-width="0.2" x1="111.52712176392284" x2="110.44712416084046" y1="83.73235689666492" y2="102.75171822078242" /><line stroke="gold" stroke-width="0.2" x1="110.44712416084046" x2="118.04611093073112" y1="102.75171822078242" y2="129.2802620166537" /><polygon fill="purple" fill-opacity="0.4" points="140.59616255164028,75.84267263922968 139.24616554778734,99.61687429437654 120.22680422366983,98.53687669129421 121.57680122752275,74.76267503614734 140.59616255164028,75.84267263922968" stroke="purple" stroke-width="0.2" /><polygon fill="purple" fill-opacity="0.4" points="148.4429738830441,107.84206004194733 143.51247107384108,126.24294703275409 120.51136233533262,120.07981852125032 125.44186514453564,101.67893153044356 148.4429738830441,107.84206004194733" stroke="purple" stroke-width="0.2" /><polygon fill="purple" fill-opacity="0.4" points="148.1126928215428,127.47557273505488 143.18219001233973,145.8764597258616 124.78130302153299,140.9459569166586 129.711805830736,122.54506992585185 148.1126928215428,127.47557273505488" stroke="purple" stroke-width="0.2" /><polygon fill="purple" fill-opacity="0.4" points="143.18219001233976,145.8764597258616 138.25168720313673,164.27734671666838 119.85080021232996,159.34684390746537 124.78130302153299,140.9459569166586 143.18219001233976,145.8764597258616" stroke="purple" stroke-width="0.2" /><polygon fill="purple" fill-opacity="0.4" points="124.78130302153299,140.9459569166586 119.85080021232996,159.34684390746537 101.44991322152322,154.4163410982623 106.38041603072624,136.01545410745553 124.78130302153299,140.9459569166586" stroke="purple" stroke-width="0.2" /><polygon fill="purple" fill-opacity="0.4" points="105.14779032842547,140.61567585515724 100.21728751922244,159.01656284596402 81.81640052841568,154.086060036761 86.7469033376187,135.68517304595423 105.14779032842547,140.61567585515724" stroke="purple" stroke-width="0.2" /><line stroke="purple" stroke-width="0.2" x1="130.41148338765504" x2="134.47716810918837" y1="87.18977466526195" y2="113.96093928159883" /><line stroke="purple" stroke-width="0.2" x1="134.47716810918837" x2="136.44699792153787" y1="113.96093928159883" y2="134.21076482585673" /><line stroke="purple" stroke-width="0.2" x1="136.44699792153787" x2="131.5164951123349" y1="134.21076482585673" y2="152.61165181666345" /><line stroke="purple" stroke-width="0.2" x1="131.5164951123349" x2="113.1156081215281" y1="152.61165181666345" y2="147.68114900746048" /><line stroke="purple" stroke-width="0.2" x1="113.1156081215281" x2="93.48209542842058" y1="147.68114900746048" y2="147.35086794595912" /></g></svg>
#include <bluefruit.h>
BLEDis dis;
BLEHidAdafruit hid;
BLEClientUart clientUart;
BLEBas battery;
static uint8_t battery_level = 0;
static uint32_t last_bat_time = 0;
static const int colPins[] = {16,15,7,11,30,27};
static const int rowPins[] = {2,3,4,5,28,29};
struct matrix_t {
uint16_t rows[6];
};
struct matrix_t remoteMatrix = {0,0,0,0,0,0};
struct matrix_t lastRead = {0,0,0,0,0,0};
void resetKeyMatrix();
void setup()
{
for (auto &pin: rowPins) {
pinMode(pin, OUTPUT);
digitalWrite(pin, HIGH);
}
for (auto &pin: colPins) {
pinMode(pin, INPUT_PULLUP);
}
Serial.begin(115200);
resetKeyMatrix();
// Central and peripheral
Bluefruit.begin(true, true);
//Bluefruit.clearBonds();
Bluefruit.autoConnLed(false);
battery.begin();
Bluefruit.setTxPower(0);
Bluefruit.setName("Scission RHS");
Bluefruit.Central.setConnectCallback(cent_connect_callback);
Bluefruit.Central.setDisconnectCallback(cent_disconnect_callback);
dis.setManufacturer("Wez Furlong");
dis.setModel("Handwire1");
dis.setHardwareRev("0001");
dis.setSoftwareRev(__DATE__);
dis.begin();
clientUart.begin();
// clientUart.setRxCallback(cent_bleuart_rx_callback);
/* Start Central Scanning
* - Enable auto scan if disconnected
* - Interval = 100 ms, window = 80 ms
* - Filter only accept bleuart service
* - Don't use active scan
* - Start(timeout) with timeout = 0 will scan forever (until connected)
*/
Bluefruit.Scanner.setRxCallback(scan_callback);
Bluefruit.Scanner.restartOnDisconnect(true);
Bluefruit.Scanner.setInterval(160, 80); // in unit of 0.625 ms
Bluefruit.Scanner.filterUuid(BLEUART_UUID_SERVICE);
Bluefruit.Scanner.useActiveScan(false);
Bluefruit.Scanner.start(0); // 0 = Don't stop scanning after n seconds
hid.begin();
// delay(5000);
// Bluefruit.printInfo();
startAdv();
}
void cent_connect_callback(uint16_t conn_handle)
{
char peer_name[32] = { 0 };
Bluefruit.Gap.getPeerName(conn_handle, peer_name, sizeof(peer_name));
Serial.print("[Cent] Connected to ");
Serial.println(peer_name);
if (clientUart.discover(conn_handle)) {
// Enable TXD's notify
clientUart.enableTXD();
} else {
Bluefruit.Central.disconnect(conn_handle);
}
resetKeyMatrix();
}
void cent_disconnect_callback(uint16_t conn_handle, uint8_t reason)
{
(void) conn_handle;
(void) reason;
Serial.println("[Cent] Disconnected");
resetKeyMatrix();
}
void scan_callback(ble_gap_evt_adv_report_t* report)
{
// Check if advertising contain BleUart service
if (Bluefruit.Scanner.checkReportForService(report, clientUart)) {
// Connect to device with bleuart service in advertising
Bluefruit.Central.connect(report);
}
}
void startAdv(void)
{
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
Bluefruit.Advertising.addAppearance(BLE_APPEARANCE_HID_KEYBOARD);
Bluefruit.Advertising.addService(hid);
Bluefruit.ScanResponse.addService(battery);
Bluefruit.Advertising.addName();
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
}
static constexpr uint32_t kMask = 0xf00;
static constexpr uint32_t kKeyPress = 0x100;
static constexpr uint32_t kModifier = 0x200;
static constexpr uint32_t kLayer = 0x300;
static constexpr uint32_t kTapHold = 0x400;
static constexpr uint32_t kToggleMod = 0x500;
static constexpr uint32_t kKeyAndMod = 0x600;
typedef uint32_t action_t;
#define PASTE(a, b) a ## b
#define ___ 0
#define KEY(a) kKeyPress | PASTE(HID_KEY_, a)
#define MOD(a) kModifier | PASTE(KEYBOARD_MODIFIER_, a)
#define TMOD(a) kToggleMod | PASTE(KEYBOARD_MODIFIER_, a)
#define TAPH(a, b) kTapHold | PASTE(HID_KEY_, a) | (PASTE(KEYBOARD_MODIFIER_, b) << 16)
#define KANDMOD(a, b) kKeyAndMod | PASTE(HID_KEY_, a) | (PASTE(KEYBOARD_MODIFIER_, b) << 16)
#define LAYER(n) kLayer | n
#define KEYMAP( \
l00, l01, l02, l03, l04, l05, \
l10, l11, l12, l13, l14, l15, \
l20, l21, l22, l23, l24, l25, \
l30, l31, l32, l33, l34, l35, \
l40, l41, l42, l43, l44, l45, \
l50, l51, l52, l53, l54, l55, \
r00, r01, r02, r03, r04, r05, \
r10, r11, r12, r13, r14, r15, \
r20, r21, r22, r23, r24, r25, \
r30, r31, r32, r33, r34, r35, \
r40, r41, r42, r43, r44, r45, \
r50, r51, r52, r53, r54, r55) \
{l00, l01, l02, l03, l04, l05, r00, r01, r02, r03, r04, r05, \
l10, l11, l12, l13, l14, l15, r10, r11, r12, r13, r14, r15, \
l20, l21, l22, l23, l24, l25, r20, r21, r22, r23, r24, r25, \
l30, l31, l32, l33, l34, l35, r30, r31, r32, r33, r34, r35, \
l40, l41, l42, l43, l44, l45, r40, r41, r42, r43, r44, r45, \
l50, l51, l52, l53, l54, l55, r50, r51, r52, r53, r54, r55}
struct keystate {
uint8_t scanCode;
bool down;
uint32_t lastChange;
action_t action;
};
struct keystate keyStates[16];
uint8_t layer_stack[8];
static uint8_t layer_pos = 0;
void resetKeyMatrix() {
layer_pos = 0;
layer_stack[0] = 0;
memset(&remoteMatrix, 0, sizeof(remoteMatrix));
memset(&lastRead, 0, sizeof(lastRead));
memset(keyStates, 0xff, sizeof(keyStates));
hid.keyRelease();
}
void printState(struct keystate *state) {
Serial.print("scanCode=");
Serial.print(state->scanCode, HEX);
Serial.print(" down=");
Serial.print(state->down);
Serial.print(" lastChange=");
Serial.print(state->lastChange);
Serial.print(" action=");
Serial.print(state->action, HEX);
Serial.println("");
}
struct keystate* stateSlot(uint8_t scanCode, uint32_t now) {
struct keystate *vacant = nullptr;
struct keystate *reap = nullptr;
for (auto &s : keyStates) {
if (s.scanCode == scanCode) {
return &s;
}
if (!vacant && s.scanCode == 0xff) {
vacant = &s;
continue;
}
if (!s.down) {
if (!reap) {
reap = &s;
} else if (now - s.lastChange > now - reap->lastChange) {
// Idle longer than the other reapable candidate; choose
// the eldest of them
reap = &s;
}
}
}
if (vacant) {
return vacant;
}
return reap;
}
const action_t keymap[2][72] = {
// Layer 0
KEYMAP(
// LEFT
KEY(1), KEY(2), KEY(3), ___ /* BLUE */, KANDMOD(C,LEFTGUI),MOD(LEFTALT),
KEY(Q), KEY(W), KEY(E), KEY(4), KEY(5), MOD(LEFTGUI),
KEY(A), KEY(S), KEY(D), KEY(R), KEY(T), KEY(TAB),
KEY(Z), KEY(X), KEY(C), KEY(F), KEY(G), KEY(DELETE),
KEY(BACKSLASH), KEY(MINUS), KEY(EQUAL), KEY(V), KEY(B), KEY(BACKSPACE),
LAYER(1), KEY(BRACKET_LEFT), KEY(BRACKET_RIGHT), MOD(LEFTSHIFT), ___ /* REKT */, TAPH(ESCAPE, LEFTCTRL),
// RIGHT
MOD(RIGHTALT), KANDMOD(V,LEFTGUI), ___ /* RED */, KEY(8), KEY(9), KEY(0),
MOD(RIGHTGUI), KEY(6), KEY(7), KEY(I), KEY(O), KEY(P),
KEY(PAGE_UP), KEY(Y), KEY(U), KEY(K), KEY(L), KEY(SEMICOLON),
KEY(PAGE_DOWN), KEY(H), KEY(J), KEY(COMMA), KEY(PERIOD), KEY(SLASH),
KEY(SPACE), KEY(N), KEY(M), KEY(GRAVE), KEY(ARROW_UP), KEY(APOSTROPHE),
MOD(RIGHTCTRL), KEY(RETURN), MOD(RIGHTSHIFT), KEY(ARROW_LEFT), KEY(ARROW_DOWN), KEY(ARROW_RIGHT)
),
// Layer 1
KEYMAP(
// LEFT
KEY(F1), KEY(F2), KEY(F3), ___, ___, ___,
___, ___, ___, KEY(F4), KEY(F5), ___,
___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___,
// RIGHT
___, ___, ___, KEY(F8), KEY(F9), KEY(F10),
___, KEY(F6), KEY(F7), ___, ___, ___,
___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___
)
};
// Remote matrix is the LHS
void updateRemoteMatrix() {
while (clientUart.available() )
{
auto ch = (uint8_t) clientUart.read();
auto down = ch & 0x80;
ch &= ~0x80;
auto rowNum = ch / 6;
auto colNum = ch - (rowNum * 6);
if (down) {
remoteMatrix.rows[rowNum] |= 1 << colNum;
} else {
remoteMatrix.rows[rowNum] &= ~(1 << colNum);
}
#if 0
Serial.print("remote=");
Serial.print(ch, HEX);
Serial.print("\r\n");
#endif
}
}
struct matrix_t readMatrix() {
matrix_t matrix = remoteMatrix;
for (int rowNum = 0; rowNum < 6; ++rowNum) {
digitalWrite(rowPins[rowNum], LOW);
for (int colNum = 0; colNum < 6; ++colNum) {
if (!digitalRead(colPins[colNum])) {
matrix.rows[rowNum] |= 1 << (colNum + 6);
}
}
digitalWrite(rowPins[rowNum], HIGH);
}
return matrix;
}
void readBattery() {
auto now = millis();
if (now - last_bat_time <= 10000) {
// There's a lot of variance in the reading, so no need
// to over-report it.
return;
}
last_bat_time = now;
constexpr int VBAT = 31; // pin 31 is available for sampling the battery
float measuredvbat = analogRead(VBAT) * 6.6 / 1024;
uint8_t bat_percentage = (uint8_t)round((measuredvbat - 3.7) * 200);
bat_percentage = min(bat_percentage, 100);
if (battery_level != bat_percentage) {
battery_level = bat_percentage;
battery.notify(battery_level);
}
}
static uint32_t resolveActionForScanCodeOnActiveLayer(uint8_t scanCode) {
int s = layer_pos;
while (s >= 0 && keymap[layer_stack[s]][scanCode] == ___) {
--s;
}
return keymap[layer_stack[s]][scanCode];
}
void loop()
{
auto down = readMatrix();
bool keysChanged = false;
updateRemoteMatrix();
readBattery();
auto now = millis();
for (int rowNum = 0; rowNum < 6; ++rowNum) {
for (int colNum = 0; colNum < 12; ++colNum) {
auto scanCode = (rowNum * 12) + colNum;
auto isDown = down.rows[rowNum] & (1 << colNum);
auto wasDown = lastRead.rows[rowNum] & (1 << colNum);
if (isDown == wasDown) {
continue;
}
keysChanged = true;
auto state = stateSlot(scanCode, now);
if (isDown && !state) {
// Silently drop this key; we're tracking too many
// other keys right now
continue;
}
printState(state);
bool isTransition = false;
if (state) {
if (state->scanCode == scanCode) {
// Update the transition time, if any
if (state->down != isDown) {
state->lastChange = now;
state->down = isDown;
if (isDown) {
state->action = resolveActionForScanCodeOnActiveLayer(scanCode);
}
isTransition = true;
}
} else {
// We claimed a new slot, so set the transition
// time to the current time.
state->down = isDown;
state->scanCode = scanCode;
state->lastChange = now;
if (isDown) {
state->action = resolveActionForScanCodeOnActiveLayer(scanCode);
}
isTransition = true;
}
if (isTransition) {
switch (state->action & kMask) {
case kLayer:
if (state->down) {
// Push the new layer stack position
layer_stack[++layer_pos] = state->action & 0xff;
} else {
// Pop off the layer stack
--layer_pos;
}
break;
}
}
}
}
}
if (keysChanged) {
uint8_t report[6] = {0,0,0,0,0,0};
uint8_t repsize = 0;
uint8_t mods = 0;
for (auto &state: keyStates) {
if (state.scanCode != 0xff && state.down) {
switch (state.action & kMask) {
case kTapHold:
if (now - state.lastChange > 200) {
// Holding
mods |= (state.action >> 16) & 0xff;
} else {
// Tapping
auto key = state.action & 0xff;
if (key != 0 && repsize < 6) {
report[repsize++] = key;
}
}
break;
case kKeyAndMod:
{
mods |= (state.action >> 16) & 0xff;
auto key = state.action & 0xff;
if (key != 0 && repsize < 6) {
report[repsize++] = key;
}
}
break;
case kKeyPress:
{
auto key = state.action & 0xff;
if (key != 0 && repsize < 6) {
report[repsize++] = key;
}
}
break;
case kModifier:
mods |= state.action & 0xff;
break;
case kToggleMod:
mods ^= state.action & 0xff;
break;
}
}
}
#if 1
Serial.print("mods=");
Serial.print(mods, HEX);
Serial.print(" repsize=");
Serial.print(repsize);
for (int i = 0; i < repsize; i++) {
Serial.print(" ");
Serial.print(report[i], HEX);
}
Serial.print("\r\n");
#endif
hid.keyboardReport(mods, report);
lastRead = down;
}
// Request CPU to enter low-power mode until an event/interrupt occurs
waitForEvent();
}
void rtos_idle_callback(void)
{
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.