The keyboard controller latches a single key. When one was pending,
m9KeyboardPoll read the register up to three times back to back, a
"drain" meant to catch a second key, but with one slot a second read
can only return nothing or the same key again if the controller has not
cleared it yet. Keys came out twice, often and on any key.
- One read per poll, as the Launcher and Meshtastic M9 ports do.
- The same key reported again within 70 ms of the last accepted one is
dropped as the same press. That covers a stale latch read on the next
poll and the matrix bouncing on a slow, deliberate press, which the
report found worse. Nobody presses one key twice that fast on purpose.
The guard runs from the last accepted key, so bounces cannot stretch it.
Fixes#625
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
The GPS enable pin (GPIO11) is active-LOW on the original ThinkNode-M9 but
active-HIGH on the newer revision (keyboard controller at 0x6D, e.g. Elecrow's
Meshtastic SKU LMM15409D2). Driven LOW on those units the GPS never gets a
fix: on one unit, same spot, 0 satellites with EN active-LOW vs 15 with
active-HIGH, and 34 after 20 minutes outdoors with the GPS toggled off and on
again from the UI. Meshtastic's ELECROW-ThinkNode-M9 variant makes the same
split, keyed on the same keyboard address (m9Version > 1 ? HIGH : LOW), and
Elecrow engineers have confirmed the split.
WadaNmeaLocationProvider: keep the enable level in a member that defaults to
the compile-time GPS_EN_ACTIVE, add setEnActive() to override it at runtime
(re-driving the pin so a running GPS stays on), and an optional
setPostEnableHook() run at the end of begin(). Every other board is unaffected.
thinknode_m9: m9KeyboardAddr() and a one-shot found-callback on the keyboard
driver; target.cpp uses it, when the controller is at 0x6D, to call
gps.setEnActive(true) and to register a GPIO10 wake pulse (50 ms, HIGH 5 ms,
LOW, 50 ms) after enabling, as Elecrow engineers confirmed. Any other address
keeps the existing default (LOW, no pulse), so the 0x6C path is unchanged; it
is not tested here, no 0x6C unit was available.
Depends on the keyboard 0x6D probe change.
Signed-off-by: Pratik Patel <Pratik@MakerNova.io>
Three battery fixes plus the record in M9_PORT.md:
- The touchSleep battery saver was unreachable on the M9: the settings
switch and its callback were compiled only for the T-Deck and V4-R8,
and the pref defaults OFF with no other setter. Both #ifs now include
HAS_THINKNODE_M9 (the M9 is in batteryIsCharging's #else branch, so
the "USB powered" gate can never wrongly block there).
- WadaNmeaLocationProvider parked GPS_RESET asserted while the GPS was
stopped, per the core's convention. On the M9 that polarity is
inverted through R46 into NPN Q16, so "asserted" sourced base current
the entire time the GPS was OFF. Park the line LOW instead — the
zero-current level for both circuit styles; every start path still
pulses reset() explicitly, so the clean-reset guarantee holds.
- A mid-session GPS enable ran the whole session on the stock 256 B
Serial1 RX ring (the 4 KB TTFF ring is boot-gated on the persisted
pref, and setRxBufferSize is a no-op on a running UART) — and with
the saver newly reachable, one 50 ms park overflows that ring at the
M9's 115200 baud. gpsEnsureBigRxRing() cycles the UART to 4 KB from
both "gps on" funnels (toggleGPS, CMD_SET_CUSTOM_VAR) right after a
successful start, so a refused enable never spends the DRAM.
Also corrects both stale "keyboard S2 on the switched rail" comments
(schematic truth: always-on 3V3 via R3 — it decides the deep-sleep
drain budget). All 8 envs compile; on-device VERIFY list in M9_PORT.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MYDmpc53KpdmTLKzUNsX2s
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>
New board: ThinkNode_M9_companion_radio_touch env, variants/thinknode_m9/
(board glue, LR1110 target, keyboard driver, partitions, M9_PORT.md pin
reference), boards/thinknode_m9.json, device_caps HAS_THINKNODE_M9 block,
HAS_M9_KEYBOARD input path in the touch UI (d-pad focus nav, tab jump keys,
QWERTY typing).
Based on a contributed bring-up patch by ded (SalishMesh), who also remote-
tested every build on a preproduction unit. Hard-won fixes on top of the
patch, each verified on hardware:
- LR1110 radio init -706: preprod chips run transceiver FW 0x0303, which
rejects DriveDiosInSleepMode (0x012A, added in FW 0x0308) that RadioLib
7.x sends unconditionally during begin(). New build-time patch
scripts/build/patch_radiolib_lr11x0.py tolerates it on old FW;
radio_init() prints the chip's device/FW/errors either way.
- TCXO: DIO3 at 3.3 V (the patch's active-oscillator/tcxo=0 theory gave
-707 on hardware).
- Display: rotation 1 (not 3), plus the missing landscape s_ui_rotation
override so LVGL renders 320x240 instead of portrait-into-landscape.
- Keyboard: the controller is a separate ESP32-S2 I2C slave (0x6C) with
addressed registers - reads select register 0x01 first (bare reads
return the HW-version register forever). Boot probe logs the
controller's HW/FW; backlight duty register wired.
- USB pad release: the keyboard bus lives on GPIO20/21 = the S3's native
USB D-/D+ pads, owned from reset by the ROM USB-Serial-JTAG (D+ pullup
on SDA). M9Board::begin() releases them before any bus init.
- GPIO35-37 are octal-PSRAM pins on the S3R8 - never drive them (the
patch's "SD CS = 36" misread package pin 36 = GPIO48; SD is on the
shared SPI bus, support returns with PIN_SD_CS=48 later).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>