diff --git a/README.md b/README.md index 13a3753..9ca16b7 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ Library for KEYBOARD using [M5UnitUnified](https://github.com/m5stack/M5UnitUnified). M5UnitUnified is a library for unified handling of various M5 units products. +### SKU:U215 +CardKB v2 is a card-size '42 key' QWERTY keyboard. ### SKU:U035-B CardKB v1.1 is a card-size '50 key' QWERTY keyboard. Adpots ATMega8A as the MCU, communication port I2C, and one 'RGB-LED' indicator. @@ -69,6 +71,6 @@ It will output it under docs/html If you want to output Git commit hashes to html, do it for the git cloned folder. #### Required -- [Doxyegn](https://www.doxygen.nl/) +- [Doxygen](https://www.doxygen.nl/) - [pcregrep](https://formulae.brew.sh/formula/pcre2) - [Git](https://git-scm.com/) (Output commit hash to html) diff --git a/examples/UnitUnified/SimpleDisplay/main/SimpleDisplay.cpp b/examples/UnitUnified/SimpleDisplay/main/SimpleDisplay.cpp index 3ded191..2715076 100644 --- a/examples/UnitUnified/SimpleDisplay/main/SimpleDisplay.cpp +++ b/examples/UnitUnified/SimpleDisplay/main/SimpleDisplay.cpp @@ -1,10 +1,10 @@ /* - * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD * * SPDX-License-Identifier: MIT */ /* - Display example using M5UnitUnified for UnitCardKB/UnitFacesQWERTY + Display example using M5UnitUnified for UnitCardKB/UnitCardKB2/UnitFacesQWERTY */ #include #include @@ -16,9 +16,17 @@ // ************************************************************* // Choose one define symbol to match the unit you are using // ************************************************************* -#if !defined(USING_UNIT_CARDKB) && !defined(USING_UNIT_FACES_QWERTY) +#if !defined(USING_UNIT_CARDKB) && !defined(USING_UNIT_CARDKB2) && !defined(USING_UNIT_FACES_QWERTY) // For CardKB // #define USING_UNIT_CARDKB + +// For CardKB2 +#define USING_UNIT_CARDKB2 +#if defined(USING_UNIT_CARDKB2) +// #define USE_I2C_FOR_CARDKB2 +#define USE_UART_FOR_CARDKB2 +#endif + // For FacesQWERTY // #define USING_UNIT_FACES_QWERTY #endif @@ -29,10 +37,12 @@ auto& lcd = M5.Display; m5::unit::UnitUnified Units; #if defined(USING_UNIT_CARDKB) m5::unit::UnitCardKB unit; +#elif defined(USING_UNIT_CARDKB2) +m5::unit::UnitCardKB2 unit; #elif defined(USING_UNIT_FACES_QWERTY) m5::unit::UnitFacesQWERTY unit; #else -#error Must choose unit define, USING_UNIT_CARDKB or USING_UNIT_FACES_QWERTY +#error Must choose unit define, USING_UNIT_CARDKB or USING_UNIT_CARDKB2 or USING_UNIT_FACES_QWERTY #endif bool small_display{}; @@ -54,13 +64,33 @@ void setup() small_display = lcd.width() < 240; lcd.fillScreen(TFT_LIGHTGRAY); - auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda); - auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl); - M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl); + auto pin_num_sda_tx = M5.getPin(m5::pin_name_t::port_a_sda); + auto pin_num_scl_rx = M5.getPin(m5::pin_name_t::port_a_scl); + M5_LOGI("getPin: SDA/TX:%u SCL/RX:%u", pin_num_sda_tx, pin_num_scl_rx); +#ifdef USE_I2C_FOR_CARDKB2 Wire.end(); - Wire.begin(pin_num_sda, pin_num_scl, 100 * 1000U); + Wire.begin(pin_num_sda_tx, pin_num_scl_rx, 100 * 1000U); if (!Units.add(unit, Wire) || !Units.begin()) { +#elif defined(USE_UART_FOR_CARDKB2) + M5.Power.setExtPower(false); // Turn off port power to reset the sym status. + m5::utility::delay(100); + M5.Power.setExtPower(true); + m5::utility::delay(100); +#if defined(CONFIG_IDF_TARGET_ESP32C6) + auto& serial = Serial1; +#elif SOC_UART_NUM > 2 + auto& serial = Serial2; +#elif SOC_UART_NUM > 1 + auto& serial = Serial1; +#else +#error "Not enough Serial" +#endif + + serial.begin(115200, SERIAL_8N1, pin_num_scl_rx, pin_num_sda_tx); + + if (!Units.add(unit, serial) || !Units.begin()) { +#endif M5_LOGE("Failed to begin"); lcd.fillScreen(TFT_RED); while (true) { @@ -72,6 +102,9 @@ void setup() #if defined(USING_UNIT_CARDKB) M5.Log.printf("Hardware:%02X Firmware:%02X\n", unit.hardwareType(), unit.firmwareVersion()); #endif +#if defined(USING_UNIT_CARDKB2) + M5.Log.printf("Firmware:%02X\n", unit.firmwareVersion()); +#endif #if defined(USING_UNIT_FACES_QWERTY) M5.Log.printf("FacesType:%02X Firmware:%02X\n", unit.facesType(), unit.firmwareVersion()); #endif @@ -89,6 +122,7 @@ void loop() auto touch = M5.Touch.getDetail(); Units.update(); +#if !defined(USING_UNIT_CARDKB2) // Toggle behavior if using M5Unit-KEYBOARD firmware if (unit.firmwareVersion() && (M5.BtnA.wasClicked() || touch.wasClicked())) { scan_mode = !scan_mode; @@ -97,7 +131,7 @@ void loop() str = ""; dirty = true; } - +#endif auto prev_str = str.size(); static auto prev_mod = unit.modifierBits(); @@ -108,10 +142,10 @@ void loop() if (unit.updated()) { while (unit.available()) { ch = unit.getchar(); - M5.Log.printf("Char:[%02X %c]\n", ch, std::isprint(ch) ? ch : ' '); + M5.Log.printf("Char:[0x%02X=%d %c]\n", ch, ch, std::isprint(ch) ? ch : ' '); if (std::isprint(ch)) { str += ch; - } else if (ch == 0x0D) { + } else if (ch == '\r' || ch == '\n') { str += '\n'; } else if (ch == 0x08 && !str.empty()) { str.pop_back(); @@ -123,6 +157,7 @@ void loop() dirty |= (unit.nowBits() ^ unit.previousBits()); +#if !defined(USING_UNIT_CARDKB2) if (scan_mode) { // For Scan mode @@ -200,9 +235,12 @@ void loop() #endif } else { +#endif // For Released mode lcd.drawString("Conventional", 0, 0); +#if !defined(USING_UNIT_CARDKB2) } +#endif // String if (str.size() != prev_str) { diff --git a/src/M5UnitUnifiedKEYBOARD.hpp b/src/M5UnitUnifiedKEYBOARD.hpp index 918abb4..dcedd77 100644 --- a/src/M5UnitUnifiedKEYBOARD.hpp +++ b/src/M5UnitUnifiedKEYBOARD.hpp @@ -15,6 +15,7 @@ #include "unit/unit_Keyboard.hpp" #include "unit/unit_CardKB.hpp" +#include "unit/unit_CardKB2.hpp" #include "unit/unit_FacesQWERTY.hpp" /*! diff --git a/src/unit/unit_CardKB.cpp b/src/unit/unit_CardKB.cpp index b5709c8..e2ce2a9 100644 --- a/src/unit/unit_CardKB.cpp +++ b/src/unit/unit_CardKB.cpp @@ -325,7 +325,7 @@ bool UnitCardKB::update_new_firmware(const types::elapsed_time_t at) auto prev_holding = _holding; uint8_t rbuf[(NUMBER_OF_KEYS + 7) / 8 + 1]{}; - if (!readRegister(CMD_SCAN_REG, rbuf, m5::stl::size(rbuf), 0)) { + if (!readRegister(scan_reg(), rbuf, m5::stl::size(rbuf), 0)) { M5_LIB_LOGE("Failed to read"); return false; } diff --git a/src/unit/unit_CardKB2.cpp b/src/unit/unit_CardKB2.cpp new file mode 100644 index 0000000..0475cce --- /dev/null +++ b/src/unit/unit_CardKB2.cpp @@ -0,0 +1,465 @@ +/* + * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/*! + @file unit_CardKB2.cpp + @brief CardKB2 Unit for M5UnitUnified +*/ +#include "unit_CardKB2.hpp" +#include +#include + +using namespace m5::utility::mmh3; +using namespace m5::unit::types; +using namespace m5::unit::keyboard; +using namespace m5::unit::keyboard::command; +using m5::unit::UnitCardKB2; +using Packet = m5::unit::UnitCardKB2::Packet; + +namespace { +constexpr uint8_t key_map[][4 /* mode: normal, shift, sym, fn */] = { + // key_id = line *11+column, line range(0~3), column range(0~10) + // line1 + {'1', '1', '!', 27}, // 0: 1 (Fn+1=ESC) + {'2', '2', '@', 128}, // 1: 2 + {'3', '3', '#', 129}, // 2: 3 + {'4', '4', '$', 130}, // 3: 4 + {'5', '5', '%', 131}, // 4: 5 + {'6', '6', '^', 132}, // 5: 6 + {'7', '7', '&', 133}, // 6: 7 + {'8', '8', '*', 134}, // 7: 8 + {'9', '9', '(', 135}, // 8: 9 + {'0', '0', ')', 136}, // 9: 0 + // line2 + {0, 0, 0, 0}, // 10: no key + {'q', 'Q', '~', 137}, // 11: q + {'w', 'W', '`', 138}, // 12: w + {'e', 'E', '?', 139}, // 13: e + {'r', 'R', '\\', 140}, // 14: r + {'t', 'T', '/', 141}, // 15: t + {'y', 'Y', '|', 142}, // 16: y + {'u', 'U', '_', 143}, // 17: u + {'i', 'I', '-', 144}, // 18: i + {'o', 'O', '+', 145}, // 19: o + {'p', 'P', '=', 146}, // 20: p + {0x08, 0x08, 0x08, 0x08}, // 21: delete + // line3 + {0, 0, 0, 0}, // 22: Aa (cap lock toggle, no char) + {'a', 'A', '{', 148}, // 23: a + {'s', 'S', '}', 149}, // 24: s + {'d', 'D', '^', 181}, // 25: d (Fn+D=UP) + {'f', 'F', '[', 150}, // 26: f + {'g', 'G', ']', 151}, // 27: g + {'h', 'H', '"', 152}, // 28: h + {'j', 'J', '\'', 153}, // 29: j + {'k', 'K', ';', 154}, // 30: k + {'l', 'L', ':', 155}, // 31: l + {0x0A, 0x0A, 0x0A, 0x0A}, // 32: enter + // line4 + {0, 0, 0, 0}, // 33: fn (function key, no char) + {0, 0, 0, 0}, // 34: sym (symbol key, no char) + {'z', 'Z', 'z', 180}, // 35: z (Fn+Z=LEFT) + {'x', 'X', 'x', 182}, // 36: x (Fn+X=DOWN) + {'c', 'C', 'c', 183}, // 37: c (Fn+C=RIGHT) + {'v', 'V', '<', 157}, // 38: v + {'b', 'B', '>', 158}, // 39: b + {'n', 'N', ',', 159}, // 40: n + {'m', 'M', '.', 160}, // 41: m + {' ', ' ', ' ', 161} // 42: space +}; +static_assert(m5::stl::size(key_map) == UnitCardKB2::NUMBER_OF_KEYS, "Invalid size"); + +// modifier bit to key_map mode index +constexpr uint8_t mod_table[] = {1, 0, 3, 2}; // 0x01:Shift, 0x80:Symbol 0x40:Fucntion + +// ASCII to mode bit and key_index_t +// 1:normal 2:shift 4:symbol 8:function +// 0x0: no key, 0xFF: invalid char +constexpr std::pair character_map[] = { + {0x00, 0xFF}, // NULL (0) + {0x00, 0xFF}, // SOH (1) + {0x00, 0xFF}, // STX (2) + {0x00, 0xFF}, // ETX (3) + {0x00, 0xFF}, // EOT (4) + {0x00, 0xFF}, // ENG (5) + {0x00, 0xFF}, // ACK (6) + {0x00, 0xFF}, // BEL (7) + {1 + 4, UnitCardKB2::KEY_DELETE}, // BS (8) + {0x00, 0xFF}, // HT (9) + {1 + 2 + 4, UnitCardKB2::KEY_ENTER}, // LF (10) + {0x00, 0xFF}, // VT (11) + {0x00, 0xFF}, // FF (12) + {1 + 2 + 4, UnitCardKB2::KEY_ENTER}, // CR (13) + {0x00, 0xFF}, // SO (14) + {0x00, 0xFF}, // SI (15) + {0x00, 0xFF}, // DLE (16) + {0x00, 0xFF}, // DC1 (17) + {0x00, 0xFF}, // DC2 (18) + {0x00, 0xFF}, // DC3 (19) + {0x00, 0xFF}, // DC4 (20) + {0x00, 0xFF}, // NAK (21) + {0x00, 0xFF}, // SYN (22) + {0x00, 0xFF}, // ETB (23) + {0x00, 0xFF}, // CAN (24) + {0x00, 0xFF}, // EM (25) + {0x00, 0xFF}, // SUB (26) + {0x00, 0xFF}, // ESC (27) + {0x00, 0xFF}, // FS (28) + {0x00, 0xFF}, // GS (29) + {0x00, 0xFF}, // RS (30) + {0x00, 0xFF}, // US (31) + {1 + 2 + 4, UnitCardKB2::KEY_SPACE}, // SP (32) + {4, UnitCardKB2::KEY_1}, // ! (33) + {4, UnitCardKB2::KEY_H}, // " (34) + {4, UnitCardKB2::KEY_3}, // # (35) + {4, UnitCardKB2::KEY_4}, // $ (36) + {4, UnitCardKB2::KEY_5}, // % (37) + {4, UnitCardKB2::KEY_7}, // & (38) + {4, UnitCardKB2::KEY_J}, // ' (39) + {4, UnitCardKB2::KEY_9}, // ( (40) + {4, UnitCardKB2::KEY_0}, // ) (41) + {4, UnitCardKB2::KEY_8}, // * (42) + {4, UnitCardKB2::KEY_O}, // + (43) + {4, UnitCardKB2::KEY_N}, // , (44) + {4, UnitCardKB2::KEY_I}, // - (45) + {4, UnitCardKB2::KEY_M}, // . (46) + {4, UnitCardKB2::KEY_T}, // / (47) + {1 + 2, UnitCardKB2::KEY_0}, // 0 (48) + {1 + 2, UnitCardKB2::KEY_1}, // 1 (49) + {1 + 2, UnitCardKB2::KEY_2}, // 2 (50) + {1 + 2, UnitCardKB2::KEY_3}, // 3 (51) + {1 + 2, UnitCardKB2::KEY_4}, // 4 (52) + {1 + 2, UnitCardKB2::KEY_5}, // 5 (53) + {1 + 2, UnitCardKB2::KEY_6}, // 6 (54) + {1 + 2, UnitCardKB2::KEY_7}, // 7 (55) + {1 + 2, UnitCardKB2::KEY_8}, // 8 (56) + {1 + 2, UnitCardKB2::KEY_9}, // 9 (57) + {4, UnitCardKB2::KEY_L}, // : (58) + {4, UnitCardKB2::KEY_K}, // ; (59) + {4, UnitCardKB2::KEY_V}, // < (60) + {4, UnitCardKB2::KEY_P}, // = (61) + {4, UnitCardKB2::KEY_B}, // > (62) + {4, UnitCardKB2::KEY_E}, // ? (63) + {4, UnitCardKB2::KEY_2}, // @ (64) + {2, UnitCardKB2::KEY_A}, // A (65) + {2, UnitCardKB2::KEY_B}, // B (66) + {2, UnitCardKB2::KEY_C}, // C (67) + {2, UnitCardKB2::KEY_D}, // D (68) + {2, UnitCardKB2::KEY_E}, // E (69) + {2, UnitCardKB2::KEY_F}, // F (70) + {2, UnitCardKB2::KEY_G}, // G (71) + {2, UnitCardKB2::KEY_H}, // H (72) + {2, UnitCardKB2::KEY_I}, // I (73) + {2, UnitCardKB2::KEY_J}, // J (74) + {2, UnitCardKB2::KEY_K}, // K (75) + {2, UnitCardKB2::KEY_L}, // L (76) + {2, UnitCardKB2::KEY_M}, // M (77) + {2, UnitCardKB2::KEY_N}, // N (78) + {2, UnitCardKB2::KEY_O}, // O (79) + {2, UnitCardKB2::KEY_P}, // P (80) + {2, UnitCardKB2::KEY_Q}, // Q (81) + {2, UnitCardKB2::KEY_R}, // R (82) + {2, UnitCardKB2::KEY_S}, // S (83) + {2, UnitCardKB2::KEY_T}, // T (84) + {2, UnitCardKB2::KEY_U}, // U (85) + {2, UnitCardKB2::KEY_V}, // V (86) + {2, UnitCardKB2::KEY_W}, // W (87) + {2, UnitCardKB2::KEY_X}, // X (88) + {2, UnitCardKB2::KEY_Y}, // Y (89) + {2, UnitCardKB2::KEY_Z}, // Z (90) + {4, UnitCardKB2::KEY_F}, // [ (91) + {4, UnitCardKB2::KEY_R}, // \ (92) + {4, UnitCardKB2::KEY_G}, // ] (93) + {4, UnitCardKB2::KEY_D}, // ^ (94) + {4, UnitCardKB2::KEY_U}, // _ (95) + {4, UnitCardKB2::KEY_W}, // ` (96) + {1, UnitCardKB2::KEY_A}, // a (97) + {1, UnitCardKB2::KEY_B}, // b (98) + {1, UnitCardKB2::KEY_C}, // c (99) + {1, UnitCardKB2::KEY_D}, // d (100) + {1, UnitCardKB2::KEY_E}, // e (101) + {1, UnitCardKB2::KEY_F}, // f (102) + {1, UnitCardKB2::KEY_G}, // g (103) + {1, UnitCardKB2::KEY_H}, // h (104) + {1, UnitCardKB2::KEY_I}, // i (105) + {1, UnitCardKB2::KEY_J}, // j (106) + {1, UnitCardKB2::KEY_K}, // k (107) + {1, UnitCardKB2::KEY_L}, // l (108) + {1, UnitCardKB2::KEY_M}, // m (109) + {1, UnitCardKB2::KEY_N}, // n (110) + {1, UnitCardKB2::KEY_O}, // o (111) + {1, UnitCardKB2::KEY_P}, // p (112) + {1, UnitCardKB2::KEY_Q}, // q (113) + {1, UnitCardKB2::KEY_R}, // r (114) + {1, UnitCardKB2::KEY_S}, // s (115) + {1, UnitCardKB2::KEY_T}, // t (116) + {1, UnitCardKB2::KEY_U}, // u (117) + {1, UnitCardKB2::KEY_V}, // v (118) + {1, UnitCardKB2::KEY_W}, // w (119) + {1, UnitCardKB2::KEY_X}, // x (120) + {1, UnitCardKB2::KEY_Y}, // y (121) + {1, UnitCardKB2::KEY_Z}, // z (122) + {4, UnitCardKB2::KEY_A}, // { (123) + {4, UnitCardKB2::KEY_Y}, // | (124) + {4, UnitCardKB2::KEY_S}, // } (125) + {4, UnitCardKB2::KEY_Q}, // ~ (126) + {2, UnitCardKB2::KEY_DELETE}, // DEL (127) +}; +static_assert(m5::stl::size(character_map) == 128, "Invalid size"); + +constexpr std::pair special_character_map[] = { + {1 + 2 + 4, UnitCardKB2::KEY_Z}, // Left cursor (Fn+Z) + {1 + 2 + 4, UnitCardKB2::KEY_D}, // Up cursor (Fn+D) + {1 + 2 + 4, UnitCardKB2::KEY_X}, // Down cursor (Fn+X) + {1 + 2 + 4, UnitCardKB2::KEY_C}, // Right cursor (Fn+C) +}; + +constexpr uint16_t PACKET_HEADER{0xAA}; +enum PID : uint8_t { + PID_DATA_LEN = 0x03, +}; + +enum KeyState : uint8_t { + KEY_STATE_PRESSED = 0x01, + KEY_STATE_RELEASED = 0x02, +}; + +constexpr uint32_t CAPS_DOUBLE_CLICK_WINDOW_MS = 280; +constexpr uint32_t CAPS_HOLD_THRESHOLD_MS = 350; + +bool is_valid_ack(const Packet& p) +{ + if (p.size() != 5 || p[0] != PACKET_HEADER || p[1] != PID_DATA_LEN) { + return false; + } + if (p[3] != KEY_STATE_PRESSED && p[3] != KEY_STATE_RELEASED) { + return false; + } + const uint8_t checksum = static_cast((p[1] + p[2] + p[3]) & 0xFF); + return p[4] == checksum; +} + +} // namespace + +namespace m5 { +namespace unit { + +// class UnitCardKB +const char UnitCardKB2::name[] = "UnitCardKB2"; +const types::uid_t UnitCardKB2::uid{"UnitCardKB2"_mmh3}; +const types::attr_t UnitCardKB2::attr{attribute::AccessI2C | attribute::AccessUART}; + +key_index_t UnitCardKB2::character_to_key_index(const char ch) +{ + unsigned char uc = ch; + // function (>= 0x80) + if (uc & 0x80) { + key_index_t kidx = (key_index_t)(uc - 0x80); + // Special key? + if (uc >= SCHAR_LEFT && uc <= SCHAR_RIGHT) { + return special_character_map[uc - SCHAR_LEFT].second; + } + return static_cast((kidx < m5::stl::size(key_map)) ? kidx : 0xFF); + } + // normal,shift or symbol + return static_cast((uc < m5::stl::size(character_map)) ? (character_map[uc].second) : 0xFF); +} + +uint8_t UnitCardKB2::character_to_mode_bits(const char ch) +{ + unsigned char uc = ch; + // function? (>= 0x80) + if (uc & 0x80) { + key_index_t kidx = (key_index_t)(uc - 0x80); + // Special key? + if (uc >= SCHAR_LEFT && uc <= SCHAR_RIGHT) { + // M5_LIB_LOGI("%c => %02X", ch, special_character_map[uc - SCHAR_LEFT].first); + return special_character_map[uc - SCHAR_LEFT].first; + } + + // M5_LIB_LOGI("%c => %02X", ch, (kidx < m5::stl::size(key_map)) ? 0x08 : 0x00); + return (kidx < m5::stl::size(key_map)) ? 0x08 : 0x00; + } + // normal,shift or symbol + // M5_LIB_LOGI("%c => %02X", ch, (uc < m5::stl::size(character_map)) ? (character_map[uc].first) : 0x00); + return (uc < m5::stl::size(character_map)) ? (character_map[uc].first) : 0x00; +} + +bool UnitCardKB2::begin() +{ + auto ssize = stored_size(); + assert(ssize && "stored_size must be greater than zero"); + if (ssize != _data->capacity()) { + _data.reset(new m5::container::CircularBuffer(ssize)); + if (!_data) { + M5_LIB_LOGE("Failed to allocate"); + return false; + } + } + + auto uart = asAdapter(Adapter::Type::UART); + if (uart) { + uart->setTimeout(10); + uart->flushRX(); + _mode = Mode::M5UnitUnified; + _firmware_version = 0; + return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.interval) : true; + } + + auto i2c = asAdapter(Adapter::Type::I2C); + if (!i2c) { + M5_LIB_LOGE("Illegal adapter"); + return false; + } + + uint8_t discard{}; + // Read and reject values to avoid false evaluations due to transmission of values for released keys + readWithTransaction(&discard, 1); + auto reg = registerMap(); + reg.firmware_version_reg = 0xF1; + registerMap(reg); + readFirmwareVersion(_firmware_version); + M5_LIB_LOGI("Type:%S Firmware:%02X", "CardKB2", _firmware_version); + + if (!firmwareVersion()) { + return false; + } + return UnitKeyboardBitwise::begin() && (_cfg.start_periodic ? startPeriodicMeasurement(_cfg.interval) : true); +} + +uint8_t UnitCardKB2::read_data(Packet& rbuf) +{ + rbuf.clear(); + rbuf.resize(1); + + // Sync to frame header first to avoid permanent desynchronization. + if (readWithTransaction(rbuf.data(), 1) != m5::hal::error::error_t::OK || rbuf[0] != PACKET_HEADER) { + return 0; + } + + rbuf.resize(5); + if (readWithTransaction(rbuf.data() + 1, 4) == m5::hal::error::error_t::OK && is_valid_ack(rbuf)) { + return rbuf[3]; // key state + } + return 0; +} + +void UnitCardKB2::update(const bool force) +{ + if (!inPeriodic()) { + return; + } + + if (asAdapter(Adapter::Type::UART)) { + auto at = m5::utility::millis(); + if (!(force || !_latest || at >= _latest + _interval)) { + return; + } + + // Expire click sequence window. + if (!_caps_pressing && _caps_click_count && (at - _caps_last_release_at) > CAPS_DOUBLE_CLICK_WINDOW_MS) { + _caps_click_count = 0; + } + + // While keeping Caps key pressed, enable temporary uppercase. + if (_caps_pressing && !_caps_hold_active && (at - _caps_pressed_at) >= CAPS_HOLD_THRESHOLD_MS) { + _caps_hold_active = true; + _caps_shift_once = false; + _caps_lock = false; + _caps_click_count = 0; + } + + _updated = false; + _prev = _now; + _wasPressed = _wasReleased = _wasHold = 0; + _holding = _repeating = 0; + + Packet rbuf{}; + const uint8_t state = read_data(rbuf); + if (!state || rbuf.size() != 5) { + return; + } + + const uint8_t kidx = rbuf[2]; + M5_LIB_LOGI("kidx:%d", kidx); + if (kidx >= NUMBER_OF_KEYS) { + return; + } + + const uint64_t bit = (1ULL << kidx); + uint8_t ch = 0; + if (state == KEY_STATE_PRESSED) { + _now |= bit; + + if (kidx == KEY_SYM) { // sym pressed + _sym_was_pressed = !_sym_was_pressed; + } + + if (kidx == KEY_AA) { // caps key + _caps_pressing = true; + _caps_pressed_at = at; + } else { + const bool caps_active = (_caps_lock || _caps_hold_active || _caps_shift_once); + if (!_sym_was_pressed) { + ch = key_map[kidx][caps_active ? 1 : 0]; + } else { + ch = key_map[kidx][2]; + } + + if (ch) { + _data->push_back(ch); + } + + // One-shot uppercase is consumed by the next non-caps key. + if (_caps_shift_once && !_caps_lock && !_caps_hold_active) { + _caps_shift_once = false; + } + } + } else if (state == KEY_STATE_RELEASED) { + _now &= ~bit; + + if (kidx == KEY_AA && _caps_pressing) { + _caps_pressing = false; + + if (_caps_hold_active) { + _caps_hold_active = false; + _caps_shift_once = false; + _caps_lock = false; + _caps_click_count = 0; + } else { + if (_caps_click_count == 1 && (at - _caps_last_release_at) <= CAPS_DOUBLE_CLICK_WINDOW_MS) { + _caps_lock = !_caps_lock; + _caps_shift_once = false; + _caps_click_count = 0; + } else { + if (_caps_lock) { + _caps_lock = false; + _caps_shift_once = false; + } else { + _caps_shift_once = true; + } + _caps_click_count = 1; + _caps_last_release_at = at; + } + } + } + } else { + return; + } + + _wasPressed = (_now ^ _prev) & _now; + _wasReleased = (_now ^ _prev) & ~_now; + _updated = (_wasPressed | _wasReleased); + if (_updated) { + _latest = at; + } + return; + } + + UnitKeyboardBitwise::update(force); +} + +} // namespace unit +} // namespace m5 diff --git a/src/unit/unit_CardKB2.hpp b/src/unit/unit_CardKB2.hpp new file mode 100644 index 0000000..8193256 --- /dev/null +++ b/src/unit/unit_CardKB2.hpp @@ -0,0 +1,191 @@ +/* + * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/*! + @file unit_CardKB2.hpp + @brief CardKB2 Unit for M5UnitUnified +*/ +#ifndef M5_UNIT_KEYBOARD_UNIT_CARD_KB2_HPP +#define M5_UNIT_KEYBOARD_UNIT_CARD_KB2_HPP + +#include "unit_Keyboard.hpp" +#include +#include + +namespace m5 { +namespace unit { +/*! + @class m5::unit::UnitCardKB2 + @brief Card-size 50 key QWERTY keyboard + @warning Note that older firmware can only detect if the key is released +*/ +class UnitCardKB2 : public UnitKeyboardBitwise { + M5_UNIT_COMPONENT_HPP_BUILDER(UnitCardKB2, 0x5F); + +public: + using Packet = std::vector; + + static constexpr uint8_t NUMBER_OF_KEYS{43}; + + ///@name key index (bit position in scan result) + ///@{ + static constexpr keyboard::key_index_t KEY_1{0}; + static constexpr keyboard::key_index_t KEY_2{1}; + static constexpr keyboard::key_index_t KEY_3{2}; + static constexpr keyboard::key_index_t KEY_4{3}; + static constexpr keyboard::key_index_t KEY_5{4}; + static constexpr keyboard::key_index_t KEY_6{5}; + static constexpr keyboard::key_index_t KEY_7{6}; + static constexpr keyboard::key_index_t KEY_8{7}; + static constexpr keyboard::key_index_t KEY_9{8}; + static constexpr keyboard::key_index_t KEY_0{9}; + static constexpr keyboard::key_index_t KEY_Q{11}; + static constexpr keyboard::key_index_t KEY_W{12}; + static constexpr keyboard::key_index_t KEY_E{13}; + static constexpr keyboard::key_index_t KEY_R{14}; + static constexpr keyboard::key_index_t KEY_T{15}; + static constexpr keyboard::key_index_t KEY_Y{16}; + static constexpr keyboard::key_index_t KEY_U{17}; + static constexpr keyboard::key_index_t KEY_I{18}; + static constexpr keyboard::key_index_t KEY_O{19}; + static constexpr keyboard::key_index_t KEY_P{20}; + static constexpr keyboard::key_index_t KEY_DELETE{21}; + static constexpr keyboard::key_index_t KEY_AA{22}; + static constexpr keyboard::key_index_t KEY_A{23}; + static constexpr keyboard::key_index_t KEY_S{24}; + static constexpr keyboard::key_index_t KEY_D{25}; + static constexpr keyboard::key_index_t KEY_F{26}; + static constexpr keyboard::key_index_t KEY_G{27}; + static constexpr keyboard::key_index_t KEY_H{28}; + static constexpr keyboard::key_index_t KEY_J{29}; + static constexpr keyboard::key_index_t KEY_K{30}; + static constexpr keyboard::key_index_t KEY_L{31}; + static constexpr keyboard::key_index_t KEY_ENTER{32}; + static constexpr keyboard::key_index_t KEY_FN{33}; + static constexpr keyboard::key_index_t KEY_SYM{34}; + static constexpr keyboard::key_index_t KEY_Z{35}; + static constexpr keyboard::key_index_t KEY_X{36}; + static constexpr keyboard::key_index_t KEY_C{37}; + static constexpr keyboard::key_index_t KEY_V{38}; + static constexpr keyboard::key_index_t KEY_B{39}; + static constexpr keyboard::key_index_t KEY_N{40}; + static constexpr keyboard::key_index_t KEY_M{41}; + static constexpr keyboard::key_index_t KEY_SPACE{42}; + ///@} + + ///@name Character code for special keys + ///@{ + static constexpr char SCHAR_LEFT{(char)180}; + static constexpr char SCHAR_UP{(char)181}; + static constexpr char SCHAR_DOWN{(char)182}; + static constexpr char SCHAR_RIGHT{(char)183}; + ///@} + + /*! + @struct config_t + @brief Settings for begin + */ + struct config_t { + //! Start periodic measurement on begin? + bool start_periodic{true}; + ///@name For M5Unit-KEYBOARD firmware + ///@{ + /*! Mode */ + keyboard::Mode mode{keyboard::Mode::Conventional}; + //! Periodic interval + uint32_t interval{10}; + //! Threshold for key repeating (ms) + uint32_t repeating_threshold{400}; + //! Threshold for key holding (ms) + uint32_t holding_threshold{800}; + ///@} + }; + + explicit UnitCardKB2(const uint8_t addr = DEFAULT_ADDRESS) : UnitKeyboardBitwise(addr) + { + _repeat_start_at.resize(NUMBER_OF_KEYS); + _hold_start_at.resize(NUMBER_OF_KEYS); + } + virtual bool begin() override; + virtual void update(const bool force = false) override; + + ///@name Settings for begin + ///@{ + /*! @brief Gets the configration */ + inline config_t config() + { + return _cfg; + } + //! @brief Set the configration + inline void config(const config_t& cfg) + { + _cfg = cfg; + } + ///@} + + inline virtual keyboard::key_index_t toKeyIndex(const char ch) const override + { + return character_to_key_index(ch); + } + + /*! + @brief Character to key index + @retval != 0xFF keyboard::key_index_t + @retval == 0xFF No corresponding key index exists + */ + static keyboard::key_index_t character_to_key_index(const char ch); + /*! + @brief Character to mode bits + @retval == 0 Not exists + @retval != 0 Bits in corresponding mode + @note 0x01:normal 0x02:shift 0x04:symbol 0x08:function + */ + static uint8_t character_to_mode_bits(const char ch); + + ///@warning API valid only if using M5Unit-KEYBOARD firmware + ///@name Hardware type + ///@{ + /*! + @brief Gets the hardware type + @warning Valid after begin + */ + uint8_t hardwareType() const + { + return _type; + } + /*! + @brief Read the hardware type + @param[out] htype Hardware type + @return True if successful + */ + bool readHardwareType(uint8_t& htype); + ///@} + +protected: + uint8_t read_data(Packet& rbuf); + + inline virtual uint8_t to_mode_bits(const char ch) const override + { + return character_to_mode_bits(ch); + } + +protected: + uint8_t _type{}; + config_t _cfg{}; + +private: + bool _sym_was_pressed{false}; + bool _caps_shift_once{false}; + bool _caps_lock{false}; + bool _caps_hold_active{false}; + bool _caps_pressing{false}; + uint8_t _caps_click_count{0}; + uint32_t _caps_pressed_at{0}; + uint32_t _caps_last_release_at{0}; +}; + +} // namespace unit +} // namespace m5 +#endif diff --git a/src/unit/unit_FacesQWERTY.cpp b/src/unit/unit_FacesQWERTY.cpp index 0f1225c..cdc4106 100644 --- a/src/unit/unit_FacesQWERTY.cpp +++ b/src/unit/unit_FacesQWERTY.cpp @@ -357,7 +357,7 @@ bool UnitFacesQWERTY::update_new_firmware(const types::elapsed_time_t at) auto prev_holding = _holding; uint8_t rbuf[(NUMBER_OF_KEYS + 7) / 8 + 1]{}; - if (!readRegister(CMD_SCAN_REG, rbuf, m5::stl::size(rbuf), 0)) { + if (!readRegister(scan_reg(), rbuf, m5::stl::size(rbuf), 0)) { M5_LIB_LOGE("Failed to read"); return false; } diff --git a/src/unit/unit_Keyboard.cpp b/src/unit/unit_Keyboard.cpp index cff5c4f..fc051cf 100644 --- a/src/unit/unit_Keyboard.cpp +++ b/src/unit/unit_Keyboard.cpp @@ -36,6 +36,7 @@ void UnitKeyboard::update(const bool force) elapsed_time_t at{m5::utility::millis()}; if (force || !_latest || at >= _latest + _interval) { _updated = (readWithTransaction(&_released_key, 1) == m5::hal::error::error_t::OK) && (_released_key != 0); + // printf("released_key:0x%02x\n", _released_key); if (_updated) { _latest = at; } @@ -84,14 +85,14 @@ bool UnitKeyboardBitwise::stop_periodic_measurement() bool UnitKeyboardBitwise::readFirmwareVersion(uint8_t& ver) { ver = 0; - return readRegister8(CMD_FIRMWARE_VERSION_REG, ver, 0); + return readRegister8(firmware_version_reg(), ver, 0); } bool UnitKeyboardBitwise::readMode(Mode& mode) { mode = Mode::Conventional; uint8_t v{}; - if (readRegister8(CMD_MODE_REG, v, 0)) { + if (readRegister8(mode_reg(), v, 0)) { mode = static_cast(v); return true; } @@ -100,7 +101,7 @@ bool UnitKeyboardBitwise::readMode(Mode& mode) bool UnitKeyboardBitwise::writeMode(const Mode mode) { - if (firmwareVersion() && writeRegister8(CMD_MODE_REG, m5::stl::to_underlying(mode))) { + if (firmwareVersion() && writeRegister8(mode_reg(), m5::stl::to_underlying(mode))) { _mode = mode; _data->clear(); diff --git a/src/unit/unit_Keyboard.hpp b/src/unit/unit_Keyboard.hpp index 6e85eb4..2f788c1 100644 --- a/src/unit/unit_Keyboard.hpp +++ b/src/unit/unit_Keyboard.hpp @@ -60,6 +60,12 @@ enum class Mode : uint8_t { */ M5UnitUnified, }; + +struct register_map_t { + uint8_t scan_reg{0x10}; + uint8_t mode_reg{0x20}; + uint8_t firmware_version_reg{0xFE}; +}; } // namespace keyboard /*! @@ -547,6 +553,18 @@ class UnitKeyboardBitwise : public UnitKeyboard, public PeriodicMeasurementAdapt bool writeMode(const keyboard::Mode mode); ///@} + ///@name Register map + ///@{ + inline keyboard::register_map_t registerMap() const + { + return _register_map; + } + inline void registerMap(const keyboard::register_map_t& map) + { + _register_map = map; + } + ///@} + protected: bool start_periodic_measurement(); bool start_periodic_measurement(const uint32_t interval); @@ -565,6 +583,21 @@ class UnitKeyboardBitwise : public UnitKeyboard, public PeriodicMeasurementAdapt return 0x00; } + inline uint8_t scan_reg() const + { + return _register_map.scan_reg; + } + + inline uint8_t mode_reg() const + { + return _register_map.mode_reg; + } + + inline uint8_t firmware_version_reg() const + { + return _register_map.firmware_version_reg; + } + bool permitted_mode(const uint8_t mbits) const { uint8_t mod8 = _now >> 56; @@ -581,6 +614,7 @@ class UnitKeyboardBitwise : public UnitKeyboard, public PeriodicMeasurementAdapt uint32_t _repeating_threshold{400}, _holding_threshold{800}; uint8_t _firmware_version{}; keyboard::Mode _mode{keyboard::Mode::Conventional}; + keyboard::register_map_t _register_map{}; }; namespace keyboard { @@ -589,6 +623,7 @@ constexpr uint8_t CMD_SCAN_REG{0x10}; constexpr uint8_t CMD_MODE_REG{0x20}; constexpr uint8_t CMD_FIRMWARE_VERSION_REG{0xFE}; } // namespace command + } // namespace keyboard } // namespace unit