mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-09-23 15:45:38 +00:00
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.
147 lines
5.4 KiB
Python
Executable File
147 lines
5.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Generate the WMM declination block that lives inside a Lua Store app.
|
|
|
|
The block is generated rather than hand-pasted for two reasons. It is four
|
|
kilobytes of coefficients that nobody can eyeball, and the wrapper around it
|
|
matters: the coefficient tables are named G/H/GD/HD, and an app that happens
|
|
to use a global named H (gpscompass uses it for the screen height) has that
|
|
global silently eaten by a module-scope `local H`. That actually happened.
|
|
Owning the `do ... end` here means it cannot come unwrapped again.
|
|
|
|
gen_lua.py --emit print the block
|
|
gen_lua.py --update <app.lua> rewrite the block in place
|
|
gen_lua.py --check <app.lua> fail if the app has drifted
|
|
|
|
--check is the one to wire into CI: it regenerates from WMM.COF and compares,
|
|
so a coefficient file that gets updated without regenerating, or a block that
|
|
gets edited by hand, is caught rather than shipped.
|
|
|
|
Verify the maths itself with verify.py (NOAA's own 100 test values), and the
|
|
generated Lua with the `declination` scenario in scripts/lua-harness.
|
|
"""
|
|
import argparse
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from wmm import load_cof
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
COF = os.path.join(HERE, "WMM.COF")
|
|
|
|
BEGIN = "-- WMM-GEN BEGIN -- generated by scripts/wmm/gen_lua.py; do not edit by hand"
|
|
END = "end -- WMM-GEN END"
|
|
|
|
|
|
def fmt(v):
|
|
"""Shortest round-trip-safe spelling: the coefficients are one decimal."""
|
|
s = f"{v:.1f}"
|
|
if s.endswith(".0"): s = s[:-2]
|
|
if s == "-0": s = "0"
|
|
if s.startswith("0."): s = s[1:] # 0.2 -> .2
|
|
if s.startswith("-0."): s = "-" + s[2:] # -0.2 -> -.2
|
|
return s
|
|
|
|
|
|
def _arr(name, vals):
|
|
body, line = [], ""
|
|
for v in vals:
|
|
if len(line) + len(v) + 1 > 96:
|
|
body.append(line)
|
|
line = ""
|
|
line += v + ","
|
|
if line:
|
|
body.append(line)
|
|
return f"local {name}={{" + "\n".join(body) + "}\n"
|
|
|
|
|
|
def gen_block(cof=COF):
|
|
"""The complete Lua block, markers included, ready to sit in an app."""
|
|
epoch, N, g, h, gd, hd = load_cof(cof)
|
|
G, H, GD, HD = [], [], [], []
|
|
for n in range(1, N + 1):
|
|
for m in range(0, n + 1):
|
|
G.append(fmt(g[(n, m)]))
|
|
GD.append(fmt(gd[(n, m)]))
|
|
if m >= 1:
|
|
H.append(fmt(h[(n, m)]))
|
|
HD.append(fmt(hd[(n, m)]))
|
|
|
|
with open(os.path.join(HERE, "wmm_block.lua"), encoding="utf-8") as f:
|
|
maths = f.read()
|
|
|
|
head = (
|
|
f"{BEGIN}\n"
|
|
f"-- WMM{int(epoch)} magnetic declination, degree {N}. East positive:\n"
|
|
f"-- true bearing = magnetic bearing + declination\n"
|
|
f"-- Data: NOAA/NCEI WMM.COF epoch {epoch}, valid {int(epoch)}.0-{int(epoch) + 5}.0.\n"
|
|
f"-- Source: https://www.ncei.noaa.gov/sites/default/files/2024-12/WMM2025COF.zip\n"
|
|
f"-- Everything below is scoped: the tables are named G/H/GD/HD and would\n"
|
|
f"-- otherwise shadow an app's own globals of those names.\n"
|
|
f"local declination\n"
|
|
f"do\n"
|
|
f"local EPOCH,NMAX={epoch},{N}\n"
|
|
)
|
|
arrays = _arr("G", G) + _arr("H", H) + _arr("GD", GD) + _arr("HD", HD)
|
|
return head + arrays + maths + END + "\n"
|
|
|
|
|
|
def _split(path):
|
|
"""(before, block, after) around the markers in `path`."""
|
|
src = open(path, encoding="utf-8").read()
|
|
a = src.find(BEGIN)
|
|
if a < 0:
|
|
sys.exit(f"{path}: no '{BEGIN}' marker")
|
|
b = src.find(END, a)
|
|
if b < 0:
|
|
sys.exit(f"{path}: BEGIN marker has no matching '{END}'")
|
|
return src[:a], src[a:b + len(END) + 1], src[b + len(END) + 1:]
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
g = ap.add_mutually_exclusive_group(required=True)
|
|
g.add_argument("--emit", action="store_true", help="print the block to stdout")
|
|
g.add_argument("--update", metavar="APP", help="rewrite the block inside APP")
|
|
g.add_argument("--check", metavar="APP", help="exit 1 if APP has drifted")
|
|
ap.add_argument("--cof", default=COF, help="coefficient file (default: WMM.COF)")
|
|
args = ap.parse_args()
|
|
|
|
block = gen_block(args.cof)
|
|
|
|
if args.emit:
|
|
sys.stdout.write(block)
|
|
return 0
|
|
|
|
path = args.update or args.check
|
|
before, current, after = _split(path)
|
|
|
|
if args.check:
|
|
if current == block:
|
|
print(f"{os.path.basename(path)}: up to date ({len(block)} bytes)")
|
|
return 0
|
|
print(f"{os.path.basename(path)}: the generated block has DRIFTED from "
|
|
f"{os.path.basename(args.cof)}", file=sys.stderr)
|
|
import difflib
|
|
diff = difflib.unified_diff(current.splitlines(), block.splitlines(),
|
|
"in app", "generated", lineterm="", n=1)
|
|
for i, line in enumerate(diff):
|
|
if i > 40:
|
|
print(" ...", file=sys.stderr)
|
|
break
|
|
print(" " + line, file=sys.stderr)
|
|
print(" run: scripts/wmm/gen_lua.py --update " + path, file=sys.stderr)
|
|
return 1
|
|
|
|
if current == block:
|
|
print(f"{os.path.basename(path)}: already up to date")
|
|
return 0
|
|
open(path, "w", encoding="utf-8").write(before + block + after)
|
|
print(f"{os.path.basename(path)}: block updated ({len(block)} bytes)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|