Files
wadamesh/variants/thinknode_m9/M9Keyboard.cpp
T
Kaj SchittecatandClaude Fable 5 a38ea1e3bb touch: Elecrow ThinkNode M9 port (ESP32-S3 + LR1110 + I2C QWERTY) — bring-up builds 1-9
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>
2026-07-06 21:49:25 +02:00

100 lines
3.5 KiB
C++

// ThinkNode M9 keyboard driver — see M9Keyboard.h.
#if defined(HAS_M9_KEYBOARD) && defined(ESP32)
#include "M9Keyboard.h"
#include <Arduino.h>
#include <Wire.h>
#ifndef PIN_KB_ADDR
#define PIN_KB_ADDR 0x6c
#endif
// Controller register map — from Elecrow's keyboard-controller firmware
// (ThinkNode-M9-KB-platformio, src/main.cpp). The keyboard is a separate
// ESP32-S2 running a matrix scanner as an I2C SLAVE with addressed registers:
// reads MUST be preceded by a 1-byte register-address write, otherwise the
// slave answers with whatever register was addressed last (0x00 = HW version
// after its reset — a constant 0x03, which is exactly why the bare-read
// protocol produced "no keys do anything").
#define M9_KB_REG_HW_VER 0x00 // constant 0x03 on this controller FW
#define M9_KB_REG_KEY 0x01 // pending key byte, 0x00 = none (single slot)
#define M9_KB_REG_BACKLIGHT 0x02 // PWM duty 0..255 (controller lights on keypress)
#define M9_KB_REG_FW_VER 0xFE // constant 0x10 on this controller FW
static volatile uint8_t s_ring[16];
static volatile uint8_t s_head = 0;
static volatile uint8_t s_tail = 0;
static bool s_inited = false;
static TwoWire* s_bus = nullptr; // set once the controller answers
static uint32_t s_last_probe_ms = 0;
static bool kbReadReg(TwoWire& w, uint8_t reg, uint8_t* out) {
w.beginTransmission((uint8_t)PIN_KB_ADDR);
w.write(reg);
if (w.endTransmission() != 0) return false; // NACK / bus error
if (w.requestFrom((int)PIN_KB_ADDR, 1) != 1) return false;
*out = (uint8_t)w.read();
return true;
}
static bool kbProbe(TwoWire& w, const char* name) {
uint8_t hw = 0, fw = 0;
if (!kbReadReg(w, M9_KB_REG_HW_VER, &hw)) return false;
kbReadReg(w, M9_KB_REG_FW_VER, &fw); // best-effort, for the log only
Serial.printf("M9 keyboard: controller on %s (hw=0x%02X fw=0x%02X)\n", name, hw, fw);
return true;
}
// The schematic reading puts the keyboard on its own bus (host 20/21); the
// shared sensor bus (Wire, 7/6) is probed as a fallback so a wrong pin
// reading shows up in the boot log instead of as a dead keyboard.
static void kbTryFindBus() {
if (kbProbe(Wire1, "Wire1 (20/21)")) s_bus = &Wire1;
else if (kbProbe(Wire, "Wire (7/6)")) s_bus = &Wire;
}
void m9KeyboardBegin() {
Wire1.begin(PIN_KB_SDA, PIN_KB_SCL);
// The controller is its own MCU on the switched peripheral rail and may
// still be booting here — poll re-probes once a second until it answers.
kbTryFindBus();
if (!s_bus) Serial.println("M9 keyboard: no controller yet (will keep probing)");
s_inited = true;
}
void m9KeyboardPoll() {
if (!s_inited) return;
if (!s_bus) {
uint32_t now = millis();
if (now - s_last_probe_ms < 1000) return;
s_last_probe_ms = now;
kbTryFindBus();
if (!s_bus) return;
}
uint8_t key = 0;
if (!kbReadReg(*s_bus, M9_KB_REG_KEY, &key)) return;
if (key == 0x00 || key == 0xFF) return; // no key / undefined-register reply
const uint8_t nh = (uint8_t)((s_head + 1) & 15);
if (nh != s_tail) { // drop if the ring is full
s_ring[s_head] = key;
s_head = nh;
}
}
int m9KeyboardReadKey() {
if (s_tail == s_head) return 0; // empty
const uint8_t key = s_ring[s_tail];
s_tail = (uint8_t)((s_tail + 1) & 15);
return key;
}
void m9KeyboardSetBacklight(uint8_t duty) {
if (!s_inited || !s_bus) return;
s_bus->beginTransmission((uint8_t)PIN_KB_ADDR);
s_bus->write(M9_KB_REG_BACKLIGHT);
s_bus->write(duty);
s_bus->endTransmission();
}
#endif