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.
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.0–2030.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 2025–2030 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):
- Drop in the new
WMM.COFandWMM2025_TestValues.txtequivalents. verify.py— should still pass; if it does not, the file format changed.gen_lua.py --update <app>.scripts/lua-harness/run.sh— update the reference values in thedeclinationscenario, which are epoch-dependent.
The evaluation code in wmm_block.lua is degree-agnostic and needs no change
for a same-degree model.