The 273 baked RGB565+alpha emoji images in src/ui-touch/emoji_data.c are
208,908 B of the Tanmatsu's application.bin, which may not exceed
50 x 64 KB pages. Deflated as one stream they are 105,053 B.
The Tanmatsu's IDF build now leaves emoji_data.c out and compiles
variants/tanmatsu/emoji_deflated.cpp, which provides the same
emojiGlyphLookup() / kEmojiGlyphCount. Its header is generated at build
time from emoji_data.c by scripts/build/gen-emoji-deflated.py (a CMake
custom command into the build dir, nothing committed), so emoji_data.c
stays the one source and add-emoji.py keeps working unchanged. The script
inflates its own output and checks every image and glyph mapping against
the source before writing, so a bad blob fails the build.
On the first lookup that hits a known emoji the blob is inflated with the
ROM tinfl (no flash cost) into one 208,908 B PSRAM block plus 273
descriptors; lookups that miss never inflate. An allocation failure gives
"no glyph" and is retried on a later lookup; a blob that will not inflate
is not retried per draw. Same pixels, same headers, same sharing of one
image by several codepoints (U+FE0F, U+200D, ...).
S3 boards, the web build and the other P4 builds do not compile anything
here (variants/tanmatsu is only globbed by tanmatsu/), so their images do
not change.
Host test test/test_emoji_deflated.cpp compares the deflated lookup with
the raw emoji_data.c for every codepoint up to U+10FFFF and drives the
out-of-memory and corrupt-blob paths (zlib stands in for the ROM tinfl).
run-host-tests.sh gains per-test prep (generate the header under $OUT)
and link flags.
Measured with WADA_FW_TAG=beta_99: 3,179,904 -> 3,076,400 B.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The main loop's Wi-Fi retry ran "every 10 s while not connected", with a
timer that only moved when it fired. After a long connected stretch it was
ancient, so the very pass that saw the link drop ran a disconnect + begin
on top of the reconnect the driver had just started for the same loss
(Arduino's auto-reconnect on the S3 boards, the C6's own AT+CWRECONNCFG on
the T-Display P4). And the S3 retry's disconnect erased the stored network
(disconnect(false, true)): that is the retry that stayed "disconnected",
while an apply after a scan (the same disconnect without the erase, then
begin) joins.
When to rejoin is now src/helpers/WifiRejoinPolicy.h, a pure state machine
shared by src/main.cpp (every S3 board) and the T-Display P4 loop:
- a link that drops gets a 10 s grace for the driver's own reconnect;
- a running scan, a join in flight or a C6 reset restarts the window;
- six fast tries 10 s apart, then every 30 s, so an AP that is gone all
night is searched about a third as often (964 vs 2881 attempts in 8 h);
- the screen coming back on asks for one prompt attempt (not within 3 s
of the last one), so a device picked up after sleeping does not wait
out the back-off.
The rejoin itself leaves the BSS without erasing it, waits 50 ms and joins
again, as the scan apply does. On the hosted P4 a scan now also holds the
retry off, as it already did on the S3 boards.
test/test_wifi_rejoin_policy.cpp drives the policy pass by pass with a
fake clock: switch-on, link loss with and without the driver recovering,
scan and join busy windows, an 8 h overnight on the battery saver's 50 ms
loop then the wake, Wi-Fi off and on, and the millis() wrap. The runner
also gets the include path test_c6_link_health needed (it failed to build
from scripts/run-host-tests.sh).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Host tests that compile the production variant files (TLoraPagerBoard.cpp,
PagerHaptics.cpp, PagerImu.cpp from #748-#751) against register-level fakes of
the BQ25896, BQ27220, DRV2605 and BHI260AP (test/pager_hw_fakes/):
- test_pager_power: REG0B decoding (USB host, adapter, power-good alone, OTG not
counted as USB, the 1 s cache), the temperature guard with its 3 C band, user
limits and implausible readings, battery care 80/75 %, both holds through the
one charger switch, charge limits rounded to the chip's 64 mA / 16 mV steps,
the gauge capacity written once per target (a sealed gauge is left alone on
later boots), and ship mode refused on USB, read afresh, BATFET_DLY cleared
and BATFET_DIS set on battery.
- test_pager_haptics: the waveform slots of every pattern and strength, all
eight slots rewritten, standby only after GO clears, no bus traffic without
the chip.
- test_pager_imu: one firmware upload per boot at 1 MHz with the bus clock put
back, scaling to g, stale samples refused, the 3 s idle stop and restart, no
retry storm after a failed upload.
Each was checked against a mutated copy of its subject (hysteresis removed,
care resume moved, ship mode on USB, OTG as USB, stale waveform tail, standby
mid-pattern, capacity written every boot): all eight mutants fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up on Martin Kremers' (@kremers) PMTiles reader, behaviour unchanged
where it was right:
- CAP_MAP_PMTILES (CAP_MICROSD minus the Tanmatsu) gates the reader, the Map
file picker and the zoom limit. The Tanmatsu has a card too, but its AppFS
image has under 1 KB left and this is ~11 KB.
- The directory walk and the gzip inflater move from UITask.cpp into
PmTiles.cpp (checkArchive, gunzip, locateTile over a read callback), so the
whole lookup runs natively. UITask keeps the File glue and the UI.
- Bounded allocations: a directory may hold at most 32768 entries (768 KB
decoded) and only blocks of 16 KB or less fall back to internal RAM, so a
hostile entry count or a big leaf can't take the internal heap Wi-Fi needs.
- Uncompressed directories (internal compression 1) are valid v3 and now
load instead of being listed as "not a PNG raster archive".
- A corrupt directory logs once per archive instead of once per tile.
- The Map file row only shows with Tiles from SD card on, like the zoom row:
picking an archive did nothing with server tiles.
- test/test_pmtiles.cpp now runs from scripts/run-host-tests.sh (it could not
find PmTiles.h there) and checks the full lookup against archives that
test/pmtiles/make_fixtures.py writes from the spec: gzip and plain
directories, leaf directories, runs, unclustered data, gzip header fields;
vector, JPEG, gzip-tile and brotli archives refused; v2, cut short, bad CRC,
lying gzip sizes, hostile counts, out-of-section entries and a leaf that
points at itself all fail cleanly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two ways the clock read hours off while the zone setting looked right.
The ESP32-P4 esp-hosted boards started SNTP with configTime(0, 0, ...),
which also sets TZ to UTC, so from the first Wi-Fi join every displayed
time was UTC until the zone was picked again. #756 fixed this on the
T-Display P4; the CrowPanel P4 10.1 still had it. It now starts SNTP with
the user's zone (configTzTime) and, as on the T-Display, only pushes a
COMPLETED sync into the clock instead of the stale boot time.
Settings > Clock & time fills "By hand (local time)" with the clock's
reading when the page opens. Picking another zone (or stepping the custom
offset) left those digits in the old zone, so "Set clock" right after a
zone change read them in the new zone and moved the clock by the
difference. The field now refills whenever the zone is applied.
The zone table and its POSIX strings move to helpers/TimeZones.h (same
strings, same order) so test/test_time_zones.cpp can check every zone
through the C library's localtime(): January, July and today's offsets,
the US and EU DST edges to the minute, the Custom offset's sign for
UTC-12..+14, and ClockFloorRTC with the zone in force (a later clock set,
like the GPS re-sync every 30 minutes, moves the time and never the zone;
a chip that reads behind the floor or garbage-future falls back to the
system clock).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The button names what the next press will do, and relabelling in place is only
safe on firmware that carries the button:set fix. Add wardrive_start_stop
(Start/Stop toggles under caps().panel) and wardrive_start_stop_old_firmware
(the label stays put without it), both scoped to the 1.3 path so 1.1 and 1.2
keep running the utf8 scenario alone.
Red until the fix on top: wardrive_start_stop expects "Stop" while the survey
runs, which the app does not do yet.
Assisted-by: OpenCode:deepseek-v4.1-flash
Signed-off-by: Sebastian Rutofski <kontakt@sebastian-rutofski.de>
When emcc is installed, release.sh <beta> first runs web/build.sh web and,
when Playwright is in web/node_modules, web/build.sh webtest (shots to a
temp dir, so the tracked web/docs stay as they are), and stops on a
failure. Without emcc it says so and carries on; WADA_SKIP_WEB=1 skips
it. Promotions are untouched. Nothing web is published from here:
web/dist is deployed by scripts/deploy-webclient.sh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No textual conflicts. Checked by hand: device_caps.h keeps the WADA_WEB
block (CAP_USB_FILES 0 there) under main's new native-USB CAP_USB_FILES
gate; apps.json keeps main's new and bumped apps (blackjack 1.2.2,
protreck 1.2, tripodometer 1.1, spaceinvaders) and web-build's
"radio": "own" on RF Monitor, Airtime and Wardrive; lua_builtin.h
regenerates unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Harness: #648 and #607 each declared the map mock's map_obj/map_centers
locals; the second declaration shadowed the first, so reset_world() and the
scenarios after it would never see the map the mock created. One declaration
is kept. tripodometer, protreck and gpscompass harness runs: ALL OK.
Catalog: the UTF-8 BOM the PR put in front of apps.json is dropped (json.load
and other strict readers refuse it), and ProTreck keeps "seed": false, which
the PR had removed: without it gen-lua-builtin.py would bake ProTreck into
every firmware image instead of serving it from the Store.
Measured on a T-Deck with link counters (now in the HELLO reply as "rx"):
every request arrived and was answered, but 1 reply in 5 stayed in the
HW-CDC's 64-byte FIFO until the next write, so the page sat out its 3 s
timeout and asked again. Every frame is now flushed. During an upload the
session also waits 15 ms (was 5) for the next chunk instead of paying a UI
pass per chunk. 256 KB: SD 2.1 s, internal 3.2 s, tiles 6.5 s (LittleFS-bound);
selftest up 79 KB/s, down 61 KB/s. scripts/usbfiles.py reads what has arrived
instead of waiting out its 50 ms timeout per reply.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merges main 8954fe06. The drift alarm caught the repeater admin app's new
members: sendStatusPingForUI, sendTelemetryRequestForUI,
uiSendBinaryRequest (-> onBinaryReply), UiReqKind::Binary and
uiLastLoginInfo/UiLoginInfo. Over a node: CMD_SEND_BINARY_REQ and its tagged
PUSH_BINARY_RESPONSE, the ACL and firmware level of a modern LOGIN_OK. The
demo's repeater and room log in (any password = admin, empty = guest) and
answer status, neighbours, the access list, owner info and CLI lines; the
mock node answers neighbours and owner info.
Also from main: nvs_open/nvs_close in the compat (SdNvsPrefs asks first),
and the tiles partition now has to exist before the UI mounts it: the compat
partition table reports "tiles" (the web board's LittleFS volume). Font
guards keep WADA_WEB next to main's new SenseCAP Indicator.
No-threads bundle: a UI-thread wait called from the page (not the main
loop) no longer sleeps (it hung the flush after a Store install).
A wheel turn counts as user activity (no screensaver mid-scroll).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- webclient.html: what the web client is, how to connect (USB, Bluetooth,
Wi-Fi), where it works (desktop, Android, iPhone), the https / http split
for Wi-Fi, where data lives, what only a device can do, and a FAQ, with
screenshots of the real client.
- index: "Use the web client" next to Install and Join the community, a Web
client button on the side rail and a resource card; sitemap.
- deploy/nginx/app.wadamesh.com.conf: the vhost (COOP/COEP/CORP, versioned
bundles cached, same-origin /tiles/ and /fw/apps/ handed to the tiles and
firmware vhosts over loopback). scripts/deploy-webclient.sh publishes
web/dist (and web/dist-nothreads to nt/).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #682 port is merged; add its env to release.sh, a flasher manifest, a
card under Seeed Studio on wadamesh.com (experimental, community port, no
device updates) and the README. The site card points at the beta feed, so
deploy the site after the next beta publishes. Board count 16.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Location: the web board has CAP_GPS 1. web/platform/WebLocation.* is its
SensorManager + LocationProvider, fed by navigator.geolocation (page shell,
only while the UI's GPS is on, the prompt deferred to a tap) or WADA_GPS on
the host, so the GPS switch, status, map centring, distances and
wada.sys.gps (with speed/course) work unchanged. The position reaches a
connected node (CMD_SET_ADVERT_LATLON, displaced by the Location privacy
radius) only while the node's advert-location setting is "share"; the demo
moves its cast to the visitor's location.
Map tiles: wadaHttpRequest serves the firmware's default tile server only:
a synchronous emscripten_fetch from the tile_fetch pthread to the
same-origin tiles/ path (serve.py proxies it; nginx location for the site),
plain HTTP natively. WiFi reports connected while the browser is online.
Fixes the static-init order of the host VFS (the UI's own tiles LittleFS
copied an empty impl).
Polish: Lua apps that need the board's own radio carry "radio": "own" in the
catalog (baked under CAP_RADIO_TOOLS, greyed in the Store); mouse wheel and
trackpad scrolling; Settings > Wi-Fi / Bluetooth explain the page bar
(CAP_WIFI_SETUP / CAP_BT_SETUP); no Download mode (CAP_USB_FLASH); no
receiver settings on Settings > GPS (CAP_GPS_RECEIVER).
Tests: webtest checks the map centres on Playwright's Ghent location and
shows tiles; nodetest checks the location privacy rule against the mock node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The touch UI (src/ui-touch, unmodified apart from WADA_WEB board guards)
builds for macOS with CMake and runs in an SDL2 window: a compat layer
for the Arduino/ESP-IDF/FreeRTOS surface, a MyMesh stand-in with a
MeshBackend interface (DemoBackend now), and the web board's SDL
display/touch/keyboard drivers. WEB_BUILD.md follows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SX1262's NSS, RESET, BUSY and DIO1 sit on the TCA9535 (P0.0..P0.3)
while SCK/MISO/MOSI are real GPIOs. IndicatorRadioHal derives from
ArduinoHal, keeps hardware FSPI at 4 MHz and maps the virtual pins
0x40..0x43 onto expander transfers; every other pin goes straight to
the Arduino pin and interrupt functions, as ArduinoHal does. The
meshcomod core is used as is: CustomSX1262 and its wrapper run
unchanged on top of the HAL.
DIO1 has no GPIO edge. The expander's open-drain /INT on GPIO42 has an
IRAM ISR that only notifies a dispatch task, because the read needs
I2C and the bus mutex. That task reads both input bytes under the bus
lock: the TCA9535 releases /INT only when the port that changed is
read, and several port-1 inputs float, so a port-0-only read could
leave GPIO42 low for good. Every port-0 byte feeds a small edge
detector (Dio1Edge) through the expander's port-0 observer, which
calls the callback RadioLib attached to DIO1 once per rise. That
callback is the core's setFlag, so TX-done and RX-done reach the core
exactly as they would from a DIO1 interrupt.
A BUSY poll is a port-0 read as well, and it clears /INT. Sampling DIO1
in those reads means a rise that lands while RadioLib waits on BUSY
fires the callback from that read instead of being lost; the dispatch
task's next read sees the level already high and does not fire again.
The task also re-reads every 50 ms, so a rise whose /INT was missed
still fires the callback, at most 50 ms late. MESH_RADIO_DIO1_POLLED is
not set: polling the IRQ register would add an SPI transfer, and with
it expander writes for NSS, on every pass of the receive path.
Floating port-1 inputs can keep /INT toggling, each toggle an I2C read
at high priority. The task counts ISR wakes and timeouts and logs them
for each of the first six 10 s windows, then only above 500 wakes/s
and at most once a minute, so the idle wake rate is visible at
bring-up without flooding the console later.
TCXO or crystal comes from the radio strap read at boot: the env's
SX126X_DIO3_TCXO_VOLTAGE calls indicatorTcxoVoltage(), which returns
2.4 V for the TCXO radio and 0 for the crystal one. radio_init() starts
the radio with the strap's setting and, if that fails, with the other
one, because std_init only falls back from TCXO to 0 V and a TCXO board
whose strap misread as crystal would never start otherwise. main.cpp's
one retry runs both again. The log says which voltage was requested and
which one the radio came up with.
The board reports GPIO42 (indicator::kExpanderIntGpio, shared with the
HAL) as its IRQ line and never light-sleeps: the inherited ESP32Board
versions would treat the virtual DIO1 pin as a GPIO, and /INT is active
low while the light-sleep wake source is high-level.
test_indicator_dio1 covers the virtual-pin routing and the
once-per-rise rule across dispatch and BUSY reads; run-host-tests.sh
gets its include path.
Signed-off-by: Confituurke <9595028+Confituurke@users.noreply.github.com>
Add the Seeed SenseCAP Indicator D1L as a new touch target. This step
builds and boots to a blank screen; the panel, touch and radio drivers
follow in their own commits so each can be reviewed against the
hardware it drives.
The SX1262's NSS, RESET, BUSY and DIO1, the LCD CS/reset and the touch
reset all sit on a TCA9535 at 0x20 on the SDA39/SCL40 bus, not on ESP32
GPIOs, so everything after this commit depends on the expander. The env
gives the radio lines virtual pin numbers 0x40..0x43 for a radio HAL to
map onto expander bits; until that HAL exists the radio is a plain
Module that links but cannot drive the chip.
The expander driver is header-only so a host test can cover it:
- its output and config shadows are seeded from the chip, because a
warm reset or the previous firmware may have left pins driven and
writing the power-on defaults back would glitch the radio, panel and
RP2040 resets;
- a failed write rolls both shadows back to the last state written in
full, and the next write brings the chip back in line with them;
- polarity inversion is cleared when a previous firmware left it set,
since it would invert BUSY, DIO1 and the strap; it is written only
when it is not already 0;
- every read of input port 0 is reported to an observer, so a DIO1 edge
is not lost when another reader clears /INT.
A five-sample majority of the P1.3 strap selects the TCXO (2.4 V) or
crystal radio at run time, so one noisy read cannot pick the wrong
oscillator.
The expander, the touch driver and the display init serialise on one
recursive mutex over the shared Wire bus (IndicatorIo::BusLock). It is a function-local static, so the compiler
guards its creation when two tasks reach the first lock together.
IndicatorIo::begin() logs the registers it inherited and names the
step that failed, since a dead expander takes the panel and radio down
with it and is otherwise silent.
The board has 8 MB of flash. The partition table has one 4.5 MB
factory app slot, a 3.375 MB SPIFFS (as on the 16 MB boards) and a
64 KB coredump, with no map tile partition. The factory slot is larger
than the 0x3E0000 OTA slots of the 16 MB boards, so an image that fits
those fits here. CAP_OTA is off: with one app slot there is no in-place
update. CAP_BATTERY is 0: no battery is fitted.
Montserrat 18/20/24 for the UI-size presets are the generated faces; the
board joins their gate in ui_font_18/20/24.c, gen-touch-fonts.sh,
hwFitFont and luaHostFigureFont. LVGL is built at -O2 with the Arabic and newlib-memcpy
settings the other envs use.
run-host-tests.sh builds every test/test_*.cpp, so the new expander
test gets its include path there in the same commit.
build-upload-monitor.sh gets --sensecap-indicator.
Signed-off-by: Confituurke <9595028+Confituurke@users.noreply.github.com>
- p4-release.sh builds and verifies the three revisions (V1.2, V1.1, V1.0)
from crowpanel_p4_101/ in the same loop as the T-Display P4 images, with
the same OTA-name and tag checks.
- Self-update is on (CAP_OTA 1): each revision updates from its own
wadamesh-crowpanel-p4-101-v1x.bin, so a V1.0 never takes a V1.2 image.
- gen-flasher-meta writes a manifest per revision; wadamesh.com gets a
CrowPanel Advanced 10.1 card with a V1.2 / V1.1 / V1.0 picker, and the
standalone flasher one card per revision. Both point at the beta feed,
so nothing shows until the next beta publishes the images.
- README, DEVICES.md and the site's board count (15).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Standalone ESP-IDF target (crowpanel_p4_101/) modelled on tdisplay_p4/, sharing its
ESP-IDF, components and esp-hosted 2.12.3 backend. Untested on hardware.
- variants/crowpanel_p4_101: EK79007 MIPI-DSI 1024x600 display (Elecrow timings,
LEDC backlight on GPIO31), GT911 touch on I2C0 45/46 (RST40/INT42), SX1262 on the
J9/J11 header with a real DIO1 IRQ, LDO3/LDO4 bring-up, C6_EN reset callback on
GPIO32, full-system-reset restart shim.
- Board revision is a build option: WADA_CP_REV=1.0|1.1|1.2 (default 1.2). V1.2 moves
LoRa DIO1/NRST from GPIO53/54 to 27/28; V1.0 has the C6 SDIO data lines reversed
(run 1-bit like Elecrow's V1.0 lesson). hostedSetPins() overrides Arduino's
generic esp32p4 SDIO pin defaults, which only match V1.0.
- LVGL at native 1024x600 with the large-screen layouts; fixed landscape.
- Shared src/: one CAP_* block in device_caps.h plus HAS_CROWPANEL_P4_101 at the P4
platform sites (storage law, SD_MMC, hosted BLE, no RTC_CNTL, no MQTT, fonts) and
the CrowPanel no-unlock-control sites. navOpenChatPanel/navGoToMainTab get
fallbacks for boards without CAP_KEYPAD_NAV.
- build.sh merge writes a 0x0 merged image per revision.
T-Display P4 still builds from the same tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Launches each catalog app (kAuditStore, generated by gen-radio-audit-apps.py
into the gitignored doc_radio_apps.h) and captures store_<id> for the
website's App Store page. Music gets two bench tracks and a play tap first.
Capture builds only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apps.html is generated from deploy/apps/apps.json (the catalog the devices
read) plus the site-only write-ups in deploy/site/apps-info.json, by
scripts/build/gen-apps-page.py, which deploy-site.sh now runs on every
deploy. Linked from the home page (Apps button + resource card), the SDK
guide and the sitemap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apps.json: one protreck row, ver=1.2, requires=sdk_ext (no seed: false)
protreck.lua:
- get_heading(): compute from raw compass x/y via atan(c.y,-c.x); remove
c.hdg (no such field) and GPS course fallback (M9-only)
- tap detection: record press={x,y} on "down", act on "up" only when
distance <= 12 px — swipes no longer trigger waypoint/chrono/timer/alarm
- alarm clock: use sys.datetime() (local time) instead of now_unix()+decompose()
- wada.timer.after: schedule_alarm() fires alarms while screen sleeps, rescheduled daily
- key events: left/right = tab nav, up/down mirror swipe actions
- canvas capped at 480×480 (Tanmatsu compatibility)
harness.lua:
- timer.after mock added
- protreck_tdeck fixture: h=240→196 (T-Deck actual resolution)
- protreck_chrono / protreck_timer / protreck_compass: send down+up tap pairs
- protreck_compass: compass mock returns raw {x,y,z} (not {hdg})
- protreck_deniv: alt_seq reordered so step 9 (first on_tick sample) is the
max value, matching the GPS call order through draw_status_bar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Builds on 1.0 (already in main). New in 1.1:
- Return to home: records the position of the first GPS fix after Start
and shows straight-line distance + cardinal bearing back to that point
each poll. Home is persisted in wada.store (trip_home_lat_e6/lon_e6)
so reopening mid-trip keeps the original departure point. Clears on Reset.
- Average speed: total_km / elapsed_s × 3600, live tile beside max speed.
- Min altitude: tracked alongside current altitude, persisted via
wada.store ("trip_minalt"). Clears on Reset.
- Speed derivation: when GPS speed_kmh is absent (non-M9 boards) speed is
derived from successive fixes; avg speed stays ≤ max speed by construction.
- Layout proportional to screen height: rows fill the space between the speed
block and the button bar — no overflow on T-Deck (320×240) or V4 (240×276).
- requires: sdk_ext (sys.gps is an ext-SDK function; errors on 2 MB V4 otherwise).
Harness: realistic fixtures (step × 0.0002° ≈ 22 m/2 s ≈ 40 km/h), home
persistence asserted, avg ≤ max by construction in all non-M9 scenarios.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
About (Build) and the boot log name the meshcomod release the core is:
"on meshcomod v1.18.0.1". release.sh reads it from the pinned core's own
examples/companion_radio/MyMesh.h, keeps the in-tree defaults in step and checks
every staged image carries it. Language store v26 (this build's new texts),
release notes for beta_95.
Bench (T-Deck, 1.18): device and self info, 404-contact sync, the companion CLI
(ver, board, get name/radio/tx/freq, unknown command), 4 packets received and a
zero-hop advert sent in 90 s, correct device time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pins move from core-v1.17.4 to core-v1.18.0, cut from the main meshcomod
companion-v1.18.0.1 was released from. The companion changes are ported from
meshcomod's rebase into wadamesh's own MyMesh:
- onCLICommandRecv: a contact's remote CLI command runs when the user granted
that contact remote CLI, and lands in the chat as a CLI message otherwise.
- CMD_RUN_CLI_COMMAND (66) / RESP_CODE_CLI_REPLY (29) and the local companion CLI
(name, pin, Wi-Fi, radio, af/dutycycle, rxgain, tx, rxdelay, path.hash.mode,
multi.acks, board, ver), also reachable from the serial CLI. Settings without a
prefs field answer "unsupported". NodePrefs layout unchanged.
- TXT_TYPE_CLI_COMMAND is accepted for sending; sendCommandData (and our queue-
preserving wrapper) carry the text type.
- Channel data is delivered to the app (onChannelDataRecv), unsolicited responses
(pushed telemetry) reach it as PUSH_CODE_BINARY_RESPONSE, and raw-data sends
decode multi-byte-hash paths.
Clock: 1.18 seeds an unsynced ESP32 clock at 1 Mar 2026, so ClockFloorRTC's
MIN_VALID_EPOCH follows it (a set equal to the seed must stay invalid).
Pager LR1121: the AGC reset re-applies the boosted-gain setting (new core arg).
IDF builds exclude the new CommonRadioPrefs.cpp (needs <target.h>/radio_driver).
The 2 MB Heltec V4 went to 100.0% with the CLI. Its built-in language table is now
one raw-deflated blob per language (895 KB -> 312 KB), inflated into PSRAM by the
S3 ROM on first use (~10 ms, ~75 KB for the active language): 91.7% flash. Checked
on a T-Deck forced onto the table (nl, hu, el) and by the post-link key check.
Builds: all 15 S3 touch envs, Tanmatsu (3,237,024 B), T-Display P4 SX1262 + LR2021.
T-Deck boots and runs on 1.18.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spectrum: stat tiles (peak, floor, our channel, busy), plot and waterfall drawn
at screen size (the lv_chart and zoomed waterfall were the page's render cost),
dark-to-yellow waterfall, peak hold, our-channel marker, Band/Channel and
Live/1 min/10 min chips. RF Monitor 1.4 (tiles, filled chart, typed feed, packet
detail card; the old type table did not match MeshCore payload types) and
Airtime 1.6 (big figure, tiles, duty line, rx/tx split).
Lua SDK: ui.panel, bold labels and a size-24 figure font, colors.warn/border,
app.on_audio (fires with the screen off), read-only wada.sd.list and sd: audio on
the SD_MMC boards (P4, Tanmatsu, Wio L2), caps panel/audio_events/epaper, a
"music" icon and an "audio" Store requirement. button:set() no longer writes
label fields into the button object (heap corruption, every earlier firmware).
The SDK page's input table described events the firmware never sent.
Music 1.0 (Store app): plays MP3/WAV from the card's Music folder, shuffle,
repeat, keeps going through the list with the screen off. Verified on the
T-Display P4 end to end.
Store: the update check also fetches the app catalog and badges the Store tile
with the number of installed apps that have a newer version. Baked Lua apps are
raw-deflated and inflated by the S3 ROM (14.6 KB back on the V4); the V4 language
table keeps only texts of apps it can run.
Fixes: the SD-card notice no longer fires when the card mounts fine (#667); the
Control Center clock keeps time while open (#666); M9 keyboard brightness (#664);
the battery widget's number no longer sits on a ring too small for it (Discord);
M9 shortcuts doc follows #669's composer order. New texts in all 14 languages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Asked for on Discord (prepper use: books and documentation offline on the card).
- The Reader opens a .txt or .md file on the card as a 16 KB window at an
offset carried in the URL (sd:/x.txt?o=49152), cut back to a line (or a whole
UTF-8 character), with Next page / Previous page / Back to the start links and
the position in percent. A book of any size pages through; Back works as usual.
- Files opens HTML on the card in the Reader (its links work there, so a folder of
pages browses offline) and text or markdown too big for the 8 KB editor; small
text files keep the editor.
- scripts/sdpack.py (standard library only): an EPUB becomes /library/<name>/ with
one page per chapter (long chapters split under 30 KB, linked in order) and a
contents page; .txt/.md are copied as they are; /home.htm lists everything, so
the Reader, which opens sd:/home.htm, starts on the library. Tested with a
generated EPUB.
ZIM (Wikipedia) files are not read on the device: their clusters are zstd/xz
blobs of 1-2 MB and the files run to tens of GB on FAT32; converting on a
computer keeps the device side small.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
beta_93 pinned the Heltec V4 to the beta_92 .lang files because the full table
no longer fit. lang_reach.py now works out which keys any source can look up:
a .lang key is kept when it equals a C string literal anywhere in the sources
(every #if branch, adjacent literals joined, LV_SYMBOL_*/UI_ICON_* and other
string macros expanded, escapes decoded as GCC does), or that literal with TR()'s
glyph prefix stripped, or a string in any Lua app. 1818 of 1925 keys survive; the
107 dropped look unused (old dropdown strings, texts the source has reworded).
The trimmed live table is 15.5 KB smaller than the pinned one (V4 4,025,365 B
used, 37.9 KB free) and carries every translation added since beta_92.
Guards: the generator refuses to trim when an input is missing (lv_symbol_def.h,
UITask.cpp, or under 80% kept) and leaves the header alone; after linking the V4,
--check-objects reads the strings the compiler emitted and fails the build if a
translated key among them was dropped (proved by dropping a live one). The
pre-build hook regenerates on source changes too, and the header is only
rewritten when its content changes. The pinned copy is gone (git history).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New env crowpanel_50_companion_radio_touch for the 800x480 ST7262 RGB panel,
GT911 touch and an SX1262 in the wireless slot. Board quirks follow Camillia-MT:
14 MHz pixel clock, internal-RAM draw band, PCA9557 (V1.0) / MCU (V1.1+)
backlight controller detection with GT911 reset at 0x5D or 0x14, NSS on GPIO8
(V1.2+), no SD/GPS/battery. Large-screen layouts and UI-size fonts; browser
file transfer to the internal tiles partition. Added to the upload helper,
release script, flasher manifests, site, DEVICES.md, README and NOTICE.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New version in deploy/apps/protreck/1.2/ (1.0 directory untouched).
- 5 tabs: ASTRO · CMPAS · CHRONO · TIMER · ALTI (was 4)
- Permanent status bar: local time (UTC offset from GPS longitude),
GPS sats, battery % — shown on all tabs
- ASTRO: 3 alarms A1/A2/A3; short tap = cycle slot, long press ≥500ms
= toggle on/off; swipe up/down ±15 min on selected alarm;
civil twilight via generalised sun_utc_dep() formula
- CMPAS (new tab): compass rose with cv:line rotating marks;
sys.compass() or GPS course fallback; tap = save/clear waypoint;
bearing + distance shown on rose
- ALTI: cumulative déniv+/− (0.5m noise threshold), climb rate m/h;
GPS sampling active on all tabs
- Harness: 10 scenarios ALL OK, worst tick ~13k / budget 100k
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Enable CAP_UI_SIZE and the font-only text-size presets (montserrat
12/14/16, 16/18/20, 18/20/24) for HAS_TDECK_PRO boards, compile the
18/20/24 px UI fonts for them, and size the status bar to match.
Docs list the T-Deck Pro and Max under Text size.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contact nicknames: TouchPrefsStore nick blob (48 x 38 B), display-only lookups
(contactShownName, threadShownName, senderShownName), Contact options >
Nickname on the shared text prompt (an empty OK clears it).
Control panel restyled in the launcher's tile language, with a Battery saver
tile; keyboard tooltips (navSetTip), an amber cursor and touch long-press
tooltips (the T-Deck touch read hid them on every poll).
T-LoRa Pager: M/C/A/S on Home, H/S tap and hold, Backspace = Back, N = jump to
new, held Enter = long press, per-tab focus memory, Power off confirm; LVGL
-O2 and IRAM, INT-gated keyboard, idle yield, status-bar setter guards, instant
tabs; PagerGps (u-blox software standby for rests, GPS rail cut when off),
panel sleep, amp off at boot, power-off parks the radio (NSS held), GPS, panel
and keyboard light.
Fixes: the DM duplicate check keys on the attempt and a path reset clears it,
the direct timeout masks the packed path byte, web remote hops (#636);
changelog over http (#638); Lua SDK send budgets and duplicate guard (#606);
GPS speed note (#613, CAP_GPS_BAUD_AUTO); emoji chat icon (#640); default
wallpaper (#626).
Heltec V4: Tetris, 2048 and Breakout unseeded and the built-in Lua stripped of
comments (identical bytecode), 27 KB freed; the baked language table is pinned
to beta_92's files (scripts/build/lang-pin-beta92) so it still fits.
Translations: 39 new keys in 14 languages, language store v24.
Site: theme, dark mode, aurora and guide refresh, SDK send limits, nicknames.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lib_deps takes RadioLib ^7.6.0, and a fresh fetch now brings 7.8.1. Its
LR11x0::config() wraps driveDiosInSleepMode in its own skip for LR1110
firmware below 0x0306, so the call sits one level deeper and the patch
script no longer found it. It failed closed, stopping every M9 build at
its pre-script.
The script now knows both shapes. The patch still matters on 7.8.1:
firmware 0x0306 and 0x0307 get past RadioLib's own skip, send the
command, answer CMD_PERR and would abort radio init with -706.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
The launcher's AppFS bootloader maps only the first 50 x 64 KB of an app
while it checks and starts it (appfsBlMmap in badgeteam/appfs 2.1.0).
beta_91 (3,329,568 B) and the first beta_92 build (3,331,120 B) were past
it, so neither would start on a Tanmatsu.
On the Tanmatsu the launcher updates the app (CAP_OTA 0) and the only OTA
slots are the launcher's own 2 MB ones, so the core's "ota start" upload
page and URL install could never work there. DISABLE_WIFI_OTA stubs them
("ERR: OTA not supported in this build"), and with them go ElegantOTA,
the async web server and AsyncTCP: the app drops to 3,200,864 B, 75,936 B
under the window.
tanmatsu-appstore-publish.sh now refuses a binary over the window, and
its PR text no longer has an em dash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frame rate: full-width PSRAM draw band (6 bands a frame instead of 14 on the
T-Deck), the still aurora drawn once and copied, LVGL at -O2 with its blend and
label loops in IRAM (scripts/build/perf_lvgl.py, 1 KB more loop stack), circle
and gradient caches, newlib memcpy. Rolled out to the M9, Attaky, RAK, Wio,
CrowPanel and V4-R8; the Pager gets the band only, the V4 the caches only.
Frame benchmark tour (DOC_PERF_BENCH) for measuring it.
Fixes: hop counts read from the packed path length (#621); Arabic shaping
rows patched in LVGL (patch_lvgl_arabic.py), Arabic 101 shift layer, kaf,
symbols, text sent as standard letters (#582); info popup colour runs that
never wrap (#602); keypad nav reaches the chat header (#623); the browser
opens the changelog (#622, gen-changelog.py at every site deploy); the
Theme background colour picker no longer edits the accent; flasher entries
for P4 units with factory ESP-AT Wi-Fi chips (#612, #619).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Home is a grid of arrangeable widgets (24 kinds, presets, a layout per
orientation, Lua app widgets via wada.widget); Unread, Chat and Mentions
get a large size. Theme > Background puts an aurora (still by default, or
moving), a colour, a picture or nothing behind every screen, with glass
cards and a smoked-glass top bar. A GPS icon in the bar, a GPS battery
saver (u-blox GNSS stop/start where there is no power switch), and a
battery saver that no longer waits for Bluetooth and Wi-Fi to be off.
Also: navigation bar position, every settings page on the redesign's
cards, a faster app drawer, slower screensaver drift for burn-in, text
wrapping at spaces only, the V4-R8 Store Apps tab, and SDK Test marked
extended-SDK only (keeps the 2 MB V4 at 99.8%).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every screen redesigned to the Oct 2026 mockups, on one design system:
palette, type roles (generated Montserrat faces with Lucide line icons on the
LV_SYMBOL codepoints), a theme apply-cb that styles every LVGL widget, button
kinds, list rows, sheets as action lists, toasts and key-glyph avatars.
Rebuilt: status bar, Chats (filters, + and menu), conversation (route header,
hop meta, "Heard by N repeaters", delivery states, pill composer), lock screen
(mark, clock, 24 h activity, unread cards), Constellation screensaver, Home
launcher and launcher sheet, Status page, grouped Settings home, animated boot
splash, and the message glance as a notification card.
New: optional PIN lock (salted SHA-256, keyboard / touch pad / number pad per
board, backoff that survives reboot), screensaver on battery with its own
duration, T-Deck text sizes, trackball hold to switch Scroll and Cursor (Scroll
default, prefs v69/v70), hold Settings tab for the control panel. M9 bottom
notices removed. Every screen measured for the Large and Huge text sizes.
Fix: adverts over 2/3-byte path hashes never reached the recently heard table
(packed path_len compared as a byte count); ADVERT_PATH replies now carry the
whole multi-byte path.
English placeholders for the 204 untranslated keys in every .lang file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The T-Display P4 ships EIGHT images: panel (AMOLED/LCD) x radio (SX1262/LR2021)
x C6 stack (esp-hosted/ESP-AT). The C6 axis is new and it is the dangerous one,
because the two builds are identical apart from the artifact name they carry,
and flashing the wrong one at an older unit used to boot-loop the board.
scripts/build/p4-release.sh builds all eight and VERIFIES each image against the
OTA name baked into it before accepting it. That is the same string the device
builds its self-update URL from, so a disagreement with the filename is exactly
the failure that bricks people -- the T-Deck Max self-updating to Pro firmware,
and this table has been wrong three times. A mismatch aborts the release.
It earned that check immediately: building the eight by hand produced two
mislabelled images, and every mechanism meant to prevent it was itself broken.
* build.sh fullclean has NEVER worked from a plain shell. It ran `cmake -E
remove_directory` above the esp-idf/export.sh that puts cmake on PATH, so it
printed that it had removed the tree, exited 0, and removed nothing. Now a
plain rm -rf, and it verifies the directory is gone instead of assuming.
This matters beyond tidiness: the hosted and ESP-AT builds SHARE a build dir
(the C6 axis does not change P4_BUILD_TARGET), the backend is read from the
environment at CMake CONFIGURE time, and CMake does not reconfigure because
an env var changed -- so a clean that silently did nothing yields an
incremental build of the OTHER variant, with no configure output to show for
it. That is precisely how the two mislabelled images were produced.
* Deleting CMakeCache.txt alone is not a clean either; it leaves CMakeFiles/
behind and the next configure dies in CMakeDetermineCCompiler.
* Detecting failure by grepping a log for "error:" misses a failed CMake
configure, which prints "Configuring incomplete" and no such line -- and the
previous application.bin is still sitting in the directory to be picked up.
Exit codes now.
gen-flasher-meta.py gains the four ESP-AT entries, labelled "older unit, ESP-AT
C6": someone choosing in the web flasher has no serial log and needs to pick on
something they can check.
scripts/run-host-tests.sh runs test/ as one command. Nothing ever did, which is
why test_touch_prefs_schema had been failing to COMPILE since v64 appended
report_ping and report_done_n without updating its sums -- and a static_assert
that does not compile is not a guard. That one exists to catch the mid-struct
insert that broke v44 and shipped the poisoned beta_57 layout. Repaired, with a
note where the next person appending a field will read it. 17 pass, 0 fail, 2
skipped with their reasons stated rather than quietly dropped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things made a full harness run untrustworthy, and together they produced a
false accusation against a community app.
wada.fs was stubs. append threw the data away, remove always said true, and
read, write and list were not there at all — although the firmware registers
all five (LuaAppHost.cpp). Any app that writes a file and reads it back
therefore failed here while working perfectly on device. wardrive 1.2 is
exactly that app: it errors on a nil fs.read at its first log flush, long
before it renders anything, and the emoji scenario then reported the repeater
name as unrendered. Read as "1.2 regressed the beta_70 emoji fix", which it
did not. It passes untouched once fs is real.
wada.fs is now an in-memory filesystem following LuaAppHost's semantics: read
returns the data plus the file's TOTAL size so a caller can window a growing
log, an offset at or past the end gives an empty string rather than nil, a
missing file gives nil, and list is sorted so a hash order cannot make a test
flaky.
The second: the gpscompass scenario list was acting as the dispatch default,
so an app with no branch of its own was run against tests written for a
different app. Six store apps have no branch (2048, airtime, monitor, nearby,
sdktest, snake), so a full sweep came back with six red apps as its normal
state — and a real regression would not have stood out against that. Those
apps now say they have no scenarios and run none. They are untested, which is
worth saying out loud, but that is not the same as broken.
A sweep of every store app is now 8 apps passing their own scenarios, 6
reported untested, 0 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>