|
#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) |
|
{ |
|
} |
|
|
Thanks for the built log, I want to build my first split ergo now 👍