Commit Graph
14 Commits
Author SHA1 Message Date
Christopher Van HooseandClaude Fable 5 181240eb55 M9: record the measured compass axis mapping and bias
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 02:22:25 -04:00
Christopher Van HooseandClaude Fable 5 4852f06033 GPS Compass: bake the MEASURED M9 axis mapping; no orientation press needed
The sensor's orientation on the board is documented nowhere, so it was
measured: held flat, logging the raw vector at four headings 90 deg apart
(M9_COMPASS_DEBUG, now off again) gives a hard-iron centre of
(-0.340, -3.378) and, after subtracting it,

    N x'=-0.055 y'=+0.310    E x'=+0.268 y'=-0.018
    S x'=+0.013 y'=-0.275    W x'=-0.225 y'=-0.016

so atan2(x, y) reads 350/94/177/266 at N/E/S/W -- 0/90/180/270 within a few
degrees, counting up clockwise. +Y is the device's top edge, +X its left.
That is now the default: correct after calibration alone.

This also explains the reversal reported on hardware. The auto-handedness
rule assumed a Z-out-of-screen sensor was the un-mirrored case; it is the
other way round -- held flat north of the magnetic equator, a Z-INTO-screen
sensor reads the downward field as POSITIVE z. Fixed, and the stored
orientation is versioned so values saved against the old formula are
discarded rather than pushing a correct default back off north.

The bias is real and large: ~-3.4 G on Y against a ~0.27 G horizontal
signal, which is why an uncalibrated device barely moves the dial, and why
the range is +/-32 G rather than +/-8 G. Meshtastic's implausible hardcoded
extrema were right after all.

Also: the satellite meter now sits beside the count instead of at the column
edge, and UITask::loop's coarse "ui:gps" stall bucket is split into
timers/threads/input/diag so a 450 ms hitch can be attributed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 02:21:53 -04:00
Christopher Van HooseandClaude Fable 5 9a5cb53b70 M9: record the map re-open measurement (2598 ms cold, 245 ms re-open)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 02:09:01 -04:00
Christopher Van HooseandClaude Fable 5 cfb083e36f Lua host: keep keypad-nav focus off the app body; GPS Compass dial layout
On the ThinkNode M9 every Lua app opened on a white page: the app body is a
clickable object (touch boards need its press events) on the top layer, so
navCollect harvested it as a leaf focus target and navFocusCb's reverse-video
fill painted it solid under the app's widgets -- the canvas on top stayed
dark, which is what gave it away. NAV_SKIP_FLAG would also hide an app's own
buttons from the d-pad, so this adds NAV_PASSTHRU_FLAG (AppPage.h, shared by
both TUs): clickable, never a target itself, children still collected.

