Files
Christopher Van Hoose 6cc0c1e56e Make the WMM block generated, and the tests reviewable
The declination model landed as 4.7 KB of constants pasted into a Lua app,
generated by a script that lived in out/ -- which is gitignored, holds firmware
bins, and is where the app's own "Regenerate:" comment pointed. So the pointer
dangled for anyone who cloned the repo, and nobody but me could answer the
first fair question a reviewer would ask about that block of magic numbers:
where did it come from, and how do I know it is right.

  scripts/wmm/       WMM.COF + NOAA's 100 official test values (both upstream
                     and unmodified), the float64 reference, the generator,
                     verify.py, and a README covering provenance, regeneration
                     and how to move to WMM2030.
  scripts/lua-harness/  the host harness, with run.sh so it is one command.

Neither goes in test/: that is PlatformIO's directory and a harness with a
main.c would be swept into `pio test`. scripts/ already holds this repo's dev
tooling, test_companion_serial.py included.

The block in the app is now genuinely generated rather than hand-pasted:

    scripts/wmm/gen_lua.py --update <app>    rewrite it
    scripts/wmm/gen_lua.py --check  <app>    fail, with a diff, if it drifted

--check catches coefficients updated without regenerating, or a block edited by
hand. The generator owns the `local declination / do ... end` wrapper too, and
that is the point: the tables are named G/H/GD/HD, gpscompass uses a global H
for the screen height, and an unscoped `local H` silently ate it. Hand-wrapping
is how that happened, so hand-wrapping is now not a step.

Verification, all reproducible from a clean clone:
  scripts/wmm/verify.py            100 NOAA values, worst D error 0.005 deg
  scripts/lua-harness/run.sh       10 scenarios, incl. the generated Lua in
                                   the device's own LUA_32BITS interpreter --
                                   0.0002 deg vs NOAA, worst tick 12k of 100k

Also refreshes the LUA_APPS.md paragraph, which still advertised the O and F
keys that were removed and quoted harness numbers from before tilt
compensation.
2026-08-22 13:30:36 -04:00
..

World Magnetic Model → Lua

Generates the magnetic-declination block that lives inside deploy/apps/gpscompass/1.0/gpscompass.lua.

Why an app carries a geophysical model

A magnetometer measures magnetic north. Every bearing computed from two sets of coordinates is relative to true north. Draw one against the other and every waypoint on the compass is wrong by the local declination — which is how this arrived: on a ThinkNode M9 in Atlantic Canada, every contact sat about 22° west of where it belonged, including nodes whose positions were known to be correct.

Declination is not a constant. It runs from about +25° to 25° across the populated world, reverses sign along the agonic line, and drifts on the order of a tenth of a degree a year. A single hard-coded offset is wrong for almost everyone, and a user-entered one is a support burden that most people will get wrong or never set.

So the app carries the real model: WMM2025 to degree 12, coefficients and evaluation in ~4.7 KB of Lua, including the secular-variation terms so it stays correct across the model's whole 2025.02030.0 window rather than drifting from a frozen snapshot. A lookup grid accurate enough to stay inside a degree would have cost several times the space and still been worse.

Past expiry it degrades gracefully — roughly 0.14°/yr — so it stays inside a degree until about 2033 even if nobody reissues it.

Files

WMM.COF NOAA/NCEI coefficients, epoch 2025.0. Upstream data, unmodified.
WMM2025_TestValues.txt NOAA's 100 official test values. Upstream data, unmodified.
wmm.py float64 reference implementation, used by both scripts below.
gen_lua.py emits the Lua block; --check / --update keep the app in sync.
verify.py checks wmm.py against NOAA's test values.
wmm_block.lua the evaluation code the generator wraps around the coefficients.

Source for both data files: https://www.ncei.noaa.gov/sites/default/files/2024-12/WMM2025COF.zip

Regenerating

scripts/wmm/gen_lua.py --update deploy/apps/gpscompass/1.0/gpscompass.lua

The block is delimited in the app by -- WMM-GEN BEGIN / end -- WMM-GEN END. Everything between those markers is generated; the prose above them is not.

scripts/wmm/gen_lua.py --check deploy/apps/gpscompass/1.0/gpscompass.lua

--check exits non-zero with a diff if the app has drifted from WMM.COF — either because the coefficients were updated without regenerating, or because someone edited the block by hand. Worth running before a release, and the obvious thing to wire into CI if this repo ever grows a PR workflow.

The generator owns the local declination / do ... end wrapper, and that is deliberate. The coefficient tables are named G, H, GD, HD; gpscompass uses a global H for the screen height, and an unscoped local H silently ate it. Keeping the wrapper generated means it cannot come unwrapped again.

Verifying

Two independent checks, because "4.7 KB of numbers that produce a plausible angle" is not evidence of anything.

The maths, against NOAA:

scripts/wmm/verify.py

Runs all 100 official test values (declination, inclination, H, X, Y, Z, F over the full 20252030 range and both hemispheres) and prints the worst absolute error per component.

The generated Lua, in the interpreter the device actually runs — single precision, LUA_32BITS, which is where an implementation that is fine in float64 can quietly fall apart:

scripts/lua-harness/run.sh          # the `declination` scenario

It pulls the block straight out of the app file rather than testing a copy, so a bad regeneration fails there instead of on the device. Current worst error against NOAA reference values: 0.0002°.

Updating to a future model

When NOAA issues WMM2030 (or an out-of-cycle revision):

  1. Drop in the new WMM.COF and WMM2025_TestValues.txt equivalents.
  2. verify.py — should still pass; if it does not, the file format changed.
  3. gen_lua.py --update <app>.
  4. scripts/lua-harness/run.sh — update the reference values in the declination scenario, which are epoch-dependent.

The evaluation code in wmm_block.lua is degree-agnostic and needs no change for a same-degree model.