mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-10-24 10:32:32 +00:00
Tracks total distance (Haversine accumulation), elapsed time, current speed, max speed, and altitude from live GPS fixes. Start/Stop button pauses accumulation; Reset clears the trip. The current trip persists across app restarts via wada.store. Also adds 6 harness scenarios (no-fix, T-Deck, moving, persist, reset, cost) and registers the app in apps.json. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
177 lines
4.8 KiB
Lua
177 lines
4.8 KiB
Lua
-- Trip Odometer — wadamesh Lua app
|
||
-- Tracks distance, time, speed, and altitude for a trip via GPS.
|
||
-- Tap Start/Stop to track, Reset to clear. Saves your trip across app restarts.
|
||
-- Contributed by samuelcoustet
|
||
|
||
local ui, sys, store, timer = wada.ui, wada.sys, wada.store, wada.timer
|
||
local C = ui.colors
|
||
|
||
local app = {}
|
||
|
||
local MIN_SPEED_KMH = 2.0 -- below this speed, GPS drift is ignored
|
||
local POLL_MS = 2000 -- GPS poll interval
|
||
|
||
-- state
|
||
local total_km, max_kmh, elapsed_s
|
||
local last_lat, last_lon
|
||
local running, fix
|
||
|
||
-- labels updated each tick
|
||
local spd_lbl, dist_lbl, time_lbl, maxspd_lbl, alt_lbl, sats_lbl, st_lbl
|
||
|
||
local function haversine(lat1, lon1, lat2, lon2)
|
||
local R = 6371.0
|
||
local dlat = (lat2 - lat1) * math.pi / 180.0
|
||
local dlon = (lon2 - lon1) * math.pi / 180.0
|
||
local a = math.sin(dlat * 0.5) ^ 2
|
||
+ math.cos(lat1 * math.pi / 180.0)
|
||
* math.cos(lat2 * math.pi / 180.0)
|
||
* math.sin(dlon * 0.5) ^ 2
|
||
return R * 2.0 * math.atan(math.sqrt(a), math.sqrt(1.0 - a))
|
||
end
|
||
|
||
local function fmt_time(s)
|
||
local h = math.floor(s / 3600)
|
||
local m = math.floor((s % 3600) / 60)
|
||
local sec = s % 60
|
||
if h > 0 then return string.format("%d:%02d:%02d", h, m, sec) end
|
||
return string.format("%d:%02d", m, sec)
|
||
end
|
||
|
||
local function fmt_dist(km)
|
||
if km < 1.0 then return string.format("%d m", math.floor(km * 1000)) end
|
||
return string.format("%.2f km", km)
|
||
end
|
||
|
||
local function save()
|
||
store.set("trip_km_x1000", math.floor(total_km * 1000))
|
||
store.set("trip_max_x10", math.floor(max_kmh * 10))
|
||
store.set("trip_s", elapsed_s)
|
||
end
|
||
|
||
local function redraw()
|
||
local spd = (fix and fix.speed_kmh) or 0.0
|
||
spd_lbl:set(string.format("%.1f", spd))
|
||
|
||
dist_lbl:set(fmt_dist(total_km))
|
||
time_lbl:set(fmt_time(elapsed_s))
|
||
maxspd_lbl:set(string.format("%.1f km/h", max_kmh))
|
||
|
||
if fix then
|
||
alt_lbl:set(string.format("%d m", fix.alt_m or 0))
|
||
sats_lbl:set(string.format("%d sats", fix.sats or 0))
|
||
if running then
|
||
st_lbl:set("tracking"); st_lbl:color(C.good)
|
||
else
|
||
st_lbl:set("paused"); st_lbl:color(C.sub)
|
||
end
|
||
else
|
||
alt_lbl:set("-- m")
|
||
sats_lbl:set("no GPS")
|
||
st_lbl:set("no fix"); st_lbl:color(C.bad)
|
||
end
|
||
end
|
||
|
||
function app.on_open(w, h)
|
||
-- restore saved trip
|
||
total_km = store.get("trip_km_x1000", 0) / 1000.0
|
||
max_kmh = store.get("trip_max_x10", 0) / 10.0
|
||
elapsed_s = store.get("trip_s", 0)
|
||
running = false
|
||
fix = nil
|
||
last_lat = nil
|
||
last_lon = nil
|
||
|
||
local col2 = math.floor(w / 2)
|
||
|
||
-- large speed at top
|
||
ui.label("SPEED", 8, 6, 11, C.sub)
|
||
spd_lbl = ui.label("0.0", 8, 20, 36, C.accent)
|
||
ui.label("km/h", 8, 60, 12, C.sub)
|
||
|
||
-- 2×2 grid
|
||
local y2 = 84
|
||
ui.label("DISTANCE", 8, y2, 11, C.sub)
|
||
dist_lbl = ui.label("0 m", 8, y2 + 14, 20, C.text)
|
||
|
||
ui.label("TIME", col2, y2, 11, C.sub)
|
||
time_lbl = ui.label("0:00", col2, y2 + 14, 20, C.text)
|
||
|
||
local y3 = y2 + 48
|
||
ui.label("MAX SPEED", 8, y3, 11, C.sub)
|
||
maxspd_lbl = ui.label("0.0 km/h", 8, y3 + 14, 16, C.text)
|
||
|
||
ui.label("ALTITUDE", col2, y3, 11, C.sub)
|
||
alt_lbl = ui.label("-- m", col2, y3 + 14, 16, C.text)
|
||
|
||
-- GPS status row
|
||
local y4 = y3 + 42
|
||
sats_lbl = ui.label("no GPS", 8, y4, 13, C.sub)
|
||
st_lbl = ui.label("no fix", col2, y4, 13, C.sub)
|
||
|
||
-- buttons anchored to bottom
|
||
local bw = math.floor((w - 20) / 2)
|
||
local btn_y = h - 46
|
||
ui.button("Start / Stop", 8, btn_y, bw, 36, function()
|
||
running = not running
|
||
if running then
|
||
last_lat = nil; last_lon = nil -- fresh segment on resume
|
||
end
|
||
redraw()
|
||
end)
|
||
ui.button("Reset", 12 + bw, btn_y, bw, 36, function()
|
||
total_km = 0; max_kmh = 0; elapsed_s = 0
|
||
last_lat = nil; last_lon = nil
|
||
running = false
|
||
save()
|
||
redraw()
|
||
sys.toast("Trip reset", 1200)
|
||
end)
|
||
|
||
timer.every(POLL_MS)
|
||
redraw()
|
||
end
|
||
|
||
function app.on_tick()
|
||
fix = sys.gps()
|
||
|
||
if running then
|
||
elapsed_s = elapsed_s + math.floor(POLL_MS / 1000)
|
||
|
||
if fix and fix.lat and fix.lon then
|
||
local spd = fix.speed_kmh or 0.0
|
||
if spd > max_kmh then max_kmh = spd end
|
||
|
||
if spd >= MIN_SPEED_KMH then
|
||
if last_lat then
|
||
local d = haversine(last_lat, last_lon, fix.lat, fix.lon)
|
||
if d > 0.003 then -- skip < 3 m to suppress stationary jitter
|
||
total_km = total_km + d
|
||
end
|
||
end
|
||
last_lat = fix.lat; last_lon = fix.lon
|
||
end
|
||
end
|
||
|
||
if elapsed_s % 10 == 0 then save() end
|
||
end
|
||
|
||
redraw()
|
||
end
|
||
|
||
function app.on_input(ev)
|
||
-- swipe down = shortcut to reset (only when stopped, to avoid accidents)
|
||
if ev.type == "swipe" and ev.dir == "down" and not running then
|
||
total_km = 0; max_kmh = 0; elapsed_s = 0
|
||
last_lat = nil; last_lon = nil
|
||
save(); redraw()
|
||
sys.toast("Trip reset", 1200)
|
||
end
|
||
end
|
||
|
||
function app.on_close()
|
||
save()
|
||
end
|
||
|
||
return app
|