Files
rootandClaude Sonnet 4.6 7254b9cca6 feat(apps): Ping 1.0 — mesh DM ping with GNSS position and RTT
Send a DM to any contact, measure round-trip time.  Both devices
exchange their GPS coordinates; the result shows RTT, distance and
cardinal bearing when both have a fix.  The app also auto-replies to
incoming pings so two devices running Ping can time each other.

Harness additions: send_dm mock, wada.geo (haversine), deliver_dm
helper, and 6 scenarios (tdeck, v4, no_contacts, round_trip,
auto_reply, cost) — all pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-02 22:50:22 +02:00

170 lines
4.9 KiB
Lua

-- Ping — wadamesh Lua app
-- Send a DM to any contact and measure round-trip time.
-- Both devices exchange their GPS position; the result shows RTT,
-- distance and cardinal bearing when both have a fix.
-- The app also auto-replies to incoming pings from other Ping users.
-- Requires: sdk_ext
local ui, sys, store, timer = wada.ui, wada.sys, wada.store, wada.timer
local mesh = wada.mesh
local geo = wada.geo -- great-circle helpers; nil on firmware without sdk_ext
local C = ui.colors
local app = {}
local PING_PFX = "WADAPING:"
local PONG_PFX = "WADAPONG:"
local W, H
local gps_lbl
local rows = {} -- {name, result_lbl} (parallel with contacts table)
local pending = {} -- name → {sent_ms, lat_e6, lon_e6}
-- ---- helpers ---------------------------------------------------------------
local function gps_line()
local fix = sys.gps()
if not fix then return "no GPS fix" end
local ns = fix.lat >= 0 and "N" or "S"
local ew = fix.lon >= 0 and "E" or "W"
return string.format("%.4f°%s %.4f°%s %d sats",
math.abs(fix.lat), ns, math.abs(fix.lon), ew, fix.sats or 0)
end
local function dist_str(m)
if m < 1000 then return string.format("%dm", math.floor(m + 0.5))
else return string.format("%.1fkm", m / 1000.0) end
end
local function find_row(name)
for _, r in ipairs(rows) do
if r.name == name then return r end
end
end
local function set_row_result(name, rtt_ms, dist_m, cardinal)
local r = find_row(name)
if not r then return end
if rtt_ms then
local s = tostring(rtt_ms) .. "ms"
if dist_m and dist_m > 0 then
s = s .. " " .. dist_str(dist_m)
if cardinal then s = s .. " " .. cardinal end
end
r.result_lbl:set(s)
r.result_lbl:color(C.good)
else
r.result_lbl:set("waiting…")
r.result_lbl:color(C.sub)
end
end
local function do_ping(name)
local fix = sys.gps()
local lat6 = fix and fix.lat_e6 or 0
local lon6 = fix and fix.lon_e6 or 0
local ts = sys.millis()
local msg = string.format("%s%d:%d:%d", PING_PFX, ts, lat6, lon6)
local ok, err = mesh.send_dm(name, msg)
if ok then
pending[name] = { sent_ms = ts, lat_e6 = lat6, lon_e6 = lon6 }
set_row_result(name, nil, nil, nil) -- "waiting…"
sys.toast("Ping → " .. name, 800)
else
sys.toast("Error: " .. tostring(err), 2000)
end
end
-- ---- message handler -------------------------------------------------------
function app.on_message(m)
if m.kind ~= "dm" then return end
local text = tostring(m.text or "")
-- ---- incoming PING: echo back with our position -------------------------
if text:sub(1, #PING_PFX) == PING_PFX then
local ts_s, _, _ = text:sub(#PING_PFX + 1):match("(-?%d+):(-?%d+):(-?%d+)")
if ts_s then
local fix = sys.gps()
local my6lat = fix and fix.lat_e6 or 0
local my6lon = fix and fix.lon_e6 or 0
mesh.send_dm(m.sender, string.format("%s%s:%d:%d", PONG_PFX, ts_s, my6lat, my6lon))
end
return
end
-- ---- incoming PONG: compute RTT + distance ------------------------------
if text:sub(1, #PONG_PFX) == PONG_PFX then
local ts_s, their_lat_s, their_lon_s =
text:sub(#PONG_PFX + 1):match("(-?%d+):(-?%d+):(-?%d+)")
if not ts_s then return end
local sent_ms = tonumber(ts_s)
local rtt = sys.millis() - sent_ms
local dist_m, cardinal
local p = pending[m.sender]
if p and geo and tonumber(their_lat_s) ~= 0 and p.lat_e6 ~= 0 then
local my_lat = p.lat_e6 / 1000000.0
local my_lon = p.lon_e6 / 1000000.0
local th_lat = tonumber(their_lat_s) / 1000000.0
local th_lon = tonumber(their_lon_s) / 1000000.0
dist_m = geo.distance(my_lat, my_lon, th_lat, th_lon)
local brg = geo.bearing(my_lat, my_lon, th_lat, th_lon)
cardinal = geo.cardinal(brg)
end
pending[m.sender] = nil
set_row_result(m.sender, rtt, dist_m, cardinal)
local notice = m.sender .. ": " .. rtt .. "ms"
if dist_m then notice = notice .. " " .. dist_str(dist_m) end
sys.toast(notice, 2500)
return
end
end
-- ---- lifecycle -------------------------------------------------------------
function app.on_open(w, h)
W, H = w, h
ui.scroll(true)
local cy = 6
gps_lbl = ui.label(gps_line(), 6, cy, 11, C.sub)
gps_lbl:width(w - 12)
cy = cy + 20
local contacts = mesh.contacts()
if #contacts == 0 then
ui.label("No contacts visible", 8, cy, 12, C.sub)
timer.every(5000)
return
end
for _, ct in ipairs(contacts) do
local name = ct.name
local nl = ui.label(name, 8, cy, 13, C.text)
nl:width(w - 68)
local rl = ui.label("—", 8, cy + 16, 11, C.sub)
rl:width(w - 68)
local cap = name -- capture for closure
ui.button("Ping", w - 60, cy + 4, 52, 28, function()
do_ping(cap)
end)
rows[#rows + 1] = { name = name, result_lbl = rl }
cy = cy + 44
end
timer.every(5000)
end
function app.on_tick(dt)
if gps_lbl then gps_lbl:set(gps_line()) end
end
function app.on_close() end
return app