From 16041b0f3af7a76a35d1722f17ea91581dd3f22b Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:55:51 +0200 Subject: [PATCH] M9 keys P1 --- releasenotes/RELEASE_NOTES_1.16.6-zephcore.md | 9 +- zephcore/CMakeLists.txt | 6 + zephcore/Kconfig | 5 +- .../esp32/thinknode_m9/Kconfig.thinknode_m9 | 4 + zephcore/boards/esp32/thinknode_m9/README.md | 37 ++- zephcore/boards/esp32/thinknode_m9/board.conf | 4 + .../boards/esp32/thinknode_m9/board.overlay | 14 ++ .../thinknode_m9/thinknode_m9-pinctrl.dtsi | 12 + .../thinknode_m9_esp32s3_procpu.dts | 14 ++ .../bindings/input/zephcore,stc8h-keypad.yaml | 47 ++++ zephcore/helpers/input/stc8h_keypad.c | 232 ++++++++++++++++++ .../helpers/ui-joystick/joystick_ui_task.cpp | 17 ++ 12 files changed, 386 insertions(+), 15 deletions(-) create mode 100644 zephcore/dts/bindings/input/zephcore,stc8h-keypad.yaml create mode 100644 zephcore/helpers/input/stc8h_keypad.c diff --git a/releasenotes/RELEASE_NOTES_1.16.6-zephcore.md b/releasenotes/RELEASE_NOTES_1.16.6-zephcore.md index 39c9f5e..288ccb2 100644 --- a/releasenotes/RELEASE_NOTES_1.16.6-zephcore.md +++ b/releasenotes/RELEASE_NOTES_1.16.6-zephcore.md @@ -115,7 +115,14 @@ It is ZephCore's first ESP32-S3 + LR1110 board, so a fair amount of it is new gr an existing port. Currently unverified: display init parameters (inherited from another ST7789V board — if the screen is mirrored or upside down, that's a known one-line fix), GPS output from the CC1167Q module, battery calibration, and whether the USB-C port reaches the native USB pads at all (BLE is the companion -path meanwhile). Not ported: the I2C keyboard, which is driven by a second MCU. +path meanwhile). + +The M9 has no user button in hardware — its keypad is the only input — so it now runs the full companion UI +driven by a new driver for the STC8H keypad MCU on I2C. Enter, back, home, menu and page prev/next are wired +up, and typed characters pass straight through. **The arrow keys are not mapped yet**: no reference we could +find documents their codes, so rather than guess, the driver logs any key it doesn't recognise along with its +hex value. Press the arrows once with a serial console attached and they can be bound in a one-line change. +Until then you can move between pages but not up and down within a list. Also worth knowing on this board specifically: the **SD slot shares the radio's SPI bus** with the LR1110 and the display. An inserted card was confirmed to break LR1110 radio-firmware flashing on this board — the diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index 6d8f647..109c1ca 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -530,6 +530,12 @@ else() message(STATUS "ZephCore Battery: generic LiPo curve") endif() +# ========== I2C keypad (boards whose only input is a keyboard) ========== +if(CONFIG_INPUT) + target_sources_ifdef(CONFIG_ZEPHCORE_UI_DISPLAY app PRIVATE + helpers/input/stc8h_keypad.c) +endif() + # ========== Board-specific startup quirks ========== # Optional per-board sd_quiesce.c — boards whose SD slot shares the radio's SPI # bus put any inserted card into SPI idle before the radio driver initialises, diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 6cfccb9..2c83be9 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -859,8 +859,9 @@ config ZEPHCORE_UI_KEYBOARD separate one. Selecting this makes ZEPHCORE_UI_DESIGN_JOYSTICK available on a board that has no 5-way stick. - No board selects this yet: the STC8H driver and the keyboard text - entry screen are not written, and the M9 is unverified on hardware. + Selected by ThinkNode M9. The STC8H driver is helpers/input/ + stc8h_keypad.c; the keypad's arrow-key codes are still unknown, so + the driver logs any code it does not recognise (see its header). The key-space reservation in helpers/ui-joystick/joystick_defs.h is the groundwork that makes them droppable in later. diff --git a/zephcore/boards/esp32/thinknode_m9/Kconfig.thinknode_m9 b/zephcore/boards/esp32/thinknode_m9/Kconfig.thinknode_m9 index aad86ff..daf1777 100644 --- a/zephcore/boards/esp32/thinknode_m9/Kconfig.thinknode_m9 +++ b/zephcore/boards/esp32/thinknode_m9/Kconfig.thinknode_m9 @@ -1,3 +1,7 @@ config BOARD_THINKNODE_M9 select SOC_ESP32S3_WROOM_N16R8 select SOC_ESP32S3_PROCPU if BOARD_THINKNODE_M9_ESP32S3_PROCPU + # STC8H keypad on I2C1 @0x6C is this board's ONLY input — there is no + # user button in hardware. Selects the full companion UI, whose key + # space is built for a keyboard. + select ZEPHCORE_UI_KEYBOARD diff --git a/zephcore/boards/esp32/thinknode_m9/README.md b/zephcore/boards/esp32/thinknode_m9/README.md index 6e61a71..5af423a 100644 --- a/zephcore/boards/esp32/thinknode_m9/README.md +++ b/zephcore/boards/esp32/thinknode_m9/README.md @@ -1,8 +1,8 @@ # Elecrow ThinkNode M9 ESP32-S3 handheld with LR1110 radio, ST7789 320x240 TFT, CC1167Q GPS, PCF8563 -RTC, buzzer, STC8H I2C keyboard, QMI8658 IMU + QMC6309 magnetometer, and an SD -slot (keyboard/IMU/SD not ported — see below). +RTC, buzzer, STC8H I2C keypad, QMI8658 IMU + QMC6309 magnetometer, and an SD +slot (IMU/SD not ported — see below). Ported 2026-07-18 from Arduino MeshCore `variants/thinknode_m9` (which itself landed upstream only days earlier), cross-checked against a second, @@ -90,10 +90,18 @@ in high flash are untouched by the updater flash. LR1110 updater). - **Charge/power sense** — GPIO1 = external-power detect, **active LOW**; GPIO8 = charger DONE. Not consumed by ZephCore yet. -- **Keyboard identified** — STC8H helper MCU at **I2C 0x6C** on the second - bus (SDA=20 SCL=21), `KB_INT` GPIO12 idle-low/rising-edge, backlight - GPIO46. Registers: 0x01 battery, 0x03 long-press ms (16-bit write), 0x05 - matrix key, 0x06 state. +- **Keypad PORTED** — STC8H helper MCU at **I2C 0x6C** on i2c1 (SDA=20 + SCL=21), `KB_INT` GPIO12 idle-low/rising-edge, backlight GPIO46. + Registers: 0x01-0x04 battery mV little-endian, 0x05 pressed key, 0x06 + state (write 0x01 = sleep). Driver: `helpers/input/stc8h_keypad.c`, + binding `zephcore,stc8h-keypad`. This is the board's ONLY input, so + `Kconfig.thinknode_m9` selects `ZEPHCORE_UI_KEYBOARD` -> the full + companion UI. **Arrow-key codes are still unknown** — no reference + documents them; the driver logs unrecognised codes at INFO so they can be + identified on hardware and added to `key_map[]`. Mapped so far: 0x0D + enter, 0x86 back, 0x82 home, 0x83 menu, 0x81/0x85 function keys -> + page prev/next, 0x87 and 0xA3 long-press variants. Printable ASCII + (0x20-0x7E) passes through untranslated. - **Also on the sensor bus** — QMI8658 IMU + QMC6309 magnetometer. Unported; no address collision with the PCF8563 @0x51. @@ -142,21 +150,26 @@ firmware-sourced (both firmwares agree on all of them). Still unverified on hardware, in rough priority order: -1. **Display init params** — gamma/porch/vcom inherited from Heltec T114's +1. **Keypad arrow codes** — the one blocker for a usable UI. Attach a serial + console, press each arrow key, and read the `unmapped keypad code 0x..` + lines; add them to `key_map[]` in `helpers/input/stc8h_keypad.c` as + `INPUT_KEY_UP/DOWN/LEFT/RIGHT`. Until then the UI changes pages but + cannot move within a list. Confirm `keypad at 0x6C ready` appears at boot. +2. **Display init params** — gamma/porch/vcom inherited from Heltec T114's ST7789V (no board-specific init values exist in any reference); check contrast/colors. If mirrored or upside down, swap `mdac` 0xA0 ↔ 0x60. -2. **GPS identity/output** — confirm NMEA at 115200 and that the CC1167Q +3. **GPS identity/output** — confirm NMEA at 115200 and that the CC1167Q talks standard sentences (it should; both upstreams parse plain NMEA). If a fix never arrives, probe `$PDTINFO` manually. -3. **Battery calibration** — the 2:1 divider is what both upstreams assume; +4. **Battery calibration** — the 2:1 divider is what both upstreams assume; sanity-check `get bat` against a multimeter once, and check whether the 4.35 V charger variant actually charges above 4.20 V (OCV curve top). -4. **USB-OTG companion transport** — see the schematic section above: +5. **USB-OTG companion transport** — see the schematic section above: verify whether the native USB pads reach the USB-C connector at all. -5. **rx-boosted** — deliberately ON (both upstreams leave boosted gain off / +6. **rx-boosted** — deliberately ON (both upstreams leave boosted gain off / unset; every other ZephCore LR1110 board uses it). Verify RX sensitivity and TX power on air. -6. **ADC2 vs WiFi** — battery reads ride ADC unit 2 (GPIO13), which WiFi also +7. **ADC2 vs WiFi** — battery reads ride ADC unit 2 (GPIO13), which WiFi also uses; expect intermittent read failures in `wifi_ota.conf` builds. ## Not ported diff --git a/zephcore/boards/esp32/thinknode_m9/board.conf b/zephcore/boards/esp32/thinknode_m9/board.conf index 757a437..9a8c996 100644 --- a/zephcore/boards/esp32/thinknode_m9/board.conf +++ b/zephcore/boards/esp32/thinknode_m9/board.conf @@ -20,6 +20,10 @@ CONFIG_ZEPHCORE_RADIO_LR1110=y # Flash size — 16MB (override Zephyr default) CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +# The full companion UI is selected via ZEPHCORE_UI_KEYBOARD in +# Kconfig.thinknode_m9 (the STC8H keypad is this board's only input), so no +# UI_DESIGN override belongs here. + # PSRAM mode — WROOM-1-N16R8 is OPI octal. # Enable is auto-detected from DTS by Kconfig.psram; only the mode is set here. CONFIG_SPIRAM_MODE_OCT=y diff --git a/zephcore/boards/esp32/thinknode_m9/board.overlay b/zephcore/boards/esp32/thinknode_m9/board.overlay index d315ab5..8076254 100644 --- a/zephcore/boards/esp32/thinknode_m9/board.overlay +++ b/zephcore/boards/esp32/thinknode_m9/board.overlay @@ -210,6 +210,20 @@ }; }; +/* + * STC8H keypad helper MCU — this board's only input device (no user button + * exists at hardware level). KB_INT on GPIO12 is idle-low and pulses high on + * a keypress; the driver also polls as a safety net. + */ +&i2c1 { + keypad: keypad@6c { + compatible = "zephcore,stc8h-keypad"; + reg = <0x6c>; + irq-gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>; + poll-interval-ms = <40>; + }; +}; + &i2c0 { #include "../../common/sensors-i2c.dtsi" /* ThinkNode M9 has a battery-backed PCF8563 @ 0x51 — opt in to boot-time diff --git a/zephcore/boards/esp32/thinknode_m9/thinknode_m9-pinctrl.dtsi b/zephcore/boards/esp32/thinknode_m9/thinknode_m9-pinctrl.dtsi index 1e4f770..201790d 100644 --- a/zephcore/boards/esp32/thinknode_m9/thinknode_m9-pinctrl.dtsi +++ b/zephcore/boards/esp32/thinknode_m9/thinknode_m9-pinctrl.dtsi @@ -39,6 +39,18 @@ }; }; + /* I2C1 — STC8H keypad helper MCU @0x6C. Separate bus from the RTC and + * sensors; the keypad is this board's only input device. */ + i2c1_default: i2c1_default { + group1 { + pinmux = , + ; + bias-pull-up; + drive-open-drain; + output-high; + }; + }; + /* UART1 — L76K GPS (GPIO2 = GPS TX → CPU RX, GPIO3 = GPS RX → CPU TX) */ uart1_default: uart1_default { group1 { diff --git a/zephcore/boards/esp32/thinknode_m9/thinknode_m9_esp32s3_procpu.dts b/zephcore/boards/esp32/thinknode_m9/thinknode_m9_esp32s3_procpu.dts index 45f53ff..c198984 100644 --- a/zephcore/boards/esp32/thinknode_m9/thinknode_m9_esp32s3_procpu.dts +++ b/zephcore/boards/esp32/thinknode_m9/thinknode_m9_esp32s3_procpu.dts @@ -67,6 +67,20 @@ pinctrl-names = "default"; }; +/* + * I2C1 — STC8H keypad helper MCU @0x6C (GPIO20 SDA / GPIO21 SCL). + * + * This board has NO user button at hardware level, so the keypad is its only + * input device: without it the UI renders but cannot be operated. The keypad + * node itself lives in board.overlay alongside the rest of the app hookup. + */ +&i2c1 { + status = "okay"; + clock-frequency = ; + pinctrl-0 = <&i2c1_default>; + pinctrl-names = "default"; +}; + &spi2 { status = "okay"; #address-cells = <1>; diff --git a/zephcore/dts/bindings/input/zephcore,stc8h-keypad.yaml b/zephcore/dts/bindings/input/zephcore,stc8h-keypad.yaml new file mode 100644 index 0000000..0026193 --- /dev/null +++ b/zephcore/dts/bindings/input/zephcore,stc8h-keypad.yaml @@ -0,0 +1,47 @@ +# Copyright 2026 ZephCore +# SPDX-License-Identifier: MIT + +description: | + STC8H companion-MCU keypad (I2C). + + A small helper MCU scans the key matrix and presents one register interface + over I2C. Used by the Elecrow ThinkNode M9, whose keypad is its only input + device — there is no user button on that board. + + Protocol: + reg 0x01..0x04 battery millivolts, little-endian + reg 0x05 code of the key currently pressed (0 = none) + reg 0x06 state; writing 0x01 requests sleep + + The MCU raises its interrupt line (idle-low, rising edge) when a key is + pressed. The driver reads register 0x05 on that edge and reports the result + through Zephyr's input subsystem. If no interrupt GPIO is supplied the + driver falls back to polling. + + Example: + &i2c1 { + keypad: keypad@6c { + compatible = "zephcore,stc8h-keypad"; + reg = <0x6c>; + irq-gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>; + poll-interval-ms = <40>; + }; + }; + +compatible: "zephcore,stc8h-keypad" + +include: [i2c-device.yaml] + +properties: + irq-gpios: + type: phandle-array + description: | + Key-press interrupt from the helper MCU. Idle-low, rising edge on press. + Optional — the driver polls when it is absent. + + poll-interval-ms: + type: int + default: 40 + description: | + Poll period. Used as the sole scan mechanism when irq-gpios is absent, + and as a safety net for a missed edge when it is present. diff --git a/zephcore/helpers/input/stc8h_keypad.c b/zephcore/helpers/input/stc8h_keypad.c new file mode 100644 index 0000000..43119bf --- /dev/null +++ b/zephcore/helpers/input/stc8h_keypad.c @@ -0,0 +1,232 @@ +/* + * ZephCore — STC8H companion-MCU keypad driver + * Copyright (c) 2026 ZephCore + * SPDX-License-Identifier: MIT + * + * Reports keypresses from an I2C helper MCU through Zephyr's input subsystem. + * Written against the register interface documented in the binding; no vendor + * driver code is reused. + * + * Board context (ThinkNode M9): the keypad is the ONLY input device — that + * board has no user button at hardware level, confirmed against both upstream + * firmwares and the V1.0 schematic. Without this driver the UI renders but + * cannot be operated at all. + * + * Key codes emitted + * ----------------- + * Printable ASCII (0x20-0x7E) is forwarded verbatim as INPUT_KEY_* would not + * express it; the joystick UI reserves exactly that range for typed characters + * (see helpers/ui-joystick/joystick_defs.h), so a character can travel to the + * UI untranslated. Everything else is a named key and is translated here. + * + * The named codes below are the ones confirmed from two independent firmwares + * for this keypad. Arrow keys are NOT among them, and no reachable reference + * documents them — so any code this driver does not recognise is reported at + * INFO level with its hex value. Press the arrow keys once on real hardware + * and the log names them; add them to key_map[] and navigation is complete. + * That is deliberate: guessing arrow codes would produce a driver that looks + * finished and silently does the wrong thing. + */ + +#define DT_DRV_COMPAT zephcore_stc8h_keypad + +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(stc8h_keypad, CONFIG_ZEPHCORE_LOG_LEVEL); + +/* Register map — see dts/bindings/input/zephcore,stc8h-keypad.yaml */ +#define STC8H_REG_BATTERY 0x01 /* 0x01..0x04, little-endian millivolts */ +#define STC8H_REG_KEY 0x05 +#define STC8H_REG_STATE 0x06 +#define STC8H_STATE_SLEEP 0x01 + +#define STC8H_KEY_NONE 0x00 + +struct stc8h_config { + struct i2c_dt_spec i2c; + struct gpio_dt_spec irq; + uint32_t poll_interval_ms; +}; + +struct stc8h_data { + const struct device *dev; + struct gpio_callback irq_cb; + struct k_work_delayable work; + uint8_t last_key; +}; + +/* + * Named keys, as reported by the helper MCU. + * + * Only codes corroborated by two independent firmwares for this keypad are + * listed. The two function keys are mapped to page navigation because on a + * board with no other input they are the only way to move between screens; + * the UI treats them as prev/next. + */ +static const struct { + uint8_t raw; + uint16_t code; +} key_map[] = { + { 0x0D, INPUT_KEY_ENTER }, /* enter / confirm */ + { 0x86, INPUT_KEY_ESC }, /* back */ + { 0x82, INPUT_KEY_HOME }, /* home */ + { 0x83, INPUT_KEY_MENU }, /* context menu */ + { 0x81, INPUT_KEY_PAGEUP }, /* left function key -> previous page */ + { 0x85, INPUT_KEY_PAGEDOWN },/* right function key -> next page */ + { 0x87, INPUT_KEY_F1 }, /* long-press map pin */ + { 0xA3, INPUT_KEY_F2 }, /* long-press enter */ +}; + +static int stc8h_read_reg(const struct stc8h_config *cfg, uint8_t reg, uint8_t *val) +{ + return i2c_write_read_dt(&cfg->i2c, ®, 1, val, 1); +} + +/* Battery millivolts as measured by the helper MCU. Not consumed yet — the + * board reads its own cell through the host ADC — but the registers exist and + * a second opinion is cheap to expose. */ +int stc8h_keypad_battery_mv(const struct device *dev, uint16_t *mv) +{ + const struct stc8h_config *cfg = dev->config; + uint8_t reg = STC8H_REG_BATTERY; + uint8_t buf[4]; + int ret; + + if (!mv) { + return -EINVAL; + } + ret = i2c_write_read_dt(&cfg->i2c, ®, 1, buf, sizeof(buf)); + if (ret < 0) { + return ret; + } + *mv = (uint16_t)(buf[0] | ((uint16_t)buf[1] << 8)); + return 0; +} + +int stc8h_keypad_sleep(const struct device *dev) +{ + const struct stc8h_config *cfg = dev->config; + const uint8_t cmd[2] = { STC8H_REG_STATE, STC8H_STATE_SLEEP }; + + return i2c_write_dt(&cfg->i2c, cmd, sizeof(cmd)); +} + +static void stc8h_report(const struct device *dev, uint8_t raw) +{ + /* Printable characters travel to the UI as themselves. */ + if (raw >= 0x20 && raw <= 0x7E) { + input_report_key(dev, raw, 1, false, K_NO_WAIT); + input_report_key(dev, raw, 0, true, K_NO_WAIT); + return; + } + + for (size_t i = 0; i < ARRAY_SIZE(key_map); i++) { + if (key_map[i].raw == raw) { + input_report_key(dev, key_map[i].code, 1, false, K_NO_WAIT); + input_report_key(dev, key_map[i].code, 0, true, K_NO_WAIT); + return; + } + } + + /* Deliberately loud: this is how the missing arrow codes get identified. + * See the file header. */ + LOG_INF("unmapped keypad code 0x%02X — add it to key_map[] to bind it", raw); +} + +static void stc8h_work_handler(struct k_work *work) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + struct stc8h_data *data = CONTAINER_OF(dwork, struct stc8h_data, work); + const struct device *dev = data->dev; + const struct stc8h_config *cfg = dev->config; + uint8_t key = STC8H_KEY_NONE; + + if (stc8h_read_reg(cfg, STC8H_REG_KEY, &key) == 0) { + /* Report on the transition only — the register reads the key + * that is *currently down*, so a held key would otherwise + * repeat at the poll rate. */ + if (key != STC8H_KEY_NONE && key != data->last_key) { + stc8h_report(dev, key); + } + data->last_key = key; + } + + k_work_reschedule(&data->work, K_MSEC(cfg->poll_interval_ms)); +} + +static void stc8h_irq_handler(const struct device *port, struct gpio_callback *cb, + uint32_t pins) +{ + struct stc8h_data *data = CONTAINER_OF(cb, struct stc8h_data, irq_cb); + + ARG_UNUSED(port); + ARG_UNUSED(pins); + + /* I2C cannot be touched from an ISR — hand off to the work queue. */ + k_work_reschedule(&data->work, K_NO_WAIT); +} + +static int stc8h_init(const struct device *dev) +{ + const struct stc8h_config *cfg = dev->config; + struct stc8h_data *data = dev->data; + uint8_t probe; + + data->dev = dev; + data->last_key = STC8H_KEY_NONE; + + if (!i2c_is_ready_dt(&cfg->i2c)) { + LOG_ERR("I2C bus not ready"); + return -ENODEV; + } + + /* A keypad that does not answer is not fatal — the node still boots, + * it just has no input. Say so plainly rather than failing init, which + * on this board would look like a dead device for a missing keypad. */ + if (stc8h_read_reg(cfg, STC8H_REG_KEY, &probe) < 0) { + LOG_WRN("keypad not responding at 0x%02X — no input available", + cfg->i2c.addr); + return 0; + } + + k_work_init_delayable(&data->work, stc8h_work_handler); + + if (cfg->irq.port != NULL) { + if (!gpio_is_ready_dt(&cfg->irq)) { + LOG_ERR("IRQ GPIO not ready"); + return -ENODEV; + } + gpio_pin_configure_dt(&cfg->irq, GPIO_INPUT); + gpio_init_callback(&data->irq_cb, stc8h_irq_handler, + BIT(cfg->irq.pin)); + gpio_add_callback(cfg->irq.port, &data->irq_cb); + gpio_pin_interrupt_configure_dt(&cfg->irq, GPIO_INT_EDGE_TO_ACTIVE); + } + + /* Poll regardless: it is the only mechanism without an IRQ line, and + * the safety net for a missed edge with one. */ + k_work_reschedule(&data->work, K_MSEC(cfg->poll_interval_ms)); + + LOG_INF("keypad at 0x%02X ready (%s)", cfg->i2c.addr, + cfg->irq.port ? "interrupt + poll" : "poll only"); + return 0; +} + +#define STC8H_INIT(n) \ + static const struct stc8h_config stc8h_cfg_##n = { \ + .i2c = I2C_DT_SPEC_INST_GET(n), \ + .irq = GPIO_DT_SPEC_INST_GET_OR(n, irq_gpios, { 0 }), \ + .poll_interval_ms = DT_INST_PROP(n, poll_interval_ms), \ + }; \ + static struct stc8h_data stc8h_data_##n; \ + DEVICE_DT_INST_DEFINE(n, stc8h_init, NULL, &stc8h_data_##n, \ + &stc8h_cfg_##n, POST_KERNEL, \ + CONFIG_INPUT_INIT_PRIORITY, NULL); + +DT_INST_FOREACH_STATUS_OKAY(STC8H_INIT) diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp index 76e47be..cc3aae0 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp @@ -439,6 +439,17 @@ static void joystick_ui_input_cb(struct input_event *evt, void *user_data) /* All other keys: fire on press, ignore release */ if (!evt->value) return; + /* A keypad reports typed characters as their own ASCII code (see the + * key-space contract in joystick_defs.h: 0x20-0x7E is reserved for + * exactly this). Pass them through untranslated — the control codes + * below all live outside that range, so there is no ambiguity. */ + if (evt->code >= 0x20 && evt->code <= 0x7E) { + if (joystick_queue_initialized) { + JoystickUITask::enqueueKey((char)evt->code); + } + return; + } + char key = 0; switch (evt->code) { case INPUT_KEY_LEFT: key = KEY_LEFT; break; @@ -446,6 +457,12 @@ static void joystick_ui_input_cb(struct input_event *evt, void *user_data) case INPUT_KEY_BACK: case INPUT_KEY_ESC: key = KEY_CANCEL; break; case INPUT_KEY_1: key = KEY_CANCEL; break; + /* Keypad boards (ThinkNode M9) — the only input those have */ + case INPUT_KEY_HOME: key = KEY_HOME; break; + case INPUT_KEY_MENU: key = KEY_SELECT; break; + case INPUT_KEY_PAGEUP: key = KEY_PREV; break; + case INPUT_KEY_PAGEDOWN:key = KEY_NEXT; break; + case INPUT_KEY_F2: key = KEY_ENTER_LONG; break; /* Multi tap outputs from input_multi_tap filter */ case INPUT_KEY_D: key = KEY_BUZZ_TOGGLE; break; /* 3 taps */ case INPUT_KEY_C: key = KEY_GPS_TOGGLE; break; /* 4 taps */