From 469cf7ada10ec6a98a61094d0334f242f1003a0a Mon Sep 17 00:00:00 2001 From: Seth Tunstall Date: Sun, 23 Oct 2022 16:35:57 +0100 Subject: [PATCH] initial commit --- ppk_ble.ino | 535 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 535 insertions(+) create mode 100644 ppk_ble.ino diff --git a/ppk_ble.ino b/ppk_ble.ino new file mode 100644 index 0000000..fa24c76 --- /dev/null +++ b/ppk_ble.ino @@ -0,0 +1,535 @@ +/* + * ppk_usb + * + * Copyright (C) 2014 cy384 + * All rights reserved. + * + * This software may be modified and distributed under the terms + * of the BSD license. See the LICENSE file for details. + * + * MODIFIED by Christian Lysholm 2021 + * Modified for BLE support using a tinyPICO ESP32 module. + * Added battery monitoring and reporting via RGB LED + * Moved keyboard booting until peripheral is detected + * + */ + +// Arduino BLE HID adapter for the Palm Portable Keyboard + +// If this include causes an error Arduino, just comment it out +//#include +#include +#include +//#include +#include +//#include + +// set to 3 for III hardware, or 5 for V hardware +#define PPK_VERSION 500 + +// set to 1 to enable debug mode, which notes to the arduino console at 9600 +#define PPK_DEBUG 0 + +#if PPK_VERSION == 3 +#define VCC_PIN 2 +#define RX_PIN 8 +#define RTS_PIN 4 +#define DCD_PIN 5 +#define GND_PIN 6 +#endif + +#if PPK_VERSION == 5 +#define VCC_PIN 7 +#define RX_PIN 8 +#define RTS_PIN 5 +#define DCD_PIN 4 +#define GND_PIN 2 +#endif + +#if PPK_VERSION == 500 +#define VCC_PIN 4 +#define RX_PIN 27 //External 10k pulldown resistor added +#define RTS_PIN 26 +#define DCD_PIN 25 +#define GND_PIN 18 //UNUSED +#define DET_PIN 22 //Peripheral detect, held low by KB +#endif + +#define PULLDOWN_PIN 23 +// set this to any unused pin +#define TX_PIN 19 + +#if (PPK_VERSION != 3) && (PPK_VERSION != 5) && (PPK_VERSION != 500) +#error +#error +#error you did not set your ppk version! +#error read the instructions or read the code! +#error +#error +#endif + +// convenience masks +#define UPDOWN_MASK 0b10000000 +#define X_MASK 0b00000111 +#define Y_MASK 0b01111000 +#define MAP_MASK 0b01111111 + +// wait this many milliseconds before making sure keyboard is still awake +#define TIMEOUT 500000 +#define BAT_CHECK_TIME 300000 //5 minutes +#define HEARTBEAT_TIME 5000 //5 seconds + +// macro for testing if a char is printable ASCII +#define PRINTABLE_CHAR(x) ((x >= 32) && (x <= 126)) + +uint32_t heartbeat_color; +SoftwareSerial keyboard_serial(RX_PIN, TX_PIN, true); // RX, TX, inverted +TinyPICO tp = TinyPICO(); +int check_battery(void); +int batLevel = check_battery(); +BleKeyboard ble_kb("PICO_PPK", "LysTech", batLevel); + +char key_map[128] = { 0 }; +char fn_key_map[128] = { 0 }; + +char last_byte = 0; +char key_byte = 0; + +int fn_key_down = 0; +int boot_state = 0; //keyboard boot state (0/1) + +unsigned long last_comm = 0; +unsigned long last_bat = 0; +unsigned long last_heartbeat = 0; +unsigned long heartbeat_length = 750; //milliseconds + +void config_keymap() +{ + // y0 row + key_map[0b00000000] = '1'; + key_map[0b00000001] = '2'; + key_map[0b00000010] = '3'; + key_map[0b00000011] = 'z'; + key_map[0b00000100] = '4'; + key_map[0b00000101] = '5'; + key_map[0b00000110] = '6'; + key_map[0b00000111] = '7'; + + // y1 row + key_map[0b00001000] = KEY_LEFT_GUI; // "CMMD" or "Cmd" + key_map[0b00001001] = 'q'; + key_map[0b00001010] = 'w'; + key_map[0b00001011] = 'e'; + key_map[0b00001100] = 'r'; + key_map[0b00001101] = 't'; + key_map[0b00001110] = 'y'; + key_map[0b00001111] = '`'; + + // y2 row + key_map[0b00010000] = 'x'; + key_map[0b00010001] = 'a'; + key_map[0b00010010] = 's'; + key_map[0b00010011] = 'd'; + key_map[0b00010100] = 'f'; + key_map[0b00010101] = 'g'; + key_map[0b00010110] = 'h'; + key_map[0b00010111] = ' '; // "Space 1" + + // y3 row + key_map[0b00011000] = KEY_CAPS_LOCK; + key_map[0b00011001] = KEY_TAB; + key_map[0b00011010] = KEY_LEFT_CTRL; + key_map[0b00011011] = 0; + key_map[0b00011100] = 0; + key_map[0b00011101] = 0; + key_map[0b00011110] = 0; + key_map[0b00011111] = 0; + + // y4 row + key_map[0b00100000] = 0; + key_map[0b00100001] = 0; + key_map[0b00100010] = 0; // Fn key + key_map[0b00100011] = KEY_LEFT_ALT; + key_map[0b00100100] = 0; + key_map[0b00100101] = 0; + key_map[0b00100110] = 0; + key_map[0b00100111] = 0; + + // y5 row + key_map[0b00101000] = 0; + key_map[0b00101001] = 0; + key_map[0b00101010] = 0; + key_map[0b00101011] = 0; + key_map[0b00101100] = 'c'; + key_map[0b00101101] = 'v'; + key_map[0b00101110] = 'b'; + key_map[0b00101111] = 'n'; + + // y6 row + key_map[0b00110000] = '-'; + key_map[0b00110001] = '='; + key_map[0b00110010] = KEY_BACKSPACE; + key_map[0b00110011] = 0; // "Special Function One" + key_map[0b00110100] = '8'; + key_map[0b00110101] = '9'; + key_map[0b00110110] = '0'; + key_map[0b00110111] = ' '; // "Space 2" + + // y7 row + key_map[0b00111000] = '['; + key_map[0b00111001] = ']'; + key_map[0b00111010] = '\\'; + key_map[0b00111011] = 0; // "Special Function Two" + key_map[0b00111100] = 'u'; + key_map[0b00111101] = 'i'; + key_map[0b00111110] = 'o'; + key_map[0b00111111] = 'p'; + + // y8 row + key_map[0b01000000] = '\''; + key_map[0b01000001] = KEY_RETURN; + key_map[0b01000010] = 0; // "Special Function Three" + key_map[0b01000011] = 0; + key_map[0b01000100] = 'j'; + key_map[0b01000101] = 'k'; + key_map[0b01000110] = 'l'; + key_map[0b01000111] = ';'; + + // y9 row + key_map[0b01001000] = '/'; + key_map[0b01001001] = KEY_UP_ARROW; + key_map[0b01001010] = 0; // "Special Function Four" + key_map[0b01001011] = 0; + key_map[0b01001100] = 'm'; + key_map[0b01001101] = ','; + key_map[0b01001110] = '.'; + key_map[0b01001111] = 0; // "DONE" or "Done" + + // y10 row + key_map[0b01010000] = KEY_DELETE; + key_map[0b01010001] = KEY_LEFT_ARROW; + key_map[0b01010010] = KEY_DOWN_ARROW; + key_map[0b01010011] = KEY_RIGHT_ARROW; + key_map[0b01010100] = 0; + key_map[0b01010101] = 0; + key_map[0b01010110] = 0; + key_map[0b01010111] = 0; + + // y11 row + key_map[0b01011000] = KEY_LEFT_SHIFT; + key_map[0b01011001] = KEY_RIGHT_SHIFT; + key_map[0b01011010] = 0; + key_map[0b01011011] = 0; + key_map[0b01011100] = 0; + key_map[0b01011101] = 0; + key_map[0b01011110] = 0; + key_map[0b01011111] = 0; +} + +void config_fnkeymap() +{ + fn_key_map[0b01010001] = KEY_HOME; // left arrow + fn_key_map[0b01010010] = KEY_PAGE_DOWN; // down arrow + fn_key_map[0b01010011] = KEY_END; // right arrow + fn_key_map[0b01001001] = KEY_PAGE_UP; // up arrow + fn_key_map[0b00011001] = KEY_ESC; // tab key + fn_key_map[0b00000000] = KEY_F1; // 1 + fn_key_map[0b00000001] = KEY_F2; // 2 + fn_key_map[0b00000010] = KEY_F3; // 3 + fn_key_map[0b00000100] = KEY_F4; // 4 + fn_key_map[0b00000101] = KEY_F5; // 5 + fn_key_map[0b00000110] = KEY_F6; // 6 + fn_key_map[0b00000111] = KEY_F7; // 7 + fn_key_map[0b00110100] = KEY_F8; // 8 + fn_key_map[0b00110101] = KEY_F9; // 9 + fn_key_map[0b00110110] = KEY_F10; // 0 + fn_key_map[0b00110000] = KEY_F11; // - + fn_key_map[0b00110001] = KEY_F12; // = +} + +void print_byte_bin(char bin_byte) +{ + Serial.print("0b"); + + for (int i = 7; i > -1; i--) Serial.print(int((bin_byte & (1 << i)) >> i)); +} + +void print_keychange(char key_byte, char key_code, int key_up) +{ + if (key_up) Serial.print("released: "); else Serial.print("pressed: "); + print_byte_bin(key_byte); + + Serial.print(" mapped to "); + + if (key_code) + { + print_byte_bin(key_code); + + if(PRINTABLE_CHAR(key_code)) + { + Serial.print(" ("); + Serial.print(key_code); + Serial.print(")"); + } + else + { + Serial.print(" (unprintable)"); + } + } + // Fn has no keycode, special case it + else if (key_byte == 34) + { + Serial.print("Fn"); + } + else + { + Serial.print("nothing"); + } + + Serial.println(""); +} + +void boot_keyboard() +{ + if (PPK_DEBUG) + { + // delay for a bit to allow for opening serial monitor etc. + for (int i = 0; i < 15; delay(1000 + i++)) Serial.print("."); + + Serial.println("beginning keyboard boot sequence"); + } + tp.DotStar_SetPower(true); + tp.DotStar_SetPixelColor(0x0000FF); + + + pinMode(VCC_PIN, OUTPUT); + pinMode(GND_PIN, OUTPUT); + pinMode(PULLDOWN_PIN, OUTPUT); + + pinMode(RX_PIN, INPUT); //Add external Pulldown + pinMode(DCD_PIN, INPUT); + pinMode(RTS_PIN, INPUT); + + digitalWrite(VCC_PIN, LOW); + digitalWrite(GND_PIN, LOW); + digitalWrite(PULLDOWN_PIN, LOW); + digitalWrite(VCC_PIN, HIGH); + + keyboard_serial.begin(9600); + keyboard_serial.listen(); + + if (PPK_DEBUG) Serial.print("waiting for keyboard response..."); + + while(digitalRead(DCD_PIN) != HIGH) {;}; + + if (PPK_DEBUG) Serial.println(" done"); + + + if (PPK_DEBUG) Serial.print("finishing handshake..."); + + if (digitalRead(RTS_PIN) == LOW) + { + delay(10); + pinMode(RTS_PIN, OUTPUT); + digitalWrite(RTS_PIN, HIGH); + } + else + { + pinMode(RTS_PIN, OUTPUT); + digitalWrite(RTS_PIN, HIGH); + digitalWrite(RTS_PIN, LOW); + delay(10); + digitalWrite(RTS_PIN, HIGH); + } + + delay(5); + + if (PPK_DEBUG) Serial.println(" done"); + + if (PPK_DEBUG) Serial.print("waiting for keyboard serial ID..."); + + while (keyboard_serial.available() < 2) {;}; + + if (PPK_DEBUG) Serial.println(" done"); + + int byte1 = keyboard_serial.read(); + int byte2 = keyboard_serial.read(); + + if (!((byte1 == 0xFA) && (byte2 == 0xFD))) + { + if (PPK_DEBUG) Serial.println("got wrong bytes? giving up here"); + tp.DotStar_SetPower(true); + tp.DotStar_SetPixelColor(0xFF00FF); + + while (1) {;}; + } + + last_comm = millis(); + tp.DotStar_SetPower(true); + tp.DotStar_SetPixelColor(0x00FFFF); +} + +int check_battery() +{ + //batVolt has voltage levels every 5% of charge level. Adjust as necessary. Slightly better than a straight linear interp of voltage + float batVolt_lookup[] = {3.27, 3.61, 3.69, 3.71, 3.73, 3.75, 3.77, 3.79, 3.80, 3.82, 3.84, 3.85, 3.87, 3.91, 3.95, 3.98, 4.02, 4.08, 4.11, 4.15, 4.20}; + float batVolt = 0.0; //volts + int batLevel = -1; //0 to 100 (percent as int) + + batVolt = tp.GetBatteryVoltage(); + for (int n=0; n<=21; n++) + { + if (batVolt < batVolt_lookup[n]) + { + batLevel = 5*n; + break; + } + } + if (batLevel < 0) + { + //You shouldn't be here, batVolt was not found in range of lookup values. + batLevel = 100; + } + return batLevel; +} + +void check_connection() +{ + if(digitalRead(DET_PIN)) + { + boot_state = 0; + ble_kb.end(); + delay(500); + tp.DotStar_SetPower(true); + tp.DotStar_SetPixelColor(0x0000FF); + } +} + +void setup() +{ + if (PPK_DEBUG) + { + Serial.begin(9600); + Serial.print("compiled in debug mode with PPK_VERSION "); + Serial.println(PPK_VERSION); + } + + config_keymap(); + config_fnkeymap(); + pinMode(DET_PIN, INPUT_PULLUP); + + boot_keyboard(); + ble_kb.begin(); + tp.DotStar_SetPower(true); + //tp.DotStar_SetBrightness(64); //25% brightness + last_bat = BAT_CHECK_TIME; + + if (PPK_DEBUG) Serial.println("setup completed"); +} + +void loop() +{ + + if (keyboard_serial.available()) + { + for (int i = keyboard_serial.available(); i > 0; i--) + { + key_byte = keyboard_serial.read(); + + int key_up = key_byte & UPDOWN_MASK; + int key_x = 0 + (key_byte & X_MASK); + int key_y = 0 + ((key_byte & Y_MASK) >> 3); + + char key_code = 0; + + if (fn_key_down && fn_key_map[key_byte & MAP_MASK]) + { + key_code += fn_key_map[key_byte & MAP_MASK]; + } + else + { + key_code += key_map[key_byte & MAP_MASK]; + } + + // keyboard duplicates the final key-up byte + if (key_byte == last_byte) + { + ble_kb.releaseAll(); + } + else + { + if (PPK_DEBUG) print_keychange(key_byte & MAP_MASK, key_code, key_up); + + if (key_code != 0) + { + if (key_up) + { + ble_kb.release(key_code); + } + else + { + ble_kb.press(key_code); + } + } + else + { + // special case the Fn key + if ((key_byte & MAP_MASK) == 34) + { + fn_key_down = !key_up; + } + } + } + + last_byte = key_byte; + last_comm = millis(); + } + } + else + { + // reboot if no recent comms, otherwise keyboard falls asleep + if ((millis() - last_comm) > TIMEOUT) + { + if (PPK_DEBUG) Serial.println("rebooting keyboard for timeout"); + + digitalWrite(VCC_PIN, LOW); + boot_keyboard(); + } + } + + if ((millis() - last_bat) > BAT_CHECK_TIME) + { + batLevel = check_battery(); + ble_kb.setBatteryLevel(batLevel); + if (batLevel > 30) //30-100% + { + //set green heartbeat + heartbeat_color = 0x00FF00; + } + else if (batLevel >= 15)//15-30% + { + //set yellow/orange heartbeat + heartbeat_color = 0xFF8000; + } + else // <15% + { + //set red heartbeat + heartbeat_color = 0xFF0000; + } + tp.DotStar_SetPower(false); + last_bat = millis(); + } + if ((millis() - last_heartbeat) > (HEARTBEAT_TIME + heartbeat_length)) + { + tp.DotStar_Clear(); + tp.DotStar_SetPower(false); + last_heartbeat = millis(); + } + else if ((millis() - last_heartbeat) > HEARTBEAT_TIME) + { + tp.DotStar_SetPower(true); + tp.DotStar_SetPixelColor(heartbeat_color); + //check_connection(); + } + +}