Last active
October 4, 2024 02:31
-
-
Save wez/b30683a4dfa329b86b9e0a2811a8c593 to your computer and use it in GitHub Desktop.
Revisions
-
wez revised this gist
Nov 25, 2017 . 2 changed files with 3 additions and 0 deletions.There are no files selected for viewing
Binary file not shown.This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -10,6 +10,9 @@ Parts: * 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 -
wez revised this gist
Nov 25, 2017 . 1 changed file with 14 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -17,12 +17,23 @@ This is a bluetooth peripheral that scans its matrix and publishes the data via ## 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. -
wez revised this gist
Nov 25, 2017 . 3 changed files with 0 additions and 0 deletions.There are no files selected for viewing
LoadingSorry, something went wrong. Reload?Sorry, we cannot display this file.Sorry, this file is invalid so it cannot be displayed.LoadingSorry, something went wrong. Reload?Sorry, we cannot display this file.Sorry, this file is invalid so it cannot be displayed.LoadingSorry, something went wrong. Reload?Sorry, we cannot display this file.Sorry, this file is invalid so it cannot be displayed. -
wez revised this gist
Nov 25, 2017 . 1 changed file with 28 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,28 @@ # 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. ## 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. -
wez revised this gist
Nov 25, 2017 . 3 changed files with 708 additions and 0 deletions.There are no files selected for viewing
LoadingSorry, something went wrong. Reload?Sorry, we cannot display this file.Sorry, this file is invalid so it cannot be displayed.LoadingSorry, something went wrong. Reload?Sorry, we cannot display this file.Sorry, this file is invalid so it cannot be displayed.LoadingSorry, something went wrong. Reload?Sorry, we cannot display this file.Sorry, this file is invalid so it cannot be displayed. -
wez created this gist
Nov 25, 2017 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,115 @@ #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) { } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,482 @@ #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) { }