Files
wadamesh/deploy/apps
tmetz1987andClaude Opus 5 49f2cac65e wardrive 1.2: review a run on the device
1.1 logs a survey to CSV and notes in its own docs that there is no on-device
viewer for it, so checking coverage meant pulling the card and opening a laptop.
1.2 adds a second screen that reads the run's own log back and plots it on the
firmware's basemap via wada.map, with each stop coloured by whichever direction
of the link you ask about. Colour cycles through how well we heard them, how well
they heard us, and the worst imbalance.

Design notes a reviewer may want up front.

- A sweep writes one row per responder against a single GPS fix, so a corner
  where ten nodes answered is ten rows sharing one coordinate. The loader
  aggregates by STOP, not by row, keeping the best link in each direction and the
  largest-magnitude single-reply asymmetry at that stop. Drawing a raw row would
  have coloured the marker by whichever node answered first, which is arrival
  order and none of the three metrics the button offers.

- The log is read in 4 KB windows, one per tick, and finishes only on a read that
  yields nothing. A callback gets 100,000 VM opcodes and parsing a whole log in
  one would exceed it; and since queued rows keep draining while the screen is
  open, treating a short read as end-of-file silently dropped everything appended
  mid-walk.

- Stops are capped. Past the cap the reservoir halves in place and widens its
  stride, and it carries the newest stop across each halving and appends the
  final position when the walk ends, so the drawn route ends where the run did.
  Simulated: a 5,000-stop run keeps 315 stops spread 79/78/79/79 across the four
  quarters of the route and ends on stop 5,000. A prefix cap would have drawn the
  first 400 and nothing after.

- Drawing is strided to at most ~150 stops. Each marker and segment is an LVGL
  object created synchronously and the host puts no ceiling on an app's view
  (k_map_markers_max belongs to the Map tab), so the bound has to come from the
  app, on a loop the radio shares.

- The map view is closed before any ui.clear(), routed through one helper so the
  order cannot be broken by a later edit. See the note in drop_map: MapUd is the
  only userdata type without a generation field, so clearing with a view open
  strands the one-view counter and leaves a stale pointer. Reported separately.

- ui.canvas argchecks 1..480 on both dimensions where map.view allows 1..800, so
  the bare-plot fallback asked for 792 px on an 800 px board and would have
  raised inside the very branch that exists to keep the app standing. Clamped,
  and the call is guarded.

- Probing pauses while the review screen is open, per the store's guidance that a
  survey should probe on a cadence its user chose. Entering banks the in-flight
  probe only if it is still inside its harvest window, and otherwise drops it, on
  the same terms the tick path uses; both call one shared predicate so they
  cannot drift apart. Either way the discovery set is cleared so nothing crosses
  the pause.

- Zoom and centre are fitted in Web Mercator units, matching latLonToWorldPx:
  latitude costs 1/cos(lat) times as many pixels as longitude, and the visual
  centre is the inverse of the mean projected y rather than the mean latitude.
  Both matter on a long north-south run. A run straddling the antimeridian is
  fitted on latitude alone, with the centre longitude unwrapped onto 0..360
  before averaging so it lands near the line rather than on the far side of the
  world, and the card says it happened.

- Layout reserves every fixed element and gives the plot the remainder, because
  on_open is handed the body height rather than the panel height, and a
  half-height plot pushed the control row off the bottom. The two-line label
  reservations were checked against the shipped glyph tables at the narrowest
  label width, which is what shortened two of the strings.

- The manifest gains "icon": "map". 1.1 shipped without one, so the drawer tile
  falls back to the generic app symbol; an app cannot ship artwork, and of the
  sixteen glyph names map is the one that describes what 1.2 now does. Drop it if
  you would rather the tile stayed as it is.

TWO FIXES HERE BELONG TO 1.1, NOT TO THE REVIEW SCREEN. Both corrupt the file the
new screen reads, which is why they are here, but say the word and I will pull
them out and send 1.2 as the viewer alone.

- Ticks pause while the display sleeps; millis() does not. On waking, the harvest
  deadline is long past, the radio still holds replies heard at the old position,
  and sys.gps() reports the new one, so those replies were logged in the wrong
  place. Pocket the device, walk 400 m, wake it. Sweeps dropped this way are now
  counted on the status line, alongside a sweep discarded because the survey was
  stopped mid-listen - two causes, one counter, and the label says dropped rather
  than naming either.

- Renaming a run wrote the previous run's queued rows into the new file, and
  wrote_header being process-local appended a second header mid-file whenever an
  existing log was reopened. The on-device reader skips those lines; a
  spreadsheet does not.

TESTED ON: nothing. I have a T-Deck Plus but have not run this, so treat every
runtime claim above as read from the firmware source rather than observed. Some
specifics are worth naming rather than hiding. The body-height figure and the
capability, argcheck and projection details above come from reading
src/ui-touch/, not from the SDK page, which is out of date in at least two places
(it documents an input event type the firmware never emits, which is why this
version has no on_input, and it omits both argcheck ranges). The riskiest
unverified behaviour is whether a map view can be created again after being
closed on a screen switch; that is guarded by pcall with a canvas fallback. Label
wrapping at the narrowest width is calculated, not seen.

Written with AI assistance, then reviewed by nine independent passes and two
cross-vendor panels. Every finding that survived verification is fixed. The one
that mattered most was the arrival-order colour above, which the first panel
missed and the second caught.

