From d91737bb3fe402845ab2f32e2db89d5d9ff636fb Mon Sep 17 00:00:00 2001 From: Christopher Van Hoose Date: Sat, 22 Aug 2026 01:52:27 -0400 Subject: [PATCH] Lua apps on the firmware's black; GPS Compass: one-press north, swapped sides The Lua app page painted itself 0x0E1216 while the rest of the firmware paints pure black (COLOR_BG), so every app read as a lighter panel floating over the UI. The page is now black and wada.ui.colors gains `panel` for the raised surface an app draws on top of it -- the compass dial uses it, so the instrument stands out instead of the page doing it. Rotate/flip are gone from the user's side. They existed because the QMC6309's axis orientation on the M9 is undocumented, which is not the user's problem to solve by trial and error. `A` now does the whole job: whether the heading runs clockwise or anticlockwise follows from which way the sensor's Z axis faces, and that shows in the sign of the vertical field -- Earth's field dips down north of the magnetic equator and up south of it -- so with a position (a fix, or the node's last known one) the app reads the handedness off the sensor and the press only has to set the offset. `F` stays as the fallback for a flat field or no position at all. Layout: stats column on the left, dial on the right (its status line above it, the hint centred along the bottom). The host harness now tumbles the simulated device during calibration rather than spinning it flat -- min/max on an axis that never moves subtracts the true field along it, which is precisely the case the handedness check has to detect and refuse, and the flat mock was hiding it. Co-Authored-By: Claude Fable 5 --- deploy/apps/gpscompass/1.0/gpscompass.lua | 129 ++++++++++++++++------ deploy/site/sdk.html | 3 + src/ui-touch/LuaAppHost.cpp | 10 +- 3 files changed, 104 insertions(+), 38 deletions(-) diff --git a/deploy/apps/gpscompass/1.0/gpscompass.lua b/deploy/apps/gpscompass/1.0/gpscompass.lua index bac1600..66ee2e4 100644 --- a/deploy/apps/gpscompass/1.0/gpscompass.lua +++ b/deploy/apps/gpscompass/1.0/gpscompass.lua @@ -11,15 +11,20 @@ -- * hard-iron calibration: press C (or Cal), turn the device through every -- orientation for ~20 s, press C again. Offsets persist in wada.store. -- * orientation: the sensor's axes vs. the screen are not documented for --- the M9, so O (Rot) steps the frame by 90° and F (Flip) mirrors it. --- Point the top of the device at a known north and adjust until the dial --- reads 000 and the number grows as you turn clockwise. Persisted too. +-- the M9, so the app works it out. Hold the device flat, point the top +-- edge at north and press A. Which way the sensor's Z axis faces decides +-- whether the heading runs clockwise or anticlockwise, and that follows +-- from the sign of the vertical field: Earth's field dips DOWN in the +-- northern hemisphere and UP in the southern one, so with a GPS fix (or +-- the last known position) the app reads the handedness off the sensor +-- itself and only needs the one press for the rest. Both persist. -- Magnetic declination is not applied: this is a magnetic compass. local ui, sys, mesh, store, timer = wada.ui, wada.sys, wada.mesh, wada.store, wada.timer local C = ui.colors local AMBER = 0xE8A33D local RING = 0x3A424A -- dial ring + minor ticks -local RING2 = 0x1C2228 -- inner ring +local RING2 = 0x252C33 -- inner ring +local PANEL = C.panel or 0x15181B -- the dial's own surface (older hosts: literal) local app = {} local caps, W, H @@ -33,8 +38,10 @@ local L = {} -- labels by name local has_compass = false local cal = nil -- { ox, oy, oz } hard-iron offsets local calib = nil -- in-progress: { mn = {x,y,z}, mx = {x,y,z}, t0 } -local orient, flip = 0, false -- quarter turns + mirror applied to the sensor frame +local align = 0 -- degrees added to the raw angle so north reads 000 +local mirror = false -- sensor Z faces into the screen: heading runs the other way local mag_norm = nil -- |B| after offsets, Gauss (sanity check for the user) +local mag_z = 0 -- vertical component after offsets (handedness + dip) local mag_sat = false -- last magnetometer sample was flagged as saturated local hv_x, hv_y = 0, 0 -- smoothed heading unit vector @@ -76,8 +83,13 @@ local function load_prefs() if type(ox) == "number" and type(oy) == "number" and type(oz) == "number" then cal = { ox = ox, oy = oy, oz = oz } end - orient = math.floor(tonumber(store.get("orient", 0)) or 0) % 4 - flip = (store.get("flip", 0) == 1) + align = tonumber(store.get("align", 0)) or 0 + mirror = (store.get("mirror", 0) == 1) +end + +local function save_align() + store.set("align", math.floor(align + 0.5)) + store.set("mirror", mirror and 1 or 0) end local function save_cal() @@ -90,16 +102,15 @@ end -- --------------------------------------------------------------------------- -- heading --- Sensor frame -> screen frame -> heading. After the offsets, `fx` is the --- field component along the screen's top edge direction and `fy` along its --- right edge. Device top pointing north: field along +fx -> 0°. Pointing --- east: north is to the device's left -> fy negative -> 90°. +-- Raw angle of the horizontal field in the sensor's own frame. `mirror` picks +-- the direction of travel (which follows from where the sensor's Z axis +-- points — see align_north); `align` rotates that so the device's top edge +-- reading north comes out as 000. local function mag_heading(m) local x, y = m.x, m.y if cal then x, y = x - cal.ox, y - cal.oy end - if flip then y = -y end - for _ = 1, orient do x, y = -y, x end - return norm360(math.deg(math.atan(-y, x))) + local a = mirror and math.atan(y, x) or math.atan(-y, x) + return norm360(math.deg(a) + align) end local function smooth_heading(h) @@ -129,6 +140,7 @@ local function update_heading(now) local x, y, z = m.x, m.y, m.z if cal then x, y, z = x - cal.ox, y - cal.oy, z - cal.oz end mag_norm = math.sqrt(x * x + y * y + z * z) + mag_z = z heading, src = smooth_heading(mag_heading(m)), "mag" end return @@ -221,7 +233,10 @@ end local function draw_dial(tgt_bearing) local h = heading or 0 local live = heading ~= nil + -- the page is the firmware's black; the dial sits on its own raised disc so + -- it reads as an instrument rather than as drawing on the page cv:fill(C.bg) + cv:circle(CX, CY, R, PANEL, true) -- rings cv:circle(CX, CY, R, live and RING or RING2, false, 2) cv:circle(CX, CY, R - 14, RING2, false, 1) @@ -283,7 +298,7 @@ local function draw_sats(n, col) for i = 0, 9 do local x = i * (cw + 1) if i < n then sats_cv:rect(x, 0, cw, SATS_H, col, true) - else sats_cv:rect(x, 0, cw, SATS_H, RING, true) end + else sats_cv:rect(x, 0, cw, SATS_H, PANEL, true) end end end @@ -416,18 +431,58 @@ local function toggle_cal() hv_x, hv_y = 0, 0 end -local function rotate_frame() - orient = (orient + 1) % 4 - store.set("orient", orient) +-- One press does the whole orientation job: hold the device flat with the top +-- edge at north and press A. +-- * handedness — whether the heading runs clockwise or anticlockwise depends +-- on which way the sensor's Z axis faces, and that shows up in the sign of +-- the vertical field: Earth's field dips DOWN north of the magnetic +-- equator and UP south of it. With a position (a fix, or the last one the +-- node knows) the sign of `mag_z` therefore says which way Z points. +-- Near the equator the dip is too shallow to read, so the mirror is left +-- as it was and only the offset is set. +-- * offset — whatever angle the sensor reports while pointing north becomes +-- the zero. +local function align_north() + if not has_compass then sys.toast("No magnetometer on this board", 1500) return end + if not cal then sys.toast("Calibrate first: press C", 2000) return end + local m = sys.compass() + if not m or m.ovfl then sys.toast("No usable reading", 1500) return end + + local lat + local g = caps.sdk_ext and sys.gps() or nil + if g then lat = g.lat else + local me = mesh.self() + if me and (me.lat ~= 0 or me.lon ~= 0) then lat = me.lat end + end + local dip_ok = math.abs(mag_z) > 0.05 * (mag_norm or 1) + if lat and dip_ok then + -- Z out of the screen: field points down (north) -> negative z reading + local z_up = (lat >= 0) == (mag_z < 0) + mirror = not z_up + end + + align = 0 + align = -mag_heading(m) -- whatever it reads now becomes 000 + save_align() hv_x, hv_y = 0, 0 - sys.toast(string.format("Frame rotated: %d\194\176", orient * 90), 900) + if lat and dip_ok then + sys.toast("North set", 1200) + elseif not lat then + sys.toast("North set (no position: turn right, then A again if it counts down)", 3000) + else + sys.toast("North set (field too flat here to check direction)", 2500) + end end +-- Fallback when align_north cannot read the dip (near the magnetic equator, or +-- no position at all): flip the direction of travel by hand. local function flip_frame() - flip = not flip - store.set("flip", flip and 1 or 0) + mirror = not mirror + align = 0 + save_align() hv_x, hv_y = 0, 0 - sys.toast(flip and "Frame mirrored" or "Frame normal", 900) + sys.toast(mirror and "Direction reversed - press A again facing north" + or "Direction normal - press A again facing north", 2500) end -- --------------------------------------------------------------------------- @@ -460,15 +515,15 @@ function app.on_open(w, h) local hint_y if landscape then - -- status line over the dial, the dial below it taking what is left after - -- a panel wide enough for the values, the hint along the bottom edge + -- stats column on the left, dial on the right under its status line, the + -- hint centred along the bottom edge hint_y = h - TH12 - 3 dial_y = TH12 + 4 D = math.min(hint_y - 4 - dial_y, w - 176) cv = ui.canvas(D, D) - dial_x = 2 - x0, y0 = D + 10, 4 - colw = w - x0 - 4 + dial_x = w - D - 2 + x0, y0 = 4, 4 + colw = dial_x - x0 - 8 line = TH12 + 2 local function total() local t = 0 @@ -524,9 +579,9 @@ function app.on_open(w, h) local by = y + 2 local bx = 6 if has_compass then - ui.button("Cal", bx, by, bw, bh, toggle_cal); bx = bx + bw + 3 - ui.button("Rot", bx, by, bw, bh, rotate_frame); bx = bx + bw + 3 - ui.button("Flip", bx, by, bw, bh, flip_frame); bx = bx + bw + 3 + ui.button("Cal", bx, by, bw, bh, toggle_cal); bx = bx + bw + 3 + ui.button("North", bx, by, bw, bh, align_north); bx = bx + bw + 3 + ui.button("Flip", bx, by, bw, bh, flip_frame); bx = bx + bw + 3 end ui.button("Target", bx, by, bw, bh, function() cycle_target(1) end) y = by + bh + 4 @@ -538,8 +593,8 @@ function app.on_open(w, h) L.hint:width(w, "center") if caps.keyboard then local wide = w >= TH12 * 20 -- room for the target hint too - set_text("hint", has_compass and (wide and "C calibrate O rotate F flip <> target" - or "C calibrate O rotate F flip") + set_text("hint", has_compass and (wide and "C calibrate A set north <> target" + or "C calibrate A set north") or "<> target", C.sub) elseif caps.touch then set_text("hint", "Tap dial: next target", C.sub) @@ -594,10 +649,14 @@ function app.on_input(ev) elseif ev.type == "key" then local k = ev.key if k == "c" or k == "C" then toggle_cal() - elseif k == "o" or k == "O" then rotate_frame() - elseif k == "f" or k == "F" then flip_frame() + elseif k == "a" or k == "A" then align_north() + elseif k == "f" or k == "F" then flip_frame() -- fallback, see align_north elseif k == "x" or k == "X" then - if cal then cal = nil; save_cal(); sys.toast("Calibration cleared", 1000) end + if cal then + cal = nil; save_cal() + align = 0; mirror = false; save_align() + sys.toast("Calibration cleared", 1000) + end end end end diff --git a/deploy/site/sdk.html b/deploy/site/sdk.html index bd2dbcb..5793b05 100644 --- a/deploy/site/sdk.html +++ b/deploy/site/sdk.html @@ -218,6 +218,9 @@ return app

wada.ui

+
+

Colours. wada.ui.colors carries the firmware's palette: bg (the page ground — black, the same as the rest of the interface), panel (a raised surface to lift a card or an instrument off that ground), text, sub, accent, good, bad. Use them rather than your own hex so an app looks like part of the device.

+

Widgets are created in order, top to bottom, on the app's page. Every constructor returns a handle with methods you can call later.

diff --git a/src/ui-touch/LuaAppHost.cpp b/src/ui-touch/LuaAppHost.cpp index 7370a4a..af435d6 100644 --- a/src/ui-touch/LuaAppHost.cpp +++ b/src/ui-touch/LuaAppHost.cpp @@ -1070,11 +1070,12 @@ void openWada(lua_State* L) { lua_pushcfunction(L, uiChart); lua_setfield(L, -2, "chart"); lua_pushcfunction(L, uiScroll); lua_setfield(L, -2, "scroll"); lua_pushcfunction(L, uiTextH); lua_setfield(L, -2, "text_h"); - lua_createtable(L, 0, 6); // wada.ui.colors (theme) + lua_createtable(L, 0, 7); // wada.ui.colors (theme) lua_pushinteger(L, 0x15B6A6); lua_setfield(L, -2, "accent"); lua_pushinteger(L, 0xE6E9ED); lua_setfield(L, -2, "text"); lua_pushinteger(L, 0x7A7F87); lua_setfield(L, -2, "sub"); - lua_pushinteger(L, 0x0E1216); lua_setfield(L, -2, "bg"); + lua_pushinteger(L, 0x000000); lua_setfield(L, -2, "bg"); // the page ground (matches the firmware) + lua_pushinteger(L, 0x15181B); lua_setfield(L, -2, "panel"); // a raised surface on top of it lua_pushinteger(L, 0xD7574E); lua_setfield(L, -2, "bad"); lua_pushinteger(L, 0x53C06B); lua_setfield(L, -2, "good"); lua_setfield(L, -2, "colors"); @@ -1323,7 +1324,10 @@ bool luaAppLaunch(const char* id, const char* title, const char* src, size_t len h->root = lv_obj_create(lv_layer_top()); lv_obj_remove_style_all(h->root); lv_obj_set_size(h->root, lv_disp_get_hor_res(nullptr), lv_disp_get_ver_res(nullptr)); - lv_obj_set_style_bg_color(h->root, lv_color_hex(0x0E1216), LV_PART_MAIN); + // Pure black, the same ground the rest of the firmware paints (COLOR_BG in + // UITask.cpp) — an app page must not read as a lighter panel floating over + // the UI. Apps that want a raised surface draw one (wada.ui.colors.panel). + lv_obj_set_style_bg_color(h->root, lv_color_hex(0x000000), LV_PART_MAIN); lv_obj_set_style_bg_opa(h->root, LV_OPA_COVER, LV_PART_MAIN); lv_obj_clear_flag(h->root, LV_OBJ_FLAG_SCROLLABLE); // A plain lv_obj is CLICKABLE by default, and it has to stay so (it is the