Some ThinkNode M9 cards shipped with a dormant Windows worm (Elecrow
security advisory, September 2026). An infected card seen since carries
autorun.inf in the root, launching xlfqf.pif on open, explore and autoplay
with random-junk comment lines in between: the Sality autorun pattern.
- SD Scan store app (deploy/apps/sdscan/1.0, requires "sd", not seeded):
walks the card a small page per tick, lists what it finds and why, and
removes it after a confirmation screen with Cancel first. It says on
every screen that it only removes files it recognises and that
formatting the card is the safe fix. On older firmware it still finds
threats by name but cannot remove them.
- Firmware: wada.sd.check(path) and wada.sd.remove(path), plus paging for
wada.sd.list(path, start, max) and caps().sd_clean. What counts as a
threat lives in SdThreat.h: autorun.inf, Windows program, script and
shortcut extensions, or a real MZ+PE header under any name. remove()
classifies again in firmware and refuses anything else, so no app can
use it to delete tiles, backups or chat history. It clears read-only,
hidden and system first, because FAT refuses to delete a read-only file.
- A warning when a card with Windows malware in its top folder is mounted,
at boot or on insert, offering SD Scan (or the Store).
- Tests: test/test_sd_threat.cpp, and SD Scan harness scenarios including
the real infected card's root. Removal checked on a T-Deck.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It was listed as fully supported, but it is community-maintained and not
tested on every release, and every build before beta_80 fetched the wrong file
when updating itself. The badge now says so, in the same form as the V4-R8
card, and points owners on older builds at the website to update.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds LilyGo_TDeck_Pro_companion_radio_touch to release.sh (as
wadamesh-tdeck-pro), a flasher manifest labelled "(experimental)", and a
website card following the V4-R8 pattern: experimental in the heading,
"Partially supported", beta only.
The card says plainly what is known: contributed through #469, all the main
hardware works, but it has been tested on a single v1.1 unit, v1.0 units are
the least proven, and the e-paper display redraws rather than updating
instantly.
The site is not deployed with this commit: the card's install button points at
manifest-tdeck-pro.json, which only exists once a beta containing the board is
published.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The MeshCore founders' givealittle fundraiser has closed (reported on
Discord by jason000008), so the banner was sending visitors to a page that no
longer takes donations. Removed the markup, its styles and its dismiss script
together.
The corner button rails need no change: .fab-rail already falls back to
top:16px, which is exactly what the banner script set once it was dismissed,
so everyone now sees the layout returning visitors already had.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A release with twenty-odd notes pushed the whole install flow off the screen, so
the box that is meant to be a summary had become the page.
It is a native <details> now, collapsed on load, reading "See what's new in
beta_78 - 24 changes". The count matters: a bare "click here" asks people to
gamble a tap, while a number lets them decide whether they care. Native rather
than scripted so it opens with the keyboard, works without JavaScript, and does
not need a click handler; the JS only fills the count and forces it closed on
each channel switch, so flipping stable/beta cannot leave a stale panel open.
The default disclosure triangle is hidden and replaced with one that rotates,
because that marker renders differently in every browser.
Markup verified as balanced and the element confirmed to carry no "open"
attribute, which is what makes it start collapsed. I could not render it in a
browser to check visually: the harness refuses to navigate to localhost, and a
file:// page is served as a static snapshot with no JavaScript, so the count and
the collapse behaviour are unverified on screen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things Jade reported, the first of which is blocking an app submission.
Missing keys on the T-Deck. A Lua app there never received A, P, Q, Enter or
Backspace. The rule that withholds them exists so an app can never swallow its
own exit, which is right on a board where a reserved key IS the only way out.
On the T-Deck it is not: an app page is closed by tapping the back bar, and the
app host is not in the popup registry, so those keys closed nothing while an app
was running. They were simply dead inside every app. They are now forwarded.
The M9 keeps its reservation, because its reserved keys are sentinel bytes for
Back and Home that are never typed text and it has no touch to tap out with, and
the Pager already reserved nothing. That is why this was T-Deck-only.
The trackball click. With an app open the trackball branch forwarded motion and
explicitly discarded the button, so an app could be steered but never clicked.
It now delivers the same synthetic centre tap the touchless boards' select key
sends, on the press edge: the flag is a per-frame "held" level, so forwarding it
raw would have fired a tap every frame the button stayed down.
Repeat counts. The firmware already counts repeaters heard rebroadcasting an
outgoing flood, keyed on a fingerprint taken at transmit time; it is the refresh
glyph on a sent bubble. wada.mesh.send() now returns that fingerprint as its
second value on success, and wada.mesh.repeats(fp) reads the count back. Additive,
so an app reading only the first return is unaffected. The count grows as repeats
arrive, so it is polled rather than read once, and a direct message has none
because it is not flooded. Documented on the SDK page.
Built on all nine S3 envs and both ESP32-P4 targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The PR added the board and its env, which makes it build. It does not make it
ship. Three lists decide that and none of them knew about it: release.sh ENVS
(what gets built and named into a release), gen-flasher-meta.py BOARDS (which
manifests the web flasher gets) and the flasher page itself, which lists boards
by hand.
Without these the board compiles cleanly forever and never appears in a release
or on the flasher, which looks like nothing is wrong.
Named wadamesh-wio-tracker-l2, its own Seeed Studio section on the flasher,
marked beta-channel-only like the Attaky was on arrival, since stable is still
beta_65 and this board has never been in a stable build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The SDK documented building widgets in on_open() and gave no way to take them
down, so an app with a second screen drew it on top of the first. pisti87 hit
this building a multi-screen app and had been working around it with buttons;
he also noted that ui.list() has clear() while the page it sits on does not.
The reason this is not just lv_obj_clean() is the handles. WidgetUd holds a raw
lv_obj_t*, guarded only by `if (u->obj)`, so a Lua variable still referring to a
label from the screen just cleared would sail past that check into freed memory.
Every handle now records the generation it was created in, clear() bumps the
generation, and the accessors null the pointer of any handle from an older one.
The null guard that every method already has then turns a stale call into a
no-op. Given the day this codebase has had with use-after-frees, a clear() that
invited one would not have been worth shipping.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both reported by Jade, and both were arbitrary caps rather than real limits.
wada.mesh.contacts() scanned the first 200 contacts, returned at most 100, and
said nothing about having truncated. On a device holding 2000 contacts most of
them were simply unreachable. It takes an offset and a limit now, defaulting to
the old 100 and capped at 250 per call, because every entry is an eight-field
Lua table and building thousands in one go would exhaust the per-app heap. The
window moves, so everything is reachable by asking more than once, and the new
wada.mesh.contact_count() means an app can page without probing for the end.
The app source limit was 64 KB, which an ordinary program can reach. It is
192 KB now. The source sits in PSRAM only until luaL_loadbuffer has compiled it,
so this is a transient allocation, not a per-app cost, and the compiled chunk
still has to fit the 256 KB app heap. The Store's download buffer was the other
half of that ceiling and moves with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cvhviz. The M9's magnetometer was documented on the board and nothing had ever
talked to it. The driver is written from the datasheet's register map rather
than SensorLib, whose setOutputDataRate() writes the ODR into the OSR bits, and
the axis orientation is measured on hardware at four headings rather than
inherited from a declaration Meshtastic marks unverified and never uses. The
+-32 G range looks absurd for a 0.5 G planet until you measure the board's own
hard-iron bias at about 7x Earth's field.
Also carries several fixes found while testing on hardware: every Lua app opened
on a white page on keypad-nav boards (the focus highlight harvested the app body
as a target and reverse-video filled the page), a use-after-free in the Lua net
worker when an app closed mid-request, an unfreed http_get buffer, canvas pixel
buffers GC'd while LVGL still drew from them, one RTC I2C read per contact, and
map re-open costing 2.5 s on every visit.
Three changes on merge:
- The map tile-keep gate read `total && total < 4 MB`, so a board reporting
zero PSRAM -- the most constrained case there is -- landed on the roomy side
of the test and kept its tiles. Dropped the non-zero guard.
- gpscompass is 55 KB of Lua, more than every other app combined, and it wants
a magnetometer the seeded boards do not have. The author deliberately left
it out of lua_builtin.h; that intent now lives in the catalog as
"seed": false rather than in whether someone remembers to regenerate, since
the generator runs from a pre-build hook as of this branch.
- consoleModeToggleCb was defined inside a !HAS_TANMATSU region while the
Settings row that binds it compiles on every board, so the Tanmatsu link
broke. Moved it out. The console boot path is gated on CAP_CONSOLE alone, so
the switch now does what it says there too.
Built on all seven S3 envs plus both ESP32-P4 targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pisti87 reported a long list of text that stays English whatever the language,
and said the strings were in his language file and still did not appear (#257).
Both halves are true, and the reason is the audit.
The extractor only ever recognised TR("literal"). Three very common shapes were
therefore invisible:
mk_row_btn("Reload tiles in view", cb) // helper TR()s its parameter
for (auto& r : rows) TR(r.label) // literal lives in a local table
TR(contactsSortOptName(m)) // helper returns one of several
All three translate correctly at runtime, so the source looks properly wrapped.
But the literal at the call site was never emitted as a key, so it never entered
a .lang file, so no translator could ever supply it -- and adding it by hand
did nothing, because the audit's key list is what the files are checked against.
That is 51 strings across the map options sheet, the sort sheets, the contacts
filters and the home launcher.
The audit now understands all three, plus tr("...") in the Lua apps, and the
newly visible keys are in all thirteen files as placeholders so translators can
see them. 1017 keys, up from 966.
Four strings were genuinely raw and are now wrapped: the reader's idle status,
the Discover empty feed, the crash-report export button and Paste (move/copy).
Lua apps had no way to translate anything at all, so every built-in was hard
English regardless of the device language. wada.sys.tr() gives them the same
table the interface uses; airtime 1.4 is the first to use it, with the
`sys.tr or identity` fallback so it still runs on older firmware.
Two more instances of the drift this issue is really about:
- gen-lua-builtin.py read out/firmware/apps/, which nothing writes -- the
deploy rsyncs deploy/apps/ straight to the VPS. So the mirror was stale and
the two apps added in beta_68 were never baked in: boards that cannot reach
the Store shipped without them. It reads the canonical directory now, and
regenerates from the same pre-build hook as the language table.
- Baking a row whose translation equals its key does nothing, since TR()
returns the key on a miss. Skipping them takes the header from 1.11 MB to
939 KB and gives the V4 back 16 KB of flash, which matters at 89%.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two small conflicts, both additive on each side: upstream added ui.text_w /
ui.text_lines and a "measure" capability while this branch added the panel
colour and the compass/accel caps. Everything from both is kept and the table
hints match the merged key counts.
GPS Compass now uses ui.text_w where the firmware offers it and keeps the
character estimate as the fallback. That estimate is what once put the heading
digits off centre -- it counted UTF-8 bytes, so the degree sign read as two
characters -- and measuring removes the guess entirely, for the dial's centred
text and the satellite meter's reserve alike.
M9, V4, V4-R8, T-Deck and Pager all build; the app harness passes, including
the tilt check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both sensors' 1 Hz debug lines were still on from the axis measurements; off
now (the flags stay, they are how the next board gets measured). Documents the
QMI8658 in M9_PORT.md -- the three attitudes and what each proved, the ADDR_AI
trap, and why tilt compensation was needed at all -- and corrects the
calibration instruction from "tumble it" to "rotate it in one place", which is
the distinction that was corrupting the fit. SDK page gains wada.sys.accel()
and wada.sys.keep_awake().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* #305 the home Wi-Fi status was decided from WiFi.getMode(), not from what the
user asked for, so anything leaving the radio in STA made it announce
"Starting…" indefinitely with Wi-Fi switched off. Gate on
wifiConfigGetRadioEnabled().
* #304 the Wi-Fi and Bluetooth status strings were raw literals and stayed
English everywhere. Wrapped in TR(). Most already had translations sitting
unused in the language files, so this costs translators nothing for
"Connecting…", "SSID not found", "Off" and "Built-in"; only "Starting…",
"Auth failed", "Link lost" and "Init…" are new keys.
* #257 "Built-in" had two code paths and only the one you do NOT see went
through TR(), which is why it looked inconsistent to the reporter.
* #307 the theme-colour buttons were fixed at 124/84 px, measured against
"Save & restart" / "Reset". Hungarian's "Mentés és újraindítás" ran off both
edges. They size to their label now, in a wrapping row, so an over-long pair
drops to two lines rather than clipping.
* #306 the contact action sheet decided scrollability from a hand-maintained
grid_items tally that the location-sharing button (#266) was never added to.
When the tally is short the body is left unscrollable and the overflow is
unreachable. Decide from the height the buttons actually reached instead, so
the tally can drift harmlessly.
SDK: wada.ui.text_w / text_lines. An app could ask how tall a line is but not
how wide, so anything laying out its own rows had to guess whether a string
would wrap; guessing wrong draws the next row on top. That is exactly what
happened to the SDK Test app (fixed separately as store version 1.4, with
Nearby hardened the same way).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
beta_68 added named timers, so pausing only on_tick left the same problem one
API over: a repeating timer polling a sensor kept running into a dark screen.
One-shots still fire -- those are scheduled app logic, and skipping one would
drop the work rather than defer it, since the slot is released around the call.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
beta_68 expanded the Lua SDK (map, lists, packet delivery, discovery, private
messages/rooms, native crypto) across the same files as this branch, so four
files conflicted. Nothing was dropped from either side:
- wada.sys.gps(): both widenings merged into one binding. Upstream's
fix_time / lat_e6 / lon_e6 and our speed_kmh / course now share a signature,
and our stricter gate wins -- the call returns nil when the user has GPS
switched off, not just when there is no fix.
- Altitude is upstream's `alt_m` alone. The resolution first carried `alt`
beside it to protect a shipped app, but gpscompass has never been published
to the store (it exists only in this branch and on a bench device), so
carrying a duplicate key into the API forever was the wrong trade: the app
reads alt_m instead.
- sysCaps() carries all twelve feature flags (upstream's seven, our compass,
the four originals) with a matching table hint.
- wada.geo (upstream) and wada.sys.compass (ours) both survive; upstream's
"no board has a magnetometer" note is corrected in the code and on the SDK
page, since the M9 now does.
- hostTeardown frees upstream's new POST payload buffer as well as the fetch
buffer, under the same in-flight guard.
- The catalog keeps all three new apps: upstream's wardrive and nearby, ours
gpscompass (8 total, every referenced file present).
M9, V4, V4-R8, T-Deck and Pager all compile; the Lua host harness passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every installed Lua app drew the same generic play glyph in the drawer, and
the manifest's "icon" -- promised in LUA_APPS.md since the plan was written --
was never parsed. It is now read from <id>.json and mapped to a glyph by NAME
(gps / radio / chart / game / ...), not by codepoint: a name is reviewable in
a store submission, the device's JSON scanner only takes quoted strings
anyway, and an app can never ship a glyph the UI fonts lack -- anything
unrecognised falls back to the generic symbol. A bare side-loaded .lua has no
manifest and keeps that symbol too.
GPS Compass declares "icon":"gps", so it now shows the location pin the Map
tile uses.
Also corrected deploy/site/sdk.html, whose manifest table documented
"version", "min_api", "description" and "boards" -- the device has never
parsed any of them -- and described icon as "a single character".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Lua app page painted itself 0x0E1216 while the rest of the firmware
paints pure black (COLOR_BG), so every app read as a lighter panel floating
over the UI. The page is now black and wada.ui.colors gains `panel` for the
raised surface an app draws on top of it -- the compass dial uses it, so the
instrument stands out instead of the page doing it.
Rotate/flip are gone from the user's side. They existed because the QMC6309's
axis orientation on the M9 is undocumented, which is not the user's problem to
solve by trial and error. `A` now does the whole job: whether the heading runs
clockwise or anticlockwise follows from which way the sensor's Z axis faces,
and that shows in the sign of the vertical field -- Earth's field dips down
north of the magnetic equator and up south of it -- so with a position (a fix,
or the node's last known one) the app reads the handedness off the sensor and
the press only has to set the offset. `F` stays as the fallback for a flat
field or no position at all.
Layout: stats column on the left, dial on the right (its status line above
it, the hint centred along the bottom).
The host harness now tumbles the simulated device during calibration rather
than spinning it flat -- min/max on an axis that never moves subtracts the
true field along it, which is precisely the case the handedness check has to
detect and refuse, and the flat mock was hiding it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Layout, from on-device feedback: the magnetometer/heading-source line moved
from the stats column to a centred line over the dial; the stats panel now
starts at the top of the column, and the rows that freed up went to the
target -- name, range + bearing, how far to turn ("56 deg right", "ahead")
and when the contact was last heard. The key hint is centred along the
bottom edge of the view and spells the actions out ("C calibrate O rotate
F flip <> target"). The heading's DIGITS are centred with the degree sign
hanging off their right edge, so the number does not appear to shift as the
reading crosses 100/200; the width estimate also counts characters rather
than bytes now, which is what put it half a glyph off (the degree sign is
two bytes in UTF-8).
Host: label:width(px) takes an optional alignment ("center"/"right") -- an
app cannot measure glyphs, so this is the only way for it to centre a line
exactly. Also excluded the app ROOT from keyboard-nav focus: excluding only
the body moved the reverse-video highlight up one level instead of removing
it, which is why the page was still white.
sideload_app.py retries fput/fend as well as fadd -- the same UART byte loss
that garbles a long line can garble a short one ("Error: unknown command").
Calibration now reports the measured field strength in its toast, which is
the number that says whether the calibration is any good (Earth: 0.25-0.65 G).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ThinkNode M9's microSD is soldered on, so the SDK's "drop it on the card"
route does not exist there and the Store can only fetch from its own host.
Three console commands write into the same /apps (or /lang) the Store uses:
fput /apps/<name> open (truncate); names [A-Za-z0-9._-]
fadd <off> <len> <sum> <base64> append a chunk, every field verified
fend close
Same physical-access trust level as the existing "rm"/"erase", narrower scope
(two directories). DataStore gains a root-aware mkdirRooted(); the CLI line
buffer grows from 80 to 200 bytes.
Why the chunks are self-checking and short: the UART interrupt is not
IRAM-resident, so while the loop is inside a flash-cache pause only the
128-byte hardware FIFO buffers console input and the middle of a longer line
is lost -- observed on the M9 as a 197-char line echoed back as 120. Offset,
decoded length and byte sum reject a damaged line; the host re-sends. The
host script also waits for "[BOOT] ui ready" because the CH34x bridge resets
the board whenever the port is opened and the console is not serviced before
the UI is up.
Verified on the M9: gpscompass.lua (19055 B) + .json pushed, sizes confirmed
by the device and by `ls /apps` after a reboot. M9 and V4-R8 envs compile.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Firmware
- variants/thinknode_m9/M9Compass.{h,cpp}: QMC6309 at 0x7C on the peripheral
bus. Probe by chip id, soft reset with the explicit clear, CTRL2 0x20
(50 Hz, +/-32 G, set/reset on), CTRL1 0x61 (normal, OSR 8/8), read-back
verified. Synchronous read from the Lua host bridge, 1 s cache, lazy
re-probe while the rail-powered part is still in POR, OVFL kept + flagged +
logged. HAS_M9_COMPASS=1 in the env -> new hardware gate CAP_COMPASS.
- src/helpers/WadaNmeaLocationProvider.h: Wadamesh-owned copy of the core
MicroNMEALocationProvider that also exposes RMC speed/course (the core
keeps its parser private; no libdeps patch). M9 target.cpp builds on it,
HAS_GPS_MOTION=1, wadaGpsMotion() for UITask.
- wada.sys.gps(): + alt (m), + speed_kmh/course where the board provides
them (course only while moving -- an empty RMC course parses as 0), and
nil while the user has GPS switched off. wada.sys.compass(): {x,y,z,ovfl}
Gauss, sensor frame, uncalibrated, registered only where CAP_COMPASS;
caps().compass. Calibration, axis mapping and the heading maths live in
the app so they can be adjusted per user without a firmware cut.
- Host: luaHostContactAt reads the RTC once per contacts() walk instead of
once per contact (an I2C transaction each on the M9); pressCb reports
press coordinates in body content space (scroll offset folded in).
App
- deploy/apps/gpscompass/1.0 + apps.json: rotating rose with a fixed index,
live fix readout, bearing/range to a selected contact, magnetometer
heading with hard-iron calibration (C), frame rotate/mirror (O/F, the
M9's sensor orientation is undocumented), GPS-course fallback on every
other board, saturation warning. Not baked into lua_builtin.h on purpose
(CAP_BUILTIN_LUA_APPS also removes the Store > Apps tab).
Verified: M9, V4, V4-R8, T-Deck compile (M9 flash +2 KB); two adversarial
review passes, all confirmed findings fixed; host Lua harness (vendored
Lua, LUA_32BITS) -- calibration recovers a simulated bias exactly, heading
error 0 deg, worst tick ~10k of the 100k budget. Hardware validation list
(axis orientation, bias magnitude, 0x7C ACK) in M9_PORT.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The four capabilities that kept third-party apps a sketch of a built-in one:
* wada.map the firmware's own tiles, projection and cache inside an app
page, with its own capped pool so it never evicts the Map tab's
* wada.ui.list the missing "pick one of N" widget; rows are real buttons, so
keyboard and trackball nav walk them for free
* on_packet each frame delivered once instead of polling a 16-deep ring,
which sampled rather than observed
* wada.mesh.discover the active zero-hop probe, behind its own permission
because it spends every neighbour's airtime, not just ours
Plus the surface those need to be useful: packet identity reported only where
the frame actually carries it, exact micro-degree coordinates (Lua is built
LUA_32BITS, so its floats were quietly costing a metre), altitude and satellite
time, wada.geo, wada.ui.input, named and one-shot timers, http_post, windowed
fs.read, and wada.sys.env on a hardware gate rather than the memory one.
Fixes:
* Both ESP32-P4 targets could not link. g_wifi_last_disc_reason was defined in
src/main.cpp, which the IDF builds never compile, so all nine S3 envs stayed
green while Tanmatsu and T-Display P4 were dead.
* Map zoom level was invisible in +/- buttons mode; the readout was hidden with
the slider it was anchored to.
* Hungarian and Dutch held each other's "No SD card" translation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither was documented. The Discover app has been in since beta_47 and the user
guide never mentioned it, so the only way to find out what it did was to open it.
The section leads with the thing that makes it different from everything else in
the firmware: Discover ASKS rather than listens. It broadcasts a request that
neighbouring repeaters answer directly, so a reply proves reachability from the
exact spot you are standing, which is also what makes the wardrive data mean
something.
Explicitly separates two names that are one word apart and are not the same
thing: the "Discovered" list in the Contacts overflow is passive, built from
overheard adverts, while the "Discover" app is active. Cross-linked from the
Contacts section where that confusion actually happens, and from the Map section
where the coverage dots show up with no explanation of where they came from.
Covers what a user needs to actually do one: SD card in, wait for a GPS fix, open
Discover and leave it open, then read the map. States the sampling rule (~15 m of
movement, or 20 seconds standing still) so the behaviour is predictable rather
than mysterious, and says plainly that it transmits continuously while open, so
it costs power and airtime and is not something to leave running all day.
Documents the two real limitations rather than leaving them to be discovered: the
map keeps only the newest 160 samples in memory, and the CSV on the card is the
only part that survives a reboot; and there is no on-device viewer for that log,
so reading it back means taking the card to a computer.
Live on wadamesh.com/docs.html.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was no way for anyone but me to put a self-built wadamesh on a Tanmatsu.
The repo carried tanmatsu/build.sh and nothing that could install what it built:
the AppFS writer lived only on my machine, so a fresh clone produced a binary and
no path onto the device. Anyone trying would land on `idf.py flash`, which
replaces the launcher OS.
tanmatsu/tools/ now carries the loop, generalised out of my local copy:
fetch-appfs.sh one-time, clones badge.team's esp32-component-appfs
dump-pristine.sh one-time, dumps YOUR device's AppFS partition as the baseline
tan_deploy.py builds the write-images and works out the changed sectors
tan_flash.sh detects the P4, writes app then metadata, verifies the commit
sermon.py non-resetting serial monitor
Three things had to change before this could work for anybody else. Absolute
paths to my checkout are gone. The hardcoded MAC of MY Tanmatsu is replaced by
detecting whichever port answers as an ESP32-P4, which is also more robust since
the board exposes two ports whose names move between replugs. And tan_flash.sh no
longer invokes tan_deploy.py twice: the second run would have read the metadata
the first had just written and bumped the version an extra time.
appfs.py is deliberately NOT vendored. It is badge.team's and the copy in
circulation has no licence header, so it is cloned instead. dev_appfs.bin is not
committed either: it is 8 MB and device-specific, and using someone else's dump
risks overwriting apps you have, since the deploy writes only what differs from
it. Both are gitignored.
Written up in TANMATSU_SIDELOAD.md and as a page on the site, linked from the
user guide, the homepage resources section and the README. The guide leads with
the model rather than the commands, because the failure that matters is not a
typo, it is not knowing that this board runs wadamesh as an app under a launcher
and that flashing it normally destroys that launcher.
Not re-verified on hardware: the tooling is the same loop that has been deploying
to my Tanmatsu, with the paths and device detection generalised. The first person
to run it on another machine is the real test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page said only "there is an instruction budget: an app that spins forever is
stopped", which is why an app developer had to ask what it actually is. Now there
is a section answering the four questions that were asked: 100,000 Lua VM
instructions per callback, counted as VM OPCODES by Lua's own LUA_MASKCOUNT hook,
re-armed per callback rather than per session, 5x for on_open, and native wada.*
call time does not count against it.
It also says what to do when you hit it, because the honest answer is usually not
"optimise your Lua": a pure-Lua HMAC genuinely exceeds 100,000 instructions and
the fix is wada.crypto, which the section links to. The alternative, splitting a
long job across ticks using state on the app table, is spelled out too.
Audited the whole page against the firmware afterwards rather than trusting that
I had covered everything: every one of the 50 registered wada.* names and all
five lifecycle callbacks now appear on the page. The stale "ext" list in Sandbox
limits also gained send_dm.
Live on wadamesh.com/sdk.html.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pre-release tidy-up, so the same mistake as last week does not repeat: the SDK
page described a lifecycle the firmware never had, and shipping an expanded SDK
against a page describing the old two-permission model would be the same thing
again.
sdk.html now documents wada.mesh.send_dm and wada.mesh.channels, the m.kind field
on on_message, all four permissions with why they are four rather than one, and a
new wada.crypto section including why it exists (pure-Lua HMAC does not fit the
instruction budget) and why it is on every board rather than ext-gated.
i18n: 11 keys were missing across all 13 languages, six from the SDK work and
five from PR #287 which added strings without lang entries. All covered; audit
reports 924 keys with nothing missing, builtin fallback regenerated, published as
language v14 and the store is live.
Also removes an em dash from "Wi-Fi off, new map areas can't download", new in
#287 and not yet translated, so the key could be corrected for free.
All 8 S3 boards build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It reads "Save FAIL" permanently on the Tanmatsu, so on that board the one
indicator meant to make a sick store impossible to miss was instead crying wolf
at every user on every boot. An indicator that is wrong on a whole board is
worse than no indicator: it trains people to ignore it, and it is the first
thing they ask about.
Removed rather than board-gated. Everything it showed, and more, already lives
on Settings > About > Chat store: the backend in use, the segment count and byte
total, when history last saved, and the exact stage plus errno of a failing
save. The chip was a shortcut to a question most people never need to ask.
Also removes the now-unreachable homeStoreChipText() and the per-tick refresh
that ran on every home-screen frame.
Docs updated in the same commit: the user guide had a whole section explaining
how to read the chip, which would otherwise describe something nobody can see.
Replaced with a pointer to the About panel that holds the real answer.
All 8 S3 boards build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
These four destinations only existed as the small round buttons floating at the
right edge. People were not finding them, so the user guide, the SDK, the offline
map tiles and the issue tracker were effectively invisible to anyone who did not
already know they were there.
New "Docs, tools and help" section between the install steps and the community
videos, with a card each: what it is, why you would want it, and where it goes.
Same four icons as the buttons, so once you have seen the section the rail reads
as a shortcut to it rather than as unexplained decoration.
Reuses the existing card look; the only new CSS is scoped to .rescard/.rescards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The published SDK page was not merely out of date, it was wrong about the first
thing an app author does. It said to "define the callbacks you need as globals"
and named them on_start / on_stop. The runtime has never called those: an app is
a table you build and `return`, and the fields are on_open(w,h), on_tick,
on_input, on_message and on_close — which is what every app in the Store actually
does. Following the published example produced an app where nothing was ever
called and no error appeared, i.e. a blank page. That cost me an hour today
building SDK Test 1.0, and I changed my app instead of noticing the docs were
wrong; anyone else writing an app hit the same wall with less to go on.
Also corrected against the shipped code: ev.name does not exist (it is ev.key,
with ev.code alongside), and label/button take explicit coordinates rather than
the flow-layout signatures the page listed.
The intro's claim that "API v1 is read-only where the mesh is concerned… it
cannot transmit" stopped being true when wada.mesh.send landed, so it now
describes the actual guarantee: transmitting exists, and it stops and asks the
user by name first.
New material for beta_64: sys.epoch/datetime/beep/caps/battery/gps, the whole
wada.fs section, mesh.send, app.on_message, a Permissions section covering the
two separate grants and Settings > App permissions, and sandbox notes for the
things that actually bite — the ext calls are absent on small boards (check
sys.caps().sdk_ext), and the rate limits return false,"too fast" as a normal
answer rather than throwing.
Every signature here is taken from the registration block in LuaAppHost.cpp or
from SDK Test, which was verified green on hardware — not from the old page.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@oumike's #263 replaces the save chip's floppy icon with words, which is the
right call — two people independently read the icon as ambiguous, and one of
them was the maintainer. Two gaps came with it, both fixed here rather than
blocking the merge:
- "Saved %s", "Save FAIL x%u" and "Save migrating" were bare English
literals. The chip previously had no words at all, so it was language
neutral; as written it would have been permanently English in all 13
languages and reopened the drift closed in d4ade2e. Wrapped in TR() and
registered in every .lang file as empty rows. audit-lang.py: 0 missing,
0 unsafe.
- the user guide, published yesterday, describes a floppy-disk icon and lists
the states as bare times. Reworded to match what the firmware now draws —
docs and UI have to move together or the guide is worse than none.
"Save FAIL x%u" and "Saved %s" carry printf placeholders, so they are covered
by the format-safety guard added for #258: a translation that reorders or drops
them falls back to English instead of misreading the stack.
All 8 S3 envs build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was no written path for contributing a Lua app. pisti87 wrote a 2048 game
and ended up asking another contributor to upload it for him, in the comments of
an unrelated issue about map panning — which is the symptom of a missing
procedure, not of anyone doing something wrong.
deploy/apps/README.md is now the canonical process: file layout, the immutable
version-directory rule, the apps.json catalog row, and how to open the PR. It
also says an issue with the .lua attached is an acceptable way in, because
requiring git fluency would cost us apps from people who can clearly write them.
Both this and the site's Apps section state up front that every submission is
reviewed and safety-checked before it is added, and what is actually looked for:
anything touching node identity, keys or channel secrets (the one that gets a
hard no), flash-write patterns that trigger GC and stall both cores, blocking the
shared UI/mesh loop, unbounded memory on a 2 MB V4, and unrequested transmits.
Those criteria come from bugs this firmware has actually shipped, so they are
concrete rather than boilerplate.
Written to be inviting about it — the review exists because apps run on other
people's radios, not to gatekeep, and a rough app that works beats a perfect one
that never gets sent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The floppy-disk icon and time in the traffic card's legend row read as a clock,
and nothing on the page said otherwise. Document what it actually is — when
chat history last reached storage — and all five states, including the -ND
suffix that marks a store which stopped saving and the red FAIL / amber
migrating cases, with the pointer to About -> Chat store for the detail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The audit found three keys the Store work added or renamed since v7 -- Store,
the out-of-memory message and the Languages hint -- untranslated in all 13
languages. Filled, using each language's own word for the Use button so the
hint matches the control it points at, and bumped the files to v8. Every
language is back to full coverage at 856 keys.
New deploy/site/sdk.html: the wada.* reference. The beta ships a public API and
had no user-facing documentation for it, so nobody outside the repo could write
an app. Covers the lifecycle, all seven tables, the app format, side-loading and
how to publish, plus the two constraints that surprise people -- the mesh is
read-only in v1 and http_get is plain HTTP because TLS does not fit in the heap
left after Wi-Fi associates.
Site device list: the Attaky Core and the T-Display P4 are fully supported now
(Kaj's call), so their not-hardware-verified caveats are gone. Dropped the "one
of the two primary development boards" wording, which stopped being true once
the P4 joined the test loop. The terms section named only the T-Deck and Heltec
V4 as flashable when the flasher offers ten boards; it now points at the list on
the page instead of going stale again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the homepage device picker into collapsible per-manufacturer boxes
(LilyGo / Heltec / Elecrow / RAKwireless / Nicolai Electronics / Attaky) with
a live search that filters by device name and manufacturer and auto-expands
matching groups. Add the T-Display P4 TFT-LCD (HI8561) card, wired to a
client-built ESP32-P4 manifest + the immutable per-tag BETA archive bin (no
feed change needed), and relabel the existing P4 card (AMOLED). All existing
install-button + download + channel-toggle wiring preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- New Discover app: active NODE_DISCOVER sweep -> signal-ranked nearby list, tap-to-add-contact, GPS wardriving log (SD CSV) + signal-coloured coverage overlay on the map. Board-agnostic.
- T-Display P4 TFT-LCD (HI8561) variant support (WADA_P4_LCD build; AMOLED bin untouched).
- Wire the Attaky Core board (#158, @attakygit) into the release matrix + web flasher.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Newest-first: Signal Trek's 'The Firmware That Finally Makes It Worth It'
(~Jul 12) on top, then KDHD Stuff's 'WadaMesh Update! Supercharge Your T-Deck'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every flasher board card now carries a support badge with a hover popover:
Heltec V4 + TFT and the T-Deck are 'Fully supported' (the two primary dev
boards); ThinkNode M9, RAK Tap V2, Heltec V4-R8, Tanmatsu and the new
T-Display P4 are 'Partially supported' — the popover spells out that the
core (LoRa mesh communication, chat, phone-app link) works while the rest
is still settling. New T-Display P4 card wired to the beta channel
(manifest-tdisplay-p4.json), same new-board pattern as the M9/RAK cards.
NOTICE: extend the ES8311 attribution to cover the P4's P4Audio.cpp
(esp-bsp-derived register sequence, re-based onto Arduino Wire).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first cut used an inline-flex link that wrapped and scattered the icon, text
and version across the narrow card. Restructure as a clean teal 'Download .bin'
row with the version as a small muted caption underneath; drop the double spacing
(the .board flex gap already separates it from Connect).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Community feedback: the only .bin download was buried in the Launcher path, so
people couldn't find a download at all. Add a compact "or download the .bin" link
to every standalone board card, right under its Connect button. Each follows the
Stable/Beta toggle (beta-only boards stay pinned to latest-beta) and shows the
exact version that feed serves (both channel tags fetched once so every link is
accurate). Downloads the full merged image — the same build Connect flashes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Community feedback (gubbinsgalore): on the Launcher install page there was no way
to tell which version the "Download .bin" gives you (it's the version-less
wadamesh-tdeck.bin), and the Stable/Beta channel toggle only existed on the
Standalone page — so Launcher users were pinned to stable and couldn't grab a
beta app image.
- Add the same Stable/Beta toggle to the Launcher page (the existing wmSetChannel
JS already drives dl-tdeck + keeps both toggles in sync).
- Add a version box under the download, auto-filled from version.json and updating
with the toggle: "You're downloading version <tag> · <channel>".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The V4-R8 uses an ESP32-S3R8 (8 MB OCTAL PSRAM) which claims GPIO33-37, so
Heltec moved every control signal off them and the Expansion Kit V2 rebuilt the
display/touch/SD block onto a hardware-SPI bus:
Vext 36->40 (active-LOW) . GPS_EN 34->42 . LED 35->46 . ADC_Ctrl removed
TFT (LovyanGFX HW-SPI): MOSI=15 SCK=16 MISO=45 CS=47 DC=48 RST=21 BL=44
touch (CHSC6x, shares board I2C 17/18) . micro-SD (shared TFT SPI, CS=3)
The build defines HELTEC_LORA_V4_TFT too, so it reuses all the V4 touch-UI code;
HELTEC_LORA_V4_R8 gates only the deltas. 8 MB enables the on-device web browser
(CAP_WEB_BROWSER) and the SD card (CAP_SD). Also wires the board into the release
+ web-flasher pipeline (release.sh, gen-flasher-meta.py, the flasher picker).
UNTESTED — no V4-R8 hardware yet. Pin sources: Meshtastic heltec_v4_r8 variant,
Heltec V4-R8 pinmap, and the Expansion_board_V2.03 schematic. All three touch
envs build clean; V4/T-Deck unregressed. On-device TODO: CHSC6x shared-Wire on
17/18, SD_CS=3, and the V2 PAM8904 buzzer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New "Web browser" section in deploy/site/docs.html (and a TOC entry)
covering the on-device text browser and tappable chat links
("Open in web" / "Create QR"), with three new device screenshots
(app_web, app_web_links, app_web_qr). Existing screenshots unchanged.
Also render the reader page inline in the DOC_CAPTURE tour: the reader
paints from the UI-update poll, which doesn't run while the tour blocks
the loop, so the tour now drives the fetch-wait + render itself before
capturing the Web app.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The DOC_CAPTURE tour re-captured the map from the live device, which showed a
less representative view. Keep the earlier curated map shot (tiles + markers).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New "Remote & web app" docs section covering the browser control panel
(Chats / Contacts / Discovered / Settings / Terminal), Screen mirror (VNC)
and Remote UI, with a 7-shot phone gallery of the real web app.
- Refreshed all device screenshots via the DOC_CAPTURE tour (now also
captures the Remote screen) and added the MQTT bridge settings page.
- New automated web-capture harness (scripts/doc/web-demo/): extracts the
real page from firmware, serves it with mock data, screenshots via Playwright.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>