ONE QUESTION, not decided here. 1.2 is 34,159 bytes against 1.1's 8,477, and
wardrive already requires sdk_ext, so boards that cannot run it still pay for it
in the baked catalog. "seed": false looks right to me, but the README calls that
your knob, so the row is left exactly as it was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: tmetz1987 <tylerzmetzger@gmail.com>
2026-09-12 22:44:11 -07:00
..
2026-08-23 20:32:26 -04:00
2026-08-23 20:32:26 -04:00

Submitting an app to the WADAMESH store

Apps are small Lua programs that appear in the device's Apps drawer and are downloaded over Wi-Fi from firmware.wadamesh.com/apps/. Snake, RF Monitor and Airtime all ship this way — they are ordinary submissions, not special cases, so reading their source is the fastest way to see what an app looks like.

Every submission is reviewed and safety-checked before it is added. What that means in practice is spelled out under Review below. It is not a formality and it is not a judgement of you — an app runs on other people's radios, so somebody has to read it first.

What an app is

One Lua file plus a small manifest. Apps talk to the firmware through the wada.* API — wada.ui (widgets, colours, a text prompt), wada.sys, wada.store (persistence), wada.timer, and on the larger boards wada.fs, wada.net, wada.crypto and the writable half of wada.mesh. Supported boards also expose asynchronous WAV/MP3 playback through wada.audio; plain filenames come from the app sandbox on internal flash, SD, or SD_MMC. Read-only physical-SD directory metadata is available through wada.sd, and direct card playback uses an explicit sd:/... path. There is no general-purpose filesystem or network access; the API is the whole surface, which is what makes reviewing tractable. It is documented at wadamesh.com/sdk.html.

Two numbers worth knowing before you start: every callback runs under a 100,000 Lua instruction budget, and Lua here is built with 32-bit numbers, so its floats are single precision. If you are logging coordinates, use the lat_e6 / lon_e6 integers rather than lat / lon.

Apps run on every board that has the Apps drawer, from the 2 MB-PSRAM Heltec V4 to the 800×480 Tanmatsu. Do not hard-code pixel sizes — read the screen from the API and lay out relative to it, the way snake.lua computes its grid.

Layout

deploy/apps/<id>/<ver>/<id>.json     manifest
deploy/apps/<id>/<ver>/<id>.lua      the app

<id> is lowercase, no spaces, and is the filename stem everywhere. <ver> is major.minor.

The manifest is one line:

{"id":"snake","name":"Snake","ver":"1.0","desc":"The classic, in Lua. Swipe to steer.","icon":"game"}

desc is what people read in the store listing before installing, so make it say what the app does. One sentence.

icon is optional and picks the drawer tile's glyph by NAME — an app cannot ship its own artwork, so anything unrecognised falls back to the generic app symbol. Choose from: gps / compass / map, radio, signal, chart, list, message, person, group, bell, star, search, settings, battery, game.

Version directories are immutable. Once 1.0/ is published it is never edited — a change ships as 1.1/. Devices cache by version, so editing in place means some users silently run different code from others under the same version number.

Submitting

  1. Fork ALLFATHER-BV/wadamesh and branch off main.
  2. Add your deploy/apps/<id>/<ver>/ directory with the two files.
  3. Add or update your app's row in deploy/apps/apps.json (the store catalog). Add "seed": false to that row if your app should be downloadable but not compiled into the firmware. Apps in the catalog are baked into the image as built-ins for boards that cannot reach the Store, and that is flash spent on the boards with the least of it. A large app, or one that needs hardware those boards do not have, is better left to the Store.
  4. Open a pull request. In the description, say what the app does, which boards you tested it on, and anything it persists via wada.store.

Please open the PR even if you are unsure about the code — a rough app that works is easier to review than a perfect one that never gets sent. If you would rather not use git at all, open an issue with the .lua file attached and say so; we would rather have the app than the paperwork.

You keep authorship: merging the PR preserves your commits, and the store listing is generated from your manifest. The repository is GPL-3.0, so your app is published under that licence too — do not submit code you are not free to license that way.

Review

Two passes. The first is ordinary code review: does it work, does it fit the screen on a small board, does the description match the behaviour.

The second is the safety check, and it exists because an app ships to other people's devices. We read every submission for:

  • Identity and keys — an app must not read, log, transmit or persist the node identity, private key, channel secrets or contact keys. This is the one that gets a submission rejected outright rather than sent back for changes.
  • Flash wear and stalls — no writes in a tight loop and none on a per-packet path. Frequent small writes to internal flash trigger garbage collection, which on ESP32 suspends the flash cache and stalls both CPU cores; we have shipped two firmware bugs of exactly this shape. Persist on a user action or a slow timer, not on every frame.
  • Blocking the UI — no busy-waits and no long synchronous work. The UI and the mesh share a loop; an app that blocks it drops packets as well as frames.
  • Memory — bounded allocations, and nothing that assumes 8 MB of PSRAM. The V4 has 2 MB and is the floor.
  • Radio behaviour — an app may read counters and statistics. Transmitting, or changing radio parameters, needs a clear reason and an explicit user action.
  • Probing — wada.mesh.discover() costs a transmission from the user and a reply from every node that hears it, so it spends other people's airtime as well as theirs. The firmware enforces 15 seconds between probes, but a survey app is still expected to probe on a cadence a person chose and to stop when it is not on screen. Probing in a loop for no stated reason gets sent back.
  • Where data goes — anything leaving the device has to be something the user asked for and can see.

If something needs changing we will say what and why on the PR, and we would rather help you land it than close it. If we reject an app we will tell you the reason plainly.

Testing before you send it

Try it on real hardware, not just one screen size. If you only have one board, say so in the PR — that is useful information, not a disqualification, and it tells the reviewer what still needs checking.