A two-axis magnetic heading assumes the device is level. At this latitude the
field dips ~60 degrees, so the vertical component is 1.6x the horizontal one
and tipping the device leaks it into the pair the heading is made from: about
1.5 degrees of heading per degree of tilt. That is what "it drifts" was once
the calibration was sound -- it was the hand holding it.
Driver: variants/thinknode_m9/M9Imu.{h,cpp}, QMI8658 at 0x6B on the peripheral
bus, accelerometer only (the gyro is most of the power budget and nothing here
needs it): +/-2 g at 62.5 Hz with the low-pass on, soft reset with a 160 ms
wait that covers both die variants, and the same idle-suspend as the compass so
it costs nothing when unused. CTRL1's ADDR_AI bit is set and read back -- with
it clear the burst read silently returns six copies of one byte, which looks
like a working sensor reporting nonsense. HAS_M9_IMU -> CAP_IMU ->
wada.sys.accel(), caps().accel.
Axes MEASURED, not guessed, by holding three attitudes and logging:
flat, screen up z = -1.02 -> +Z into the screen (down)
on bottom edge, top up x = +0.97 -> +X at the top edge (forward)
on left edge, right up y = +1.08 -> +Y at the right edge
So the IMU is already in the aerospace body frame, and it agrees with the
magnetometer's independently measured +Z-into-screen. (Meshtastic's M9 driver
passes both sensors through untransformed and mirrors the heading on the sign
of accel Z, so its compass flips when the device is turned over. Not copied.)
Heading now rotates the field back into the horizontal plane using gravity
(NXP AN4248 / ST AN3192) before taking the angle, and reports the tilt angle;
past 55 degrees it says "too steep to read" rather than lying. Calibration
returns to a 3D fit because tilt compensation needs the vertical offset too --
but the instruction is now "turn it every way ON ONE SPOT", and the
accelerometer VERIFIES it: coverage is measured by how far gravity swung, so a
flat spin is refused by name ("turn it nose over tail") instead of silently
fitting a degenerate sphere. Simulated in the harness against a modelled M9 in
a known attitude: offsets recovered exactly, and the heading holds within 3
degrees through 20 degrees of roll and pitch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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. There is no arbitrary
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
- Fork
ALLFATHER-BV/wadameshand branch offmain. - Add your
deploy/apps/<id>/<ver>/directory with the two files. - Add or update your app's row in
deploy/apps/apps.json(the store catalog). - 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.