Files
wadamesh/variants/thinknode_m9/M9Keyboard.h
T
Christopher Van HooseandClaude Fable 5 1b7d793ceb M9: audit pass 2 — 29 verified fixes (nav soft-locks, gate parity, deep sleep, build hardening)
Second 7-dimension adversarially-verified audit over the 08-19 tree.
Headliners: SUB_MAP over a display-only Lua app orphaned the app page
(key-only soft-lock); Back-ladder z-order redesign (CC/power always-
frontmost, confirm-modal deference, Lua key-forward suppressed while a
confirm is up — the send-permission dialog was unanswerable); map pan
flag could go stale across tab jumps/popups; null-close progress rows
now genuinely block the registry dismiss (shared fix); terminal RX
mirror, fullscreen title, wallpaper caption, storage-error guidance
widened to M9; accent/@-mention pickers suppressed (dead chrome on a
touchless board); kb-backlight cache only latches ACKed duties (0xFF
sentinel == duty 255 skipped the first write every boot); deep sleep
actually drops the rails now (display refcount, LEDC pin re-route,
RTC/digital holds) and powers off radio+GPS; TCXO fallback no longer
codifies the disproven 0.0f; RadioLib old-FW patch fail-closes at
link; ENV_SKIP_GPS_DETECT + CORE_DEBUG_LEVEL=0 added to the env.

Full round log in M9_PORT.md 'Audit pass 2 (2026-08-20)'. All four
touch envs (M9, T-Deck, V4-R8, pager) compile clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 08:07:37 -04:00

74 lines
3.7 KiB
C

#pragma once
// ThinkNode M9 keyboard controller. Unlike the T-Deck — where the keyboard
// shares an I2C bus with the touch controller and must be polled from a
// dedicated core-0 task — the M9 keyboard lives on its OWN bus (Wire1,
// SDA=20 / SCL=21, addr 0x6c) with no touch controller present at all. So
// this can be polled directly from the UI thread; no cross-core ring buffer
// is strictly required, but one is kept anyway for interface parity with
// TDeckKeyboard and so a future move to a dedicated poll task is a no-op.
//
// PROTOCOL — from Elecrow's own keyboard-controller firmware
// (ThinkNode-M9-KB-platformio): the keyboard is a separate ESP32-S2 running a
// matrix scanner as an I2C slave with ADDRESSED REGISTERS. A key read is
// write-reg(0x01) then read 1 byte (0x00 = no key; the controller latches a
// single pending key). The earlier "one raw byte per read" protocol note was
// WRONG — a bare read returns the last-addressed register (0x00 = HW version,
// constant 0x03), which is why bring-up builds 1-7 saw no keys. The
// controller resolves shift/symbol/alt layers itself and emits final ASCII
// for printable keys; non-ASCII bytes are the d-pad / dedicated function
// keys (see M9_KEY_* below), passed through unchanged, so
// m9KeyboardReadKey() returns either a printable ASCII code or a sentinel.
// Backlight: write-reg(0x02) + duty byte; the controller auto-lights on
// keypress and times out after 10 s on its own.
#if defined(HAS_M9_KEYBOARD) && defined(ESP32)
#include <stdint.h>
// Raw byte values confirmed on hardware (Specter bring-up, notes/m9_keycodes.md).
#define M9_KEY_LEFT 0xB4
#define M9_KEY_UP 0xB5
#define M9_KEY_DOWN 0xB6
#define M9_KEY_RIGHT 0xB7
#define M9_KEY_ENTER 0x0D // shared with d-pad centre click — indistinguishable
#define M9_KEY_DEL 0x08
#define M9_KEY_MIC 0x88 // triangle/mic glyph key
#define M9_KEY_LEFT_MESSAGE 0x81
#define M9_KEY_HOME 0x82
#define M9_KEY_SUB_MESSAGE 0x83
#define M9_KEY_SUB_MAP 0x84
#define M9_KEY_MAP 0x85
#define M9_KEY_HW_BACK 0x86
#define M9_KEY_CTRL 0x90 // bound: opens the Control Center (single latched key — can never chord)
// From the controller firmware's key tables: 0x84 long-press emits 0x87
// (labeled "GPS toggle" — bound to toggleGPS()), Enter long-press emits 0xA3
// (bound: LV_EVENT_LONG_PRESSED on the focused widget / lock-screen unlock).
// Shift/Sym/Alt never reach the wire — the controller consumes them as layer
// modifiers. DELIBERATELY UNBOUND: MIC 0x88 (no audio-capture feature exists)
// and 0x89 (matrix col3/row4, physical key cap unidentified — binding an
// unknown cap invites accidental triggers).
#define M9_KEY_GPS_LONG 0x87
#define M9_KEY_ENTER_LONG 0xA3
/** Bring up Wire1 (SDA=PIN_KB_SDA, SCL=PIN_KB_SCL) and mark the keyboard
* available. Safe to call from the UI thread — this bus has no other
* device on it. */
void m9KeyboardBegin();
/** Read one key over I2C and push it into the ring. May be called from any
* single task (this board has no cross-core contention on Wire1). */
void m9KeyboardPoll();
/** Pop the next buffered raw byte (ASCII or M9_KEY_* sentinel), or 0 if none. */
int m9KeyboardReadKey();
/** Set the keyboard backlight PWM duty (0..255) on the controller. The
* controller lights up on keypress by itself and times out after 10 s;
* this just sets HOW bright. Returns false — writing nothing — until the
* controller has been found, or when the I2C write NACKs, so a caller that
* caches "last written" can retry instead of latching a duty the
* controller never took. */
bool m9KeyboardSetBacklight(uint8_t duty);
#endif