The Settings group heading "Connections" had no row in any language, so it
stayed English (CONNECTIONS above Polish rows). The headings come from
kSettingsGroups through `for (const SettingsGroupDef& g : kSettingsGroups)
... TR(g.title)`, and i18n-audit.py's range-for rule only recognised
`for (auto& x : table)`, so the key never reached the audit. The rule now
takes any element type.
The audit then lists 18 more keys added since the last pass (the old-contact
purge, vibration, browser-build notes, the web location prompt). All 19 get
a row in all 15 files: real translations in de, nl, fr, es, it, pt-br and pl,
English placeholders elsewhere. i18n_builtin.h regenerated (V4 +3.9 KB).
i18n-audit.py: every file ok.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At the bigger text sizes (the Large/Huge presets on the T-Deck, M9, V4-R8,
T-Deck Pro, Pager; the large screens and the browser build always) the UI
swaps an 18/20/24/28 px Montserrat into g_font_12/14/16. That face is ASCII
only, so every accented letter came from the fallback chain, which stayed at
12/14/16 px: "Urządzenie", "Dźwięk", "Język" drew their ą ź ę visibly
smaller in Polish, and the same for Czech, Hungarian and Romanian. Only the
Tanmatsu spliced a size-matched Latin font (#129).
initTouchFontFallbacks() now gives every slot the extras at its primary's
own size on every CAP_UI_SIZE board: the full extras_12/14/16 (or the
Pager's extras_20/24) where one exists at that size, otherwise extras_lat_18/
20/24/28 (Latin-1, Latin Extended-A) in front of the biggest full extras for
Greek/Cyrillic/Arabic. extras_lat_18 is new; 18/20/24 are built under the
same board gate as the 18/20/24 px UI faces. Romanian's ș ț Ș Ț (U+0218-021B,
Latin Extended-B, 85 uses in ro.lang) were in no font at all and drew as the
missing-glyph mark: added to the extras, the Latin extras and the SemiBold.
Flash (vs the previous commit): heltec_v4_tft +2.2 KB (94.4%, the V4 has no
text-size setting, so only the Romanian glyphs); T-Deck +37.6 KB (91.2%),
M9 +37.5 KB (89.3%): extras_lat_18 and _24. The 28 px Latin font is only
referenced where a preset reaches 28 px (large screens), and the Pager (98%)
keeps its 20 px extras for its 18 px face instead of linking extras_lat_18.
Tanmatsu application.bin 3,126,480 B (ceiling 3,276,800).
Proof: the browser host build with pl.lang active, Settings before/after.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discord (gadgeteerza, ThinkNode M9, beta_100): no Central Africa zone, the
clock often half an hour behind on a Custom UTC offset, and "Sync clock from
system" with Wi-Fi on not always setting it.
Zones: the saved setting is an index into the zone table, and Custom used to
be "one past the list", so appending zones would have moved it. Custom now
keeps its id (16) forever and the new zones (Central/South/East/West Africa,
Egypt, Eastern/Western Europe, Moscow, Turkey, Arabia, Iran, Gulf, Pakistan,
Nepal, Bangladesh, Indochina, Singapore, Perth, Korea, Adelaide, Brisbane,
New Zealand, Argentina, Mexico City, Newfoundland) sit after it. The picker
shows its own west-to-east order and opens on the current zone.
Half an hour behind: not the offset. WadaNmeaLocationProvider::begin() kept
the parser's pre-rest sentence, so after a GPS saver rest the first loop pass
saw a "valid" fix, the 30-minute re-sync was due, and the clock was set to
the moment the receiver went to rest. begin() now clears the parser, and
UITask::setGpsPower(true) calls syncTime() so the core provider (V4 and
others) gets the same. test_gps_time_sync runs the real provider against a
receiver sending true UTC: the pre-fix provider set the clock 1801 s behind
after a 30-minute rest; now every set is within 1 s.
Sync from system: on the M9 the system clock is the device clock, so the
button copied a clock onto itself. And the Wi-Fi loop wrote only the FIRST
SNTP result into the mesh clock: SNTP's hourly re-syncs set the system clock
only, the PCF8563 kept its old value, and the hourly chip resync (#583)
pulled the clock back to it. Every completed sync is now written through
(SntpClockPush.h, test_sntp_clock_push), and the button with Wi-Fi up
restarts SNTP and says "Clock synced" when the answer lands.
test_time_zones: the legacy ids keep their zones, every zone's offset in
January/July/October, Egypt's DST edges, and all 65536 saved
(tz_zone, time_offs) byte pairs through the exact prefs path to localtime().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The device downloads a language into one buffer of at most 128 KB and reports
anything larger as "Server or connection failed". Every v30 file was over
it (Polish 149 KB, Greek 163 KB), so no language could be downloaded, and
seven were already over it in v29. deploy-apps.sh now publishes from a
staging copy in which pack-lang.py keeps the headers and the last row per key,
drops rows that equal the English key (the device falls back to the key
anyway), and, for a file still over 120 KB, keys no current firmware looks
up. A file over 128 KB aborts the deploy. The repo keeps the full files for
translators. Served v30: 72-107 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wada.sys.random full range no longer divides by zero, wada.ui.input Enter
submits on touch boards with an external keyboard, the OTA download ends
its HTTP connection when no buffer could be allocated, an accepted clock set
lifts the floor (a chip stuck at it no longer keeps the stale time), the host
build refuses removes with '..', and 10 new host tests plus headless scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
# scripts/run-host-tests.sh
# src/helpers/esp32/TouchPrefsStore.cpp
# test/clock_floor_fakes/helpers/AutoDiscoverRTCClock.h
newMsgImpl's chime/vibration decision moves into src/ui-touch/NotifyAlert.h
(same rules, a static_assert ties its slots to the prefs' TOUCH_SND_* and
TOUCH_CHMUTE_*); the P4's vibrate task takes its beat count from it.
test/test_notify_alert.cpp: Do Not Disturb silences both for every
event, the master Sound switch only the chime, Vibrate off only the buzz;
the per-event switches and channel mutes gate the buzz as they gate the
chime; a DM with its switch off does not fall back to the message slot; a
muted mention is a plain message; 1, 2 and 3 beats for message, DM and
mention.
test/test_p4_haptic.cpp runs the production P4Haptic.cpp against a
register model of the AW86224 on a fake Wire1 (test/p4_haptic_fakes),
each case in its own process: first use loads the 711-byte waveform at
0x0800 and verifies it, the FIFO thresholds match the vendor formula, a
retained SRAM is not rewritten and one differing byte reloads it; a buzz
plays waveform 1 looped with auto brake at the given gain for exactly its
duration and is stopped; 0 or negative durations touch nothing; an absent
chip stays off the bus after the first probe; a busy bus skips the buzz;
a chip that never idles does not hang; every transaction happens with the
I2C_2 bus held.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The home page gets a WADAMESH-TAK section after the install options: what the
Cascadia Mesh build adds (MediaMesh, Team, OTP messenger, detection, Sound
link, the MeshCore Open TAK app), with its flasher, user guide and source, and
a quick link in the icon rail. The question in the FAQ now points to it.
On a phone (760 px and narrower) one Menu button replaces the row of
unlabelled icons on the home page and the Home link in the other pages' top
bars: a sheet with every page and link, named and described, plus the light
or dark switch. menu.js holds the one list for all pages; the generated
changelog and apps pages include it from their generators. Wider screens are
unchanged. The Tanmatsu page's top bar said Lua SDK.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deploy-site.sh rebuilds changelog.html from every release-notes/beta_N.txt,
so draft notes for a build not cut yet went live with an unrelated site
deploy. A note is now listed only once its beta_N tag exists, and the deploy
fetches tags first so a tag made by gh release create is seen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
test/test_clock_floor.cpp runs src/helpers/ClockFloorRTC.h over a
settable hardware clock (test/clock_floor_fakes) with settimeofday /
gettimeofday faked, so the machine's clock is never touched. A chip
behind the floor or asserting 2043 gives way to the system clock when that
is ahead and plausible, else the floor or the core's seed as before; a
negative or implausible system clock is never used; a healthy chip whose
write takes is followed; a 20000-step random walk of both clocks never
steps back or past the plausible window; garbage sets are refused, a
>10 min backward set lowers the floor, a poisoned persisted floor is
refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
web/scripts/apps/inputlen.lua + web/scripts/lua-input.wada run against the
real LuaAppHost.cpp in the web host build: caps().input_len is true,
random(6) stays in 1..6 with every value seen, random(1, 1) is 1,
random(5, 3) is in 3..5, random(1) is 1, random() is a raw word; 250
characters typed into wada.ui.input(..., 300) come back whole on Enter,
100 typed with no max_len come back as 63; the full 32-bit range returns
integers.
The Lua harness mock now mirrors the current host: sys.random with the
new ranges (deterministic xorshift32), caps().input / input_len, and
wada.ui.input with its argument checks, the 63 default, the 1..2000
clamp and one prompt at a time, answered by answer_input(). Scenarios
inputlen_random and inputlen_input run the same app; every store app's
scenarios still pass.
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>
test/test_keyboard_layouts_hw.cpp compiles the production
KeyboardLayouts.cpp (LVGL names from test/kb_layouts_fakes) with the T-Deck
matrix decoder: Sym+W/E/R decode to 1 2 3 and type as themselves under
Italian, Spanish and Romanian, as do all ten digits and ) ! @ # $ % ^ & * (.
It reads the accent box's tables out of UITask.cpp and checks every letter
the old digit rows gave (à è ì ñ ă â î ş ţ and capitals) is offered after
its base letter, and that the box is on by default. Latvian keeps ā..ū on
the digits, Russian and Bulgarian map as before. It fails on the parent of
256b6b79.
A full dump of keyboardLayoutMapHwKey (14 layouts x 95 keys x shift)
before and after 256b6b79 differs only in the ES, IT and RO digit and
shifted-symbol entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test/test_tdeck_keyboard_detect.cpp compiles the production
TDeckKeyboard.cpp against a scripted keyboard controller on a fake I2C
bus. Probe: 71 71 71 71 71 goes legacy with the saved-setting evidence
(taken once), padded 71 00 00 00 00 and an 8-bit burst go legacy unsaved,
a key in column 1 goes raw. Raw path: the repeated 'q' and a repeated
space go legacy and type the one character, padded types 'q' unsaved,
q+e (01 01), 21 21, space+z (20 20) and 23 01 stay raw and type their
keys, 8 keys at once go legacy typing nothing, 7 stay raw, high-bit frames
are ignored, the setting forces legacy. Exhaustively over all 35 matrix
cells: no 1- or 2-key chord reads as a reply; the 25 3-key chords that do
are all column 0 with space or the row-6 key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The speed rules behind touchPrefsGetGpsBaud / touchPrefsSetGpsBaudFound
move into src/helpers/esp32/GpsBaudPrefs.h (same results, same single
write per change), so the test runs the real rules.
test/test_gps_baud_probe.cpp compiles the real WadaNmeaLocationProvider.h
over a fake UART whose receiver talks NMEA at one speed and reads as noise
at every other, with a fake NVS that counts writes. T-Deck default 38400
with a 9600 receiver: found on the first probe step, stored once as the
found speed (never as the user's choice), no reopen or write over two more
minutes or a GPS saver rest; the next start opens at 9600 and writes
nothing. M9 default 115200 with a 38400 receiver: 9600, 19200, 38400. A
picked speed is never probed past and nothing is stored; a swapped
receiver rewrites the found speed once; no receiver or an unlisted speed
never stores anything; a stop mid-probe still finds the speed; the
CAP_GPS_BAUD_AUTO path stores to the setting instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_c6_link_health includes c6_link_health.h, which lives in
tdisplay_p4/components/c6_at, not src/, so the suite failed to build it.
Its own build line already names that dir; the runner now passes it too.
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>