M9 compass: low-pass depth 8 at 50 Hz (the datasheet's 0x61 example) read as
sluggish on the dial; now depth 4 at 100 Hz (CTRL1 0x41, CTRL2 0x30). The
app ticks at 100 ms with lighter smoothing to match.

GPS Compass app rebuilt in the RF Monitor's look: a dial with rings,
10/30/90-degree graduations, red north, a lubber mark and the heading in
the centre; a key/value panel (FIX/LAT/LON/ALT/SPD/TGT) with a 10-cell
satellite meter; compact strings where the column is narrow at large fonts.
Confirmed on the M9 by Chris: the dial turns and calibration holds
(gpscompass.sav persists across reboots).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 01:32:57 -04:00
Christopher Van HooseandClaude Fable 5 0771e15452 Serial sideload for Lua apps: fput/fadd/fend CLI + scripts/sideload_app.py
The ThinkNode M9's microSD is soldered on, so the SDK's "drop it on the card"
route does not exist there and the Store can only fetch from its own host.
Three console commands write into the same /apps (or /lang) the Store uses:

  fput /apps/<name>                 open (truncate); names [A-Za-z0-9._-]
  fadd <off> <len> <sum> <base64>   append a chunk, every field verified
  fend                              close

Same physical-access trust level as the existing "rm"/"erase", narrower scope
(two directories). DataStore gains a root-aware mkdirRooted(); the CLI line
buffer grows from 80 to 200 bytes.

Why the chunks are self-checking and short: the UART interrupt is not
IRAM-resident, so while the loop is inside a flash-cache pause only the
128-byte hardware FIFO buffers console input and the middle of a longer line
is lost -- observed on the M9 as a 197-char line echoed back as 120. Offset,
decoded length and byte sum reject a damaged line; the host re-sends. The
host script also waits for "[BOOT] ui ready" because the CH34x bridge resets
the board whenever the port is opened and the console is not serviced before
the UI is up.

Verified on the M9: gpscompass.lua (19055 B) + .json pushed, sizes confirmed
by the device and by `ls /apps` after a reboot. M9 and V4-R8 envs compile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 01:15:56 -04:00
Christopher Van HooseandClaude Fable 5 b273f20e26 M9 compass (QMC6309) + GPS motion for Lua apps; GPS Compass Store app
Firmware
- variants/thinknode_m9/M9Compass.{h,cpp}: QMC6309 at 0x7C on the peripheral
  bus. Probe by chip id, soft reset with the explicit clear, CTRL2 0x20
  (50 Hz, +/-32 G, set/reset on), CTRL1 0x61 (normal, OSR 8/8), read-back
  verified. Synchronous read from the Lua host bridge, 1 s cache, lazy
  re-probe while the rail-powered part is still in POR, OVFL kept + flagged +
  logged. HAS_M9_COMPASS=1 in the env -> new hardware gate CAP_COMPASS.
- src/helpers/WadaNmeaLocationProvider.h: Wadamesh-owned copy of the core
  MicroNMEALocationProvider that also exposes RMC speed/course (the core
  keeps its parser private; no libdeps patch). M9 target.cpp builds on it,
  HAS_GPS_MOTION=1, wadaGpsMotion() for UITask.
- wada.sys.gps(): + alt (m), + speed_kmh/course where the board provides
  them (course only while moving -- an empty RMC course parses as 0), and
  nil while the user has GPS switched off. wada.sys.compass(): {x,y,z,ovfl}
  Gauss, sensor frame, uncalibrated, registered only where CAP_COMPASS;
  caps().compass. Calibration, axis mapping and the heading maths live in
  the app so they can be adjusted per user without a firmware cut.
- Host: luaHostContactAt reads the RTC once per contacts() walk instead of
  once per contact (an I2C transaction each on the M9); pressCb reports
  press coordinates in body content space (scroll offset folded in).

App
- deploy/apps/gpscompass/1.0 + apps.json: rotating rose with a fixed index,
  live fix readout, bearing/range to a selected contact, magnetometer
  heading with hard-iron calibration (C), frame rotate/mirror (O/F, the
  M9's sensor orientation is undocumented), GPS-course fallback on every
  other board, saturation warning. Not baked into lua_builtin.h on purpose
  (CAP_BUILTIN_LUA_APPS also removes the Store > Apps tab).

Verified: M9, V4, V4-R8, T-Deck compile (M9 flash +2 KB); two adversarial
review passes, all confirmed findings fixed; host Lua harness (vendored
Lua, LUA_32BITS) -- calibration recovers a simulated bias exactly, heading
error 0 deg, worst tick ~10k of the 100k budget. Hardware validation list
(axis orientation, bias magnitude, 0x7C ACK) in M9_PORT.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 00:53:16 -04:00
Christopher Van HooseandClaude Fable 5 16e883187c M9 + all boards: persist the companion app-sync ring across power cycles
User report: the M9 "does not remember chats or sync them to the app" while
the V4-R8 does. The companion sync ring (MyMesh::history_ring + per-client
cursors) was RAM-only on every board — invisible on a USB-powered V4 that
rarely loses power, fatal on the M9 whose only off is the hard-cut slider.

Shared fix: the ring is now an append log (/synchist) plus client cursors
(/synccur) on DataStore::getHotDataFS(), restored in MyMesh::begin() and
flushed by persistSyncHistoryNow() on the paths that lose RAM (deep sleep,
power-off, explicit save). Record + bench-test recipe: M9_PORT.md Deferred #13.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 13:50:48 -04:00
Christopher Van HooseandClaude Fable 5 e56ac117b4 M9: Spectrum brought to LR1110 spec (STDBY_XOSC between bins, span-wide image cal, TX-safe seize)
Spec-check of the sweep on the LR1110 (two analysts + adversarial
verify): per-bin standby() parked in STDBY_RC and re-paid the ~5 ms
DIO3-TCXO startup every bin (~10 ms/bin, ~2x the budget) — bins now
park in STDBY_XOSC via the raw 0x01 byte (RadioLib's STANDBY_XOSC
define is bugged identical to STANDBY_RC); one CalibImage over the
whole swept span per open (begin() only calibrates mesh_freq +/-4 MHz,
not the +/-12 span the old comment claimed), mesh cal restored exactly
on close; sweep clamps corrected to the driver-enforced 150-960 MHz on
BOTH driver families and a failed retune now skips the bin instead of
mis-attributing the previous bin's RSSI; failed-read 0 dBm sentinel no
longer wins the peak-hold (one SPI hiccup squashed the auto-scaled
trace for ~30 s); config block enters standby first; opening Spectrum
mid-TX now waits (bounded, dispatcher's own airtime budget, 6 s cap)
for the packet to finish instead of truncating it on the air; stale
SX126x-era comments refreshed, dead gain readout removed, bin-pitch
vs RBW coverage (42%) documented as the deliberate trade-off it is.

On-device verification wanted: sweep timing (micros() log), mesh RX
sensitivity after a Spectrum session, mid-TX open completing the send.
All four touch envs compile clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 09:18:27 -04:00
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
Christopher Van HooseandClaude Fable 5 68e3b45932 M9 + V4-R8: 2026-08-19 audit passes
ThinkNode M9 (see M9_PORT.md audit section): restore nav-mode HW Back,
Lua-app key forwarding + d-pad support, popup-registry gates widened,
SD-backed data store + chat history, ADC2 battery sample filter,
keyboard poll throttle + backlight, map tile cache to SD, spectrum
sweep LR1110 fix, power-menu cleanup for buttonless hardware.

Heltec V4 R8 (see R8_AUDIT.md): chat history/battery log follow SD
adoption, reinsert watch no longer unmounts live store, tile cache
prefers SD, CAP_ROTATABLE reboot trap defused, SD CS parked at boot.

Shared: atomic threads-index write (tmp+rename), recursive
scroll-into-view during keypad nav, textarea focus highlight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 07:25:18 -04:00
Enot (ded) Skelly c322b1fa3a chore: update m9 port doc 2026-07-08 09:31:05 -07:00
Enot (ded) Skelly c87cd3cc98 chore: update m9 port doc 2026-07-07 13:42:31 -07:00
Enot (ded) Skelly 38c0e7a016 correct m9 adc and add backlight handling 2026-07-06 15:45:29 -07:00
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