Greptile round 9 remediation:
The round-8 heartbeat rewrote the marker unconditionally. If
installer A stalled on a single tile operation for longer than the
marker TTL, installer B could legitimately reclaim the expired
marker, and A's next heartbeat then overwrote B's live claim --
stealing the card out from under a running install.
The renewal now reads the marker before rewriting and only
proceeds when it still carries OUR owner (the same owner comparison
used by release and commit-time verification). A reclaimed,
deleted, or corrupt marker means the claim is void: the install
aborts with a clear "wait for the other installer to finish and
retry" error, leaving the foreign claim intact. The partially
published pack stays device-harmless (no record names it) and the
user retries once the other installer finishes.
- CLI: _renew_install_marker() checks owner before rewriting;
PackError aborts the install (the existing cleanup paths remove
the staged temp pack and release the marker as a no-op).
- Flasher: renewInstallMarker() checks owner before rewriting;
fail() aborts the install (the existing cleanup paths remove the
owned pack and release the marker as a no-op).
- Tests: CLI 88 (renewal refuses to steal a reclaimed/missing/
corrupt claim; same-owner renewal still advances the epoch);
flasher 26 (same contract).
Greptile round 8 remediation:
The install marker was written once at install start. A publication
longer than the 15-minute TTL (a full world pack on slow SD storage)
aged out while its installer was still live; a second producer then
reclaimed the marker and both producers passed commit-time
revalidation against the same activation state.
Both producers now renew the marker's epoch during tile publication
(every 25 tiles plus one final renewal), keeping the claim live for
arbitrarily long installs while a crashed installer (no renewals) is
still reclaimed after the TTL. Release and commit-time marker checks
compare the owner, not the full token, so a renewed marker is still
recognized as ours and a foreign owner is never deleted by mistake.
Adds regression tests on both sides: CLI renewal count + owner-based
release, CLI crashed-marker still reclaimable, and the flasher
renewal contract (epoch advances, owner preserved).
CLI: 87 passed. Flasher: 25 passed.
Greptile round 7 remediation:
The CLI's advisory flock and the web flasher's Web Locks name live in
incompatible lock namespaces, so a CLI install and a browser install
on the same card could each hold "their" lock, both pass preflight on
identical slot state, and clobber each other's records (orphaning a
pack, or writing the same generation into two slots).
Both producers now share two symmetric layers:
1. An on-disk install marker (.pyxis-installing in pyxis-map/,
token "PYXI 1 <owner> <epoch_ms>") claimed at install start via an
atomic exclusive create, with ownership verified by read-back. A
fresh foreign marker refuses the install before any mutation; a
stale one (30-minute TTL; a future epoch counts as fresh under
clock skew) is reclaimed. Release only deletes the releasee's own
token.
2. Commit-time revalidation: activate_map_set / activateMapSet
snapshot the raw slot and style bytes at derivation, re-read them
immediately before the record writes, and abort if the activation
state moved or a fresh foreign marker is present. The published
pack is kept (device-harmless: the firmware only reads packs named
by an active selection) and the existing retry/resume path
converges it.
Regression tests on both producers: marker refusal before any
publication, stale-marker reclamation and release, and
commit-time-race abort with retry convergence. The in-memory flasher
FS now models getFileHandle({exclusive:true}) -> FileExistsError.
CLI: 85 passed. Flasher: 24 passed.
New testing/docs surface for grabbing the active LVGL screen as a
PNG over USB-CDC. Useful both for documentation (round-trip capture
of every public screen via T:SHOW <name> + T:SCREENSHOT) and
automated UI regression tests.
On-device:
- lib/lv_conf.h — enable LV_USE_SNAPSHOT (~5 KB code; uses PSRAM
via the existing hybrid allocator so internal RAM is unaffected)
- src/main.cpp — T:SCREENSHOT handler takes an lv_snapshot_take()
of lv_scr_act() under LVGL_LOCK(), dumps a delimited base64
stream over CDC. Inlines a tiny base64 encoder (no new dep).
- src/main.cpp — T:SHOW <name> dispatches to UIManager::show_*()
for the six publicly-navigable screens (conversation_list,
compose, announces, status, settings, propagation_nodes).
Wire format:
T:SCREENSHOT BEGIN W=320 H=240 FMT=rgb565<be|le> BYTES=153600
<base64 line, 76 chars>
...
T:SCREENSHOT END
Host side:
- screenshot.py — auto-detects the pyxis port via T:ID probe,
sends T:SCREENSHOT, reads until END, filters out interleaved
log lines (heap heartbeats / BLE stats can splice in
mid-dump), validates byte count matches header, decodes RGB565
with the documented byte order, expands channels via 5→8 / 6→8
high-bit replication, saves PNG. Pillow + pyserial.
Catalog:
- docs/serial_commands.md — full reference for all T:* commands
accumulated so far (identity/paths/send/receive, propagation,
voice, BLE, UI). New commands should land here when added.
Throughput: ~205 KB base64 over CDC at 115200 → ~18 s/shot. Fine
for docs and automated tests, not video. Bumping baud or zlib-
compressing on-device is queued in the doc as future work.
GitHub release download URLs redirect to release-assets.githubusercontent.com
which doesn't return Access-Control-Allow-Origin headers. The browser blocks
cross-origin fetches from the GitHub Pages flasher, causing "Failed to fetch"
for any versioned release while the latest dev build (same-origin) works fine.
Fix: Deploy versioned firmware binaries to GitHub Pages alongside the dev
build at firmware/releases/{tag}/, so all versions are fetched same-origin.
The CI workflow now downloads existing release assets and deploys them to
Pages with keep_files: true to preserve across deploys.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lets users choose between the latest dev build and tagged GitHub releases.
The dropdown queries the GitHub Releases API on page load and swaps
firmware fetch paths between Pages-relative and release-asset URLs.
CI now attaches all 4 firmware files to releases (bootloader, partitions,
boot_app0, firmware) so full installs work from any release version.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite release-firmware.yml to build the tdeck (NimBLE) env on pushes to
main (versioned as dev-<sha>) and on v* tags. Remove checked-in firmware
binaries from git tracking — CI now generates and deploys them to Pages.
Release creation is conditional on v* tags only.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace manual DTR toggle with esploader's built-in hardReset() which
uses RTS (EN pin) to properly reset the ESP32-S3 after flashing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Transport tracing and missing enableTracing flag caused timing issues
with ESP32-S3 USB-Serial/JTAG during bootloader sync. Also fixes
flashMode/flashFreq format, routes esptool output to on-page log,
and adds firmware binaries.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split T-Deck firmware from microReticulum examples/lxmf_tdeck/ into its
own repo. microReticulum is consumed as a git submodule dependency pinned
to feat/t-deck. All include paths updated from relative symlinks to bare
includes resolved via library build flags.
Both tdeck (NimBLE) and tdeck-bluedroid environments compile successfully.
Licensed under AGPLv3.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>