Files
SamandClaude Sonnet 4.6 9b7ca27747 feat(apps): Trip Odometer 1.0 — GPS trip computer as a Lua app
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>
2026-10-02 22:41:14 +02:00

177 lines
4.8 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 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