Commit Graph
86 Commits
Author SHA1 Message Date
Michael A. Cojocari cfb09edcf1 docs: record T-Deck Pro test status 2026-09-08 16:57:00 -04:00
Michael A. Cojocari 9715cac742 Work 2026-09-08 16:31:52 -04:00
Michael A. Cojocari f3136ea951 Merge branch 'main' into feat/tdeck-pro-target
# Conflicts:
#	deploy/apps/lang/bg.lang
#	deploy/apps/lang/de.lang
#	deploy/apps/lang/el.lang
#	deploy/apps/lang/es.lang
#	deploy/apps/lang/fr.lang
#	deploy/apps/lang/hu.lang
#	deploy/apps/lang/it.lang
#	deploy/apps/lang/nl.lang
#	deploy/apps/lang/pt-br.lang
#	deploy/apps/lang/ro.lang
#	deploy/apps/lang/ru.lang
#	deploy/apps/lang/sr.lang
#	deploy/apps/lang/uk.lang
#	src/ui-touch/UITask.cpp
2026-09-08 15:41:45 -04:00
Michael A. Cojocari 28fb57ef4c Fix Attaky usability issues (#423) 2026-09-06 16:55:41 -04:00
Michael A. Cojocari d4ed6df92e Work 2026-09-05 15:16:56 -04:00
Michael A. Cojocari 3749df2907 Work 2026-09-05 12:23:54 -04:00
Michael A. Cojocari 420e13cafb Improve M9 message actions and unlock gesture (#402)
Open the selected message action menu with a normal Enter press. Replace the lock-screen long wait with a d-pad-center double press while preserving directional behavior and the controller long-press fallback.

Signed-off-by: Michael A. Cojocari <michael.cojocari@gmail.com>
2026-09-04 14:00:17 -04:00
Kaj SchittecatandClaude Opus 5 17ce1ae018 DS3231 real-time clock support on the T-Deck (#378)
jrote1 is fitting a battery-backed DS3231 to the T-Deck's I2C bus so the device
keeps time across a full power-off without needing GPS or Wi-Fi. The RTC adapter
already had the hard parts, refusing an untrustworthy read and revalidating every
write, so this teaches it a third chip rather than adding a second path.

Three things differ from the soldered PCF parts and all three are handled by
normalising into the existing decoder rather than duplicating its rules:

- It lives at 0x68 rather than 0x51. That is also what makes it safe to add: it
  cannot be mistaken for either chip an existing board carries.
- Day-of-week and day-of-month are swapped in its calendar block.
- It reports "my time is not trustworthy" as OSF in a status register instead of
  in bit 7 of seconds, so that bit is folded into the one the decoder already
  refuses on. A module whose backup cell went flat is therefore rejected the
  same way a powered-off M9 is, rather than having its frozen calendar believed.

0x68 is a busy address, so identification requires a positive DS3231 trait
rather than an ACK: its temperature register pair, whose low byte is a quarter
degree in the top two bits and zero elsewhere, with a high byte in a range a
working chip could be at. A module that has never been set by us can also arrive
in 12-hour mode, which is converted rather than decoded as if it were 24-hour.

Transparent when no module is fitted, which is nearly every T-Deck: reads come
from the software clock, writes stop there, and the probe logs that nothing
answered. CAP_HARDWARE_RTC stays 0 for the board, since this is an aftermarket
addition rather than something a T-Deck has, so the cold-boot network time sync
stays on offer for everyone else.

Unverified on hardware: neither of us has the module wired yet. The host tests
for the shared decoder still pass.

Built on all nine S3 envs and both ESP32-P4 targets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 01:24:59 +02:00
Kaj SchittecatandClaude Opus 5 e374031056 Merge PR #391: Seeed Wio Tracker L2 support
oumike. ESP32-S3, 16 MB flash, 8 MB octal PSRAM, quad-SPI 320x240 panel via
LovyanGFX, GT911 touch, SX1262 at 22 dBm, GPS, microSD on SD_MMC. New board
file, new variant, one new env. Seeed start selling it next week and the
maintainers have the hardware; we do not.

Merged on their hardware bring-up rather than ours: display, touch, GPS,
microSD, battery and buttons are confirmed on-device by the author. What this
side can prove is that it does not cost anyone else anything, so the whole
matrix was built: all nine S3 envs including the new one, plus both ESP32-P4
targets.

The PR also carries accumulated touch-UI work from the author's development
line, which he flagged himself and offered to split out. Merged as-is because
the three-way merge came through with zero conflicts and nothing from today was
reverted: the prefs schema is still v54, and kb_force_legacy, boot_wifi_open,
the P4 gpsEnsureBigRxRing fallback and the chat-store migration logging are all
still present. Verified explicitly rather than assumed, since the diff against
main showed 1706 deletions purely because the branch predates today's merges.

Two bring-up findings worth keeping: Arduino's Serial1.setPins() takes (rx, tx),
so PIN_GPS_TX is the pin we receive on -- the same naming trap the V4-R8 and
Pager envs already carry, and it left this board's GPS listening on a silent
pin. And the panel is fixed landscape (CAP_ROTATABLE 0), so the keyboard's
rotate arrows are not built there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 23:55:30 +02:00
Kaj Schittecat 3a23d65b88 Merge PR #386 2026-09-03 23:08:48 +02:00
Michael A. Cojocari 19d557e70a Work 2026-09-03 12:52:03 -04:00
Michael A. Cojocari 092528a8fb Fix RTC retention and cold-boot time sync (#383) 2026-09-02 21:37:35 -04:00
Michael A. Cojocari 7cf9621fec Merge remote-tracking branch 'upstream/main' into square 2026-09-02 21:08:41 -04:00
Christopher Van HooseandClaude Fable 5 d59ce2c634 M9 battery pass: reachable saver, GPS-off leakage, mid-session UART ring
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
2026-09-02 15:32:27 -04:00
Kaj Schittecat 8f9e9993c8 Merge PR #370 2026-09-01 10:49:15 +02:00
Michael A. Cojocari f70a491480 Work 2026-08-30 14:42:46 -04:00
Michael A. Cojocari de778ff09d Working on fantastical square. 2026-08-29 14:42:49 -04:00
Christopher Van Hoose 8fbbac3b66 M9_PORT.md: record audit pass 3
Cold map open, Back navigation, the SD map-pack deletion, the label overlaps
and the measurements behind them - including the correction to the earlier
"the remaining cold-open cost is the SJPG decode" note, which was reasoning
about the two smaller halves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Christopher Van Hoose <cvhvisuals@icloud.com>
(cherry picked from commit 68fe4dbc09f450cc1e1545573cf9c53658bcf840)
2026-08-28 20:37:17 -04:00
Michael A. Cojocari 107367bf82 Merge remote-tracking branch 'upstream/main' into attaky 2026-08-27 20:41:39 -04:00
Michael A. Cojocari fe6741b26d Work 2026-08-27 20:37:59 -04:00
Michael A. Cojocari 5893c44e98 Work 2026-08-27 20:01:12 -04:00
Michael A. Cojocari ed29da087a Work 2026-08-27 13:53:55 -04:00
alphalord336 30bbefc066 Merge branch 'ALLFATHER-BV:main' into main 2026-08-27 18:17:41 +02:00
alphalord336 3dee9d62e7 Update battery voltage calculation formula 2026-08-24 19:49:47 +02:00
Michael A. Cojocari acdcc6e7fc Work 2026-08-23 20:32:26 -04:00
Christopher Van HooseandClaude Fable 5 a650d7874e M9: quieten the bring-up logging, and record the measured IMU frame
Both sensors' 1 Hz debug lines were still on from the axis measurements; off
now (the flags stay, they are how the next board gets measured). Documents the
QMI8658 in M9_PORT.md -- the three attitudes and what each proved, the ADDR_AI
trap, and why tilt compensation was needed at all -- and corrects the
calibration instruction from "tumble it" to "rotate it in one place", which is
the distinction that was corrupting the fit. SDK page gains wada.sys.accel()
and wada.sys.keep_awake().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 10:22:10 -04:00
Christopher Van HooseandClaude Fable 5 fcc146f57a M9 IMU (QMI8658) + tilt-compensated heading
A two-axis magnetic heading assumes the device is level. At this latitude the
field dips ~60 degrees, so the vertical component is 1.6x the horizontal one
and tipping the device leaks it into the pair the heading is made from: about
1.5 degrees of heading per degree of tilt. That is what "it drifts" was once
the calibration was sound -- it was the hand holding it.

Driver: variants/thinknode_m9/M9Imu.{h,cpp}, QMI8658 at 0x6B on the peripheral
bus, accelerometer only (the gyro is most of the power budget and nothing here
needs it): +/-2 g at 62.5 Hz with the low-pass on, soft reset with a 160 ms
wait that covers both die variants, and the same idle-suspend as the compass so
it costs nothing when unused. CTRL1's ADDR_AI bit is set and read back -- with
it clear the burst read silently returns six copies of one byte, which looks
like a working sensor reporting nonsense. HAS_M9_IMU -> CAP_IMU ->
wada.sys.accel(), caps().accel.

Axes MEASURED, not guessed, by holding three attitudes and logging:
  flat, screen up        z = -1.02  -> +Z into the screen (down)
  on bottom edge, top up x = +0.97  -> +X at the top edge (forward)
  on left edge, right up y = +1.08  -> +Y at the right edge
So the IMU is already in the aerospace body frame, and it agrees with the
magnetometer's independently measured +Z-into-screen. (Meshtastic's M9 driver
passes both sensors through untransformed and mirrors the heading on the sign
of accel Z, so its compass flips when the device is turned over. Not copied.)

Heading now rotates the field back into the horizontal plane using gravity
(NXP AN4248 / ST AN3192) before taking the angle, and reports the tilt angle;
past 55 degrees it says "too steep to read" rather than lying. Calibration
returns to a 3D fit because tilt compensation needs the vertical offset too --
but the instruction is now "turn it every way ON ONE SPOT", and the
accelerometer VERIFIES it: coverage is measured by how far gravity swung, so a
flat spin is refused by name ("turn it nose over tail") instead of silently
fitting a degenerate sphere. Simulated in the harness against a modelled M9 in
a known attitude: offsets recovered exactly, and the heading holds within 3
degrees through 20 degrees of roll and pitch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 10:00:44 -04:00
Christopher Van HooseandClaude Fable 5 5a4a5a8ab6 Park the magnetometer when idle, and stop Lua apps ticking into a dark screen
Two answers to "does the compass app matter for battery life", both yes-ish
and both now fixed.

The driver put the QMC6309 into normal mode at boot and left it there, so it
converted continuously from power-on whether or not anything read it: ~1 mA at
100 Hz / OSR 8, forever, on a 2300 mAh battery. It is now parked in suspend
after configuration and woken on demand, with m9CompassIdleTick() (called from
the M9's existing per-loop branch) suspending it again two seconds after the
last read. Waking is a single register write since suspend preserves the
configuration; the waking call reports "nothing fresh" and the caller's next
poll gets data, which callers already handle because the chip may be absent.

Separately, lv_timer_handler() runs unconditionally, so a Lua app's timer kept
firing while the display slept -- GPS Compass polled the magnetometer at 10 Hz
into a dark screen, which would have held the sensor awake even after the fix
above. The host now skips the tick while the screen is off and resumes on wake;
dt comes from millis(), so an app sees one long frame rather than a broken
clock. Documented on the SDK page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 09:01:19 -04:00
Christopher Van HooseandClaude Fable 5 9677dcfe19 Merge upstream beta_68 into the M9 compass / GPS work
beta_68 expanded the Lua SDK (map, lists, packet delivery, discovery, private
messages/rooms, native crypto) across the same files as this branch, so four
files conflicted. Nothing was dropped from either side:

- wada.sys.gps(): both widenings merged into one binding. Upstream's
  fix_time / lat_e6 / lon_e6 and our speed_kmh / course now share a signature,
  and our stricter gate wins -- the call returns nil when the user has GPS
  switched off, not just when there is no fix.
- Altitude is upstream's `alt_m` alone. The resolution first carried `alt`
  beside it to protect a shipped app, but gpscompass has never been published
  to the store (it exists only in this branch and on a bench device), so
  carrying a duplicate key into the API forever was the wrong trade: the app
  reads alt_m instead.
- sysCaps() carries all twelve feature flags (upstream's seven, our compass,
  the four originals) with a matching table hint.
- wada.geo (upstream) and wada.sys.compass (ours) both survive; upstream's
  "no board has a magnetometer" note is corrected in the code and on the SDK
  page, since the M9 now does.
- hostTeardown frees upstream's new POST payload buffer as well as the fetch
  buffer, under the same in-flight guard.
- The catalog keeps all three new apps: upstream's wardrive and nearby, ours
  gpscompass (8 total, every referenced file present).

M9, V4, V4-R8, T-Deck and Pager all compile; the Lua host harness passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 02:43:55 -04:00
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
Kaj SchittecatandClaude Opus 5 28e4f0f332 fix(p4): battery flickered between 0% and the real value (regression from #273)
Mine, from the fuel-gauge change earlier today.

bq27220ReadU16() returned 0 on an I2C failure, which is indistinguishable from a
real reading of zero. The voltage reader had always defended against that by
range-checking (2500-4600 mV) and keeping the last good value, but the new
state-of-charge reader accepted anything <= 100 — so every lost transaction was
taken as a genuine 0%, and the display alternated between 0 and the real figure
as reads succeeded and failed.

The gauge shares this I2C bus with the XL9535 expander, the RTC and the touch
controller, so an occasional lost transaction is normal rather than exceptional.
Encoding that as a valid measurement was the mistake.

The transport now returns false when the gauge does not answer, and both readers
keep their previous good value on a failure. Once the gauge has answered once,
the percentage never falls back to the voltage curve either, so the number cannot
jump between two different scales.

Reported by Kaj on hardware. P4 builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:16:01 +02:00
Christopher Van HooseandClaude Fable 5 844d8c3357 V4-R8 perf pass + M9 UI polish (prefs v49/v50)
Heltec V4-R8 — "are we getting full performance?" audit. Compute side was already
at spec; six gaps closed (record: variants/heltec_v4/R8_AUDIT.md "Perf pass"):
- FEM LNA defaulted OFF on a KCT8103L board: prefs v49 defaults it ON on the R8
  (one-time flip of existing installs, no new field). Toggle stays in Radio & Mesh.
- Display SPI 40 -> 80 MHz (LGFX_SPI_WRITE_HZ; -D to 40000000 steps back).
- Async DMA band flush (LGFXDisplay::flushBandRGB565): swap into a 12 KB internal
  DMA buffer, one no-convert DMA per band, lv_disp_flush_ready at once so LVGL
  renders band N+1 while band N drains; lv_disp_flush_is_last closes the frame
  transaction so the shared micro-SD gets the bus between frames. Sync fallback.
- Wi-Fi modem power save is a pref (wifi_ps; toggle in Wi-Fi settings, live once
  associated). Default ON everywhere (unchanged), OFF on the R8.
- micro-SD 4 -> 20 MHz after the proven mount, read-verified (probe-file head
  captured at 4 MHz and byte-compared after the raise; falls back). Wired at all
  three mount sites; no-op without SD_SPI_FAST_HZ. Note: SDFS::begin returns true
  if already mounted, so every clock change goes through SD.end() first.
- R8 env boots at ESP32_CPU_FREQ=240 (setup() no longer runs at the V4's 80 MHz).
- Settings -> About "Perf:" row on the R8 (no serial console on that board):
  "CPU 240 · TFT 80 MHz DMA · SD 20 MHz · LNA on" is the all-green reading.

ThinkNode M9 UI:
- Spectrum page root gets NAV_SKIP_FLAG: nothing on it is actionable, so the
  d-pad no longer highlights the chart / scale box; Back closes it as before.
- Map "Show tile z/x/y" line defaults OFF (prefs v50, one-time flip; toggle kept).
- The tab bar is bypassed entirely on the M9: the accent "glow" indicator lived on
  the screen, not in the bar, so hiding the zero-height bar left it glowing over
  the content's bottom edge. No bar styling / gestures / key hints / indicator
  are built on the M9 now.

Cross-compiled: R8, V4, M9, T-Deck. Flashed + booted on the user's R8 and M9.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 13:51:11 -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
Kaj SchittecatandClaude Opus 5 19a7ca85ce fix(p4): report the fuel gauge's state of charge, not terminal voltage (#273)
@D-Melhede saw a P4 claim 71% and then die, with Meck-P4 reading 11% on the same
pack; a full charge never quite reached 100%; and "calibrate 100% here" made it
permanently worse. @wb6zsu watched it read 51%, then 100% the instant USB went in.

All of that is one cause. The P4 carries a BQ27220 fuel gauge, which coulomb-counts
against its learned pack profile and therefore knows the actual state of charge, but
we only ever read its Voltage() register and pushed that through the generic
3.30-4.20 V linear curve. Terminal voltage is charger-driven, so it pins at 100% the
moment USB is connected however empty the pack is, sits under 100% on a rested full
pack, and calibrating "full" while charging captures a charger rail as the reference
and bakes the error in for good. The old comment conceded the limit in as many words:
"the real cell % isn't observable here". On this board it is.

Now reads StateOfCharge() and uses it directly, falling back to the voltage curve
only when the gauge does not answer or reports out of range, so a dead gauge is no
worse than today rather than a made-up number.

The register is REG 0x2C, taken from LilyGo's own SensorLib (GaugeBQ27220.hpp
"StateOfCharge; // % - REG2C") rather than from memory, since guessing a register
here would produce confident nonsense.

Does NOT address the charging/LED half of that report: whether the pack charges
correctly and what drives the red/green LED is the BQ25896 charger, which we still
do not talk to. Tracked separately in the issue.

P4 builds; T-Deck and V4 unaffected (the gauge path is board-gated).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:01:13 +02:00
Kaj SchittecatandClaude Fable 5 31e373a4b1 core-117-prep: finish the ColorVal migration on the IDF display headers + pager core-scope includes
TanmatsuDisplay.h / RM69A10Display.h / HI8561Display.h still had the 1.16
Color signatures (missed in the first sweep); pagers additionally hand the
core lib scope the TFT_eSPI include path (target.h -> ST7796LCDDisplay.h).
All 8 S3 envs + the Tanmatsu now build green on the 1.17 test pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:15:08 +02:00
Kaj SchittecatandClaude Fable 5 e3c5c4237b core-117-prep: 1.17 core compile fixes across all targets (TEST PIN — not for main)
- UIColor migration: DisplayDriver::Color enum -> ColorVal/UIColor statics in the
  6 driver headers + 3 driver .cpps + all main.cpp/UITask.cpp call sites.
- ONE palette definition: src/helpers/ui/UIColorPalette.cpp. Envs whose
  DISPLAY_CLASS is a core driver (v4_tft, attaky, tdeck, M9) take the palette
  the core driver .o carries; envs on our drivers (r8, rak via src_filter;
  pagers via helpers/ui glob; both IDF apps via the whole-archive src glob)
  compile ours. Never both — same-name core objects collide (core LGFXDisplay).
- 1.17 ESP32Board.cpp includes <target.h>: hand the core lib scope the libdeps
  include paths it can't chain to (MicroNMEA everywhere, SensorLib on pagers).
- CustomLR1121Wrapper: setRxBoostedGainMode void->bool (1.17 base).
- IDF: fetch-deps.sh gains durable vendor-time patches (ESP32Board minimal
  deep-sleep — no target.h on IDF; SerialBLEInterface esp-nimble-cpp 2.x port
  incl. esp_mac.h). helpers/ethernet/ excluded in both meshcore CMakeLists.
- platformio.ini pins point at a LOCAL test tag (mc117-clean#core-v1.17.0-test);
  swap to the real core-v1.17.0 before merging to main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 16:13:09 +02:00
Kaj SchittecatandClaude Fable 5 22e99e1854 touch: P4 port doc — correct the TFT backlight description (#242)
The doc claimed the HI8561 TFT's backlight is panel-internal (DCS 0x51 only);
in reality it's a real LED on GPIO 51 driven by LEDC PWM, which the code has
done since ~beta_53. Stale doc line flagged by bmorcelli.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:49:47 +02:00
Pixel Perfect 2996018643 fix(tpager): arbitrate shared SPI SD lifecycle
Signed-off-by: Pixel Perfect <me@pixp.cc>
2026-08-05 17:55:48 -07:00
Kaj SchittecatandClaude Opus 4.8 a2548c39ab touch: T-Pager gets the full store-on-SD machinery (#193)
The pager's SD support shipped in pieces (mount ladder with XL9555 card-detect,
tile-cache fallback, shared TFT_eSPI SPIClass) but CAP_SD/CAP_FILESYSTEM were
never flipped from the pre-wiring 0, so the Files app gates and the whole
'Store data on SD' machinery stayed compiled out -- an inserted, mounted,
browsable card that nothing would store to. Flipped the caps, extended the
boot storage selection + SPIFFS->SD migration + SdNvsPrefs routing gates to
TLORA_PAGER, and exported tloraPagerSharedSPI() from the variant so main.cpp
can mount the card at boot on the shared bus like the T-Deck.

Both pager envs + T-Deck + V4 + R8 build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-01 12:19:46 +02:00
Kaj SchittecatandClaude Opus 4.8 3bf56cdd37 p4: #167 resolved -- SD writes flash the AMOLED; hot store moved to internal LittleFS
Root cause, established by on-device elimination over one long session: SD-card
WRITE bursts electrically disturb the T-Display P4's AMOLED -- the whole-screen
blue flash of #167. The display stack itself measured clean at every layer while
flashes were visible: framebuffer content (flush tracer: every band black), DSI
bridge underrun interrupt (never fired), DSI host protocol errors incl. the host
payload-buffer underflow (never latched), DCS/brightness traffic and XL9535
expander writes (silent), disp on/off (never). The flash survived every digital
derate -- 4->1-bit SD bus, 20->10 MHz SD clock, 1000->750 Mbps DSI lane rate --
on every card tried, at any brightness (including near-zero, i.e. the panel is
being upset at the hardware level, not showing content), yet vanished the moment
the card was removed or left unwritten. The card's rail is switched off the same
3V3 domain as the panel (XL9535 SD_EN), so its program-current spikes reach the
panel and no firmware setting can filter them. Streaming tile writes never
flashed; rapid open-write-close store bursts always did. Meck-P4 not flashing
proves nothing about the board: it never writes the SD.

Fix: the hot store (contacts/channels/identity/blobs/prefs-file/chat history)
lives on the INTERNAL 'storage' partition; the SD keeps bulk, gentle writers
(map tiles, backups). The internal partition is mounted as LittleFS, not FAT:
the P4's FAT layer has documented broken metadata and the v3 migration proved
its writes are no better (a 102 KB contacts3 "copied" onto it read back as 0 B;
on LittleFS the same copy reads back byte-exact and loads all 689 contacts).
esp_littlefs looks partitions up by label with SUBTYPE_ANY, so the ex-FAT
partition converts in place on first boot (formatOnFail) -- no partition-table
change, OTA-safe.

One-time v4 migration (lessons v1-v3 encoded): substance-gated (only from a
card whose contacts3 beats the internal one -- a fresh test card once donated a
blank store), whitelist-only copy (v2 filled the partition with junk before
contacts3 landed), both historical card layouts probed (root vs /meshcomod),
marker written only after the copy, card never written (it remains the
rollback).

Also in this change, kept as opt-in diagnostics (dormant without their env
gates): WADA_P4_POKE_TRACE (DCS/expander/disp-on-off tracing + a DSI host
error-register poller), WADA_SD_KHZ / WADA_SD_1BIT (SD primary-rung derates),
WADA_DSI_MBPS (lane-rate override; default back at the vendor 1000 -- the 750
experiment changed nothing and is documented in the driver).

Verified on device: no flash on the contact-churn trigger with the store
internal (the previously 100%-reproducing trigger), 689/689 contacts after
migration, tiles still on SD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 14:50:37 +02:00
Kaj SchittecatandClaude Opus 4.8 1e6e121c85 p4: blank the framebuffer before display-on; widen LVGL bands (#167)
Two changes against the whole-screen colour flash.

1. Real fix for the flash testers see BEFORE the boot splash. The panel was
   switched on and only then was the framebuffer cleared, so the DSI streamed a
   freshly-allocated (garbage) PSRAM buffer to the screen until the clear caught
   up. Blank it with a memset + cache flush while the panel is still dark. That
   also removes 39 back-to-back draw_bitmap transfers into a buffer the panel is
   scanning out.

2. Diagnostic, P4 only: LVGL bands 24 -> 64 lines. This panel is DSI video mode
   with a SINGLE framebuffer, so every band is copied into the buffer being
   actively scanned. At 24 lines a full-screen repaint is ~26 back-to-back
   transfers into that live buffer. The reported triggers (message arrives,
   opening a chat with unread, adding/deleting a contact) all rebuild the whole
   screen; panning the map repaints only its own region and never flashes, which
   is the shape this predicts. Wider bands cut the same repaint to ~10
   transfers. Sized so the 2x upscale scratch still fits internal SRAM, so burst
   count is the only variable that moves.

Supersedes the write-path theory: the flash still occurs with contacts, history
and prefs all on SD, and the pre-splash case involves no write at all.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 11:40:40 +02:00
Kaj SchittecatandClaude Opus 4.8 3705a66288 touch: P4 — opt-in flush tracer for whole-screen colour-flash reports (#167)
Diagnostic scaffolding kept from chasing the "screen flashes teal on every
message" report, behind -DTDP4_FLUSH_TRACE so it is compiled out of normal
builds (verified: no FLUSHTRACE string in the shipped binary).

It logs any large, mostly-single-colour flush band with its RGB565 value. That
answers the one question that was otherwise unanswerable from the outside: is
the flash something LVGL painted, or something below it? A UI-painted flash
appears as a burst of bands carrying that colour; if the screen visibly flashes
and nothing logs, the cause is in the panel/DSI layer and hunting through UI
widgets is wasted effort.

Two notes recorded in the comment because both cost time here. One LVGL band on
this board is 284x24 = 6816 px, so a size gate has to sit well below that — an
earlier 8000 px threshold silently disabled the whole tracer and looked like
"nothing is drawing". And the per-band uniformity scan measurably slows the
flush path, so it can mask a timing-sensitive bug: any fix must be confirmed on
a build with the tracer OFF.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 16:07:38 +02:00