merge upstream beta_99; fork v1.1, Russian v34
This commit is contained in:
commit
2e2cccc5ef
300 files changed
+72599
-16487
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -121,8 +121,15 @@ def main():
|
||||
sys.exit('%s contains the raw-string delimiter' % aid)
|
||||
# An app that needs the extended SDK cannot run on a board without it, so it
|
||||
# is baked in only where it can (CAP_LUA_SDK_EXT, every board but the 2 MB
|
||||
# Heltec V4, the one with the least flash to give).
|
||||
entries.append((aid, name, ver, code, app.get('requires') == 'sdk_ext'))
|
||||
# Heltec V4, the one with the least flash to give). Likewise an app that reads
|
||||
# the board's OWN LoRa radio ("radio": "own": RF Monitor, Airtime, Wardrive)
|
||||
# only where there is one (CAP_RADIO_TOOLS; not the browser build).
|
||||
conds = []
|
||||
if app.get('requires') == 'sdk_ext':
|
||||
conds.append('CAP_LUA_SDK_EXT')
|
||||
if app.get('radio') == 'own':
|
||||
conds.append('CAP_RADIO_TOOLS')
|
||||
entries.append((aid, name, ver, code, ' && '.join(conds)))
|
||||
|
||||
if not entries:
|
||||
sys.exit('no app sources found under ' + APPS)
|
||||
@@ -143,7 +150,7 @@ def main():
|
||||
c = zlib.compressobj(9, zlib.DEFLATED, -15)
|
||||
z = c.compress(raw) + c.flush()
|
||||
zs[aid] = (z, len(raw))
|
||||
if ext: w.append('#if CAP_LUA_SDK_EXT')
|
||||
if ext: w.append('#if ' + ext)
|
||||
w.append('static const uint8_t kLuaZ_%s[%d] = { // %d B of Lua' % (aid, len(z), len(raw)))
|
||||
for i in range(0, len(z), 20):
|
||||
w.append(' ' + ','.join('0x%02x' % b for b in z[i:i + 20]) + ',')
|
||||
@@ -152,7 +159,7 @@ def main():
|
||||
w.append('')
|
||||
w.append('static const LuaBuiltinApp kLuaBuiltin[] = {')
|
||||
for aid, name, ver, _, ext in entries:
|
||||
if ext: w.append('#if CAP_LUA_SDK_EXT')
|
||||
if ext: w.append('#if ' + ext)
|
||||
z, n = zs[aid]
|
||||
w.append(' { "%s", "%s", "%s", kLuaZ_%s, %d, %d },' % (aid, name, ver, aid, len(z), n))
|
||||
if ext: w.append('#endif')
|
||||
@@ -166,7 +173,7 @@ def main():
|
||||
(os.path.relpath(OUT, ROOT), len(entries), total, sum(len(z) for z, _ in zs.values())))
|
||||
for aid, _, ver, code, ext in entries:
|
||||
print(' %-10s v%-4s %6d B -> %5d B%s' % (aid, ver, len(code), len(zs[aid][0]),
|
||||
' (extended SDK only)' if ext else ''))
|
||||
(' (only with ' + ext + ')') if ext else ''))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -414,7 +414,8 @@ generate_fallback_icons() {
|
||||
# some translation units and not others. 18/20/24 are the bigger UI-size presets
|
||||
# (every board with a text-size setting; the V4 has none, its flash is full).
|
||||
# DOC_CAPTURE builds carry them all: they render other boards' sizes and scales for review.
|
||||
ui_big="defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HAS_SENSECAP_INDICATOR) || defined(DOC_CAPTURE)"
|
||||
# WADA_WEB (the browser build, web/) is an 800x480 large-screen board like the CrowPanel 5.0.
|
||||
ui_big="defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HAS_SENSECAP_INDICATOR) || defined(DOC_CAPTURE) || defined(WADA_WEB)"
|
||||
for size in 12 14 16 28; do
|
||||
generate_ui "$size"
|
||||
done
|
||||
|
||||
+305
-28
@@ -108,6 +108,7 @@ end
|
||||
|
||||
-- wada.ui.list: rows with optional callbacks, same argument checks as uiList/lsAdd
|
||||
local lists = {}
|
||||
local map_obj, map_centers = nil, {}
|
||||
local function mklist(x, y, w, h)
|
||||
checkint(x, "list x"); checkint(y, "list y"); checkint(w, "list w"); checkint(h, "list h")
|
||||
assert(w > 0 and h > 0, "list: width and height must be positive")
|
||||
@@ -291,6 +292,7 @@ local function build_wada()
|
||||
wada.sys.caps = function() return { sdk_ext = cfg.caps.sdk_ext, keyboard = cfg.caps.keyboard,
|
||||
touch = cfg.caps.touch, sd = true, compass = cfg.caps.compass,
|
||||
accel = cfg.caps.accel, discover = cfg.caps.discover,
|
||||
map = cfg.caps.sdk_ext or false,
|
||||
sd_list = cfg.caps.sd_list or false,
|
||||
sd_clean = cfg.caps.sd_clean or false,
|
||||
audio = cfg.caps.audio or false,
|
||||
@@ -395,7 +397,8 @@ local function build_wada()
|
||||
storekv[k] = v end
|
||||
|
||||
wada.timer.every = function(ms) checkint(ms, "timer.every"); if ms < 33 then ms = 33 end; widgets.timer_ms = ms end
|
||||
wada.timer.stop = function() widgets.timer_ms = nil end
|
||||
wada.timer.stop = function() widgets.timer_ms = nil end
|
||||
wada.timer.after = function(ms, fn) checkint(ms, "timer.after ms"); assert(type(fn) == "function", "timer.after fn") end
|
||||
|
||||
if cfg.caps.audio then
|
||||
wada.audio = {}
|
||||
@@ -444,6 +447,29 @@ local function build_wada()
|
||||
wada.sd = { list = sdmock_list }
|
||||
if cfg.caps.sd_clean then wada.sd.check = sdmock_check; wada.sd.remove = sdmock_remove end
|
||||
end
|
||||
if cfg.caps.sdk_ext then
|
||||
local function mkmap(mx, my, mw, mh)
|
||||
if map_obj then error("only one map view at a time") end
|
||||
local m = { x = mx, y = my, w = mw, h = mh, _zoom = 10, _lat = 0, _lon = 0 }
|
||||
function m:center(lat, lon, z)
|
||||
self._lat = lat; self._lon = lon
|
||||
if z then self._zoom = z end
|
||||
map_centers[#map_centers + 1] = { lat = lat, lon = lon, z = self._zoom }
|
||||
end
|
||||
function m:zoom(z) if z then self._zoom = z else return self._zoom end end
|
||||
function m:marker(lat, lon, col, sz) end
|
||||
function m:line(la1, lo1, la2, lo2, col, w) end
|
||||
function m:clear() end
|
||||
function m:tiles() return cfg.map_tiles or 0 end
|
||||
function m:redraw() end
|
||||
function m:to_screen(lat, lon) return math.floor(mw / 2), math.floor(mh / 2) end
|
||||
function m:to_latlon(x, y) return self._lat + (mh/2-y)*0.001, self._lon + (x-mw/2)*0.001 end
|
||||
function m:close() map_obj = nil end
|
||||
map_obj = m
|
||||
return m
|
||||
end
|
||||
wada.map = { view = mkmap }
|
||||
end
|
||||
return wada
|
||||
end
|
||||
|
||||
@@ -477,6 +503,7 @@ local function reset_world()
|
||||
widgets = { canvases = 0, labels = 0, buttons = 0, scroll = false, timer_ms = nil }
|
||||
labels, buttons, toasts, drawlog = {}, {}, {}, { text = {}, circles = {}, ops = 0 }
|
||||
lists = {}
|
||||
map_obj, map_centers = nil, {}
|
||||
clock_ms = 1000
|
||||
dm_sent = {}
|
||||
audio_state = { state = "stopped", path = "", source = "", format = "", error = nil }
|
||||
@@ -1368,66 +1395,92 @@ scenarios.trip_tdeck = function()
|
||||
end
|
||||
|
||||
scenarios.trip_moving = function()
|
||||
-- simulate movement: distance and max speed accumulate
|
||||
-- distance accumulates; home bearing points back south; min altitude tracked;
|
||||
-- avg speed is non-zero and ≤ max speed
|
||||
cfg = { w = 240, h = 276, caps = { touch = true, keyboard = false, sdk_ext = true } }
|
||||
local step = 0
|
||||
cfg.gps = function()
|
||||
step = step + 1
|
||||
return { lat = 37.75 + step * 0.001, lon = -122.45, sats = 8, alt_m = 100 + step, speed_kmh = 30.0 + step }
|
||||
-- move north ~22 m per tick (0.0002° ≈ 22 m); speed_kmh = 45 > implied ~40 km/h
|
||||
-- so avg speed (from haversine) stays below reported max speed
|
||||
return { lat = 37.75 + step * 0.0002, lon = -122.45,
|
||||
sats = 8, alt_m = 100 - step, speed_kmh = 45.0 }
|
||||
end
|
||||
storekv = {}; wada = build_wada()
|
||||
local app = load_app()
|
||||
assert(guarded(BUDGET, app.on_open, cfg.w, cfg.h))
|
||||
buttons[1].fn() -- start tracking
|
||||
tick(app, 20) -- 20 GPS fixes, each ~111 m north → ~2.2 km total
|
||||
tick(app, 15) -- 15 fixes moving north ~22 m each → ~330 m
|
||||
local dump = label_dump()
|
||||
print(" after 20 ticks:", dump)
|
||||
print(" after 15 ticks:", dump)
|
||||
-- distance > 0
|
||||
assert(not dump:find("| 0 m |"), "expected non-zero distance after movement")
|
||||
assert(dump:find("km/h"), "expected max speed displayed")
|
||||
-- avg speed ≤ max speed (avg derived from Haversine ≈ 37 km/h, mock max = 45 km/h)
|
||||
local avg_s = dump:match("AVG SPEED | ([%d%.]+) km/h")
|
||||
local max_s = dump:match("MAX SPEED | ([%d%.]+) km/h")
|
||||
assert(avg_s and max_s, "expected AVG SPEED and MAX SPEED labels in dump")
|
||||
assert(tonumber(avg_s) <= tonumber(max_s),
|
||||
string.format("avg speed %s must be <= max speed %s", avg_s, max_s))
|
||||
-- home bearing: moved north, home is south → "S"
|
||||
assert(dump:find(" S"), "expected southward home bearing (1.1)")
|
||||
-- min altitude: descended from 99 to 85 over 15 steps
|
||||
assert(dump:find("85") or dump:find("min"), "expected min altitude tracked (1.1)")
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
end
|
||||
|
||||
scenarios.trip_persist = function()
|
||||
-- on_close saves; on_open restores
|
||||
-- on_close saves distance, max speed, elapsed time, min altitude and home;
|
||||
-- on_open restores all of them including home
|
||||
cfg = { w = 240, h = 276, caps = { touch = true, keyboard = false, sdk_ext = true } }
|
||||
local step = 0
|
||||
cfg.gps = function()
|
||||
step = step + 1
|
||||
return { lat = 37.75 + step * 0.001, lon = -122.45, sats = 7, alt_m = 50, speed_kmh = 15.0 }
|
||||
-- no speed_kmh → derived from positions; step*0.0002° ≈ 22 m/2 s ≈ 40 km/h
|
||||
return { lat = 37.75 + step * 0.0002, lon = -122.45, sats = 7, alt_m = 50 }
|
||||
end
|
||||
storekv = {}; wada = build_wada()
|
||||
local app = load_app()
|
||||
guarded(BUDGET, app.on_open, cfg.w, cfg.h)
|
||||
buttons[1].fn(); tick(app, 10) -- run ~1 km
|
||||
buttons[1].fn(); tick(app, 10) -- run ~200 m, home set on first fix
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
assert(storekv.trip_km_x1000 ~= nil and storekv.trip_km_x1000 > 0, "expected trip_km_x1000 saved")
|
||||
-- re-open same store → distance should be restored
|
||||
assert(storekv.trip_minalt ~= nil, "expected trip_minalt saved (1.1)")
|
||||
assert(storekv.trip_home_lat_e6 ~= nil, "expected home lat saved (1.1)")
|
||||
assert(storekv.trip_home_lon_e6 ~= nil, "expected home lon saved (1.1)")
|
||||
-- re-open same store → distance and home should be restored
|
||||
wada = build_wada()
|
||||
local app2 = load_app()
|
||||
guarded(BUDGET, app2.on_open, cfg.w, cfg.h)
|
||||
tick(app2, 2)
|
||||
local dump = label_dump()
|
||||
print(" restored:", dump)
|
||||
assert(not dump:find("| 0 m |"), "expected restored non-zero distance")
|
||||
-- home bearing should still show (not "acquiring" since home was persisted)
|
||||
assert(not dump:find("acquiring"), "home should be restored from store, not re-acquiring")
|
||||
if app2.on_close then guarded(BUDGET, app2.on_close) end
|
||||
end
|
||||
|
||||
scenarios.trip_reset = function()
|
||||
-- Reset button clears everything
|
||||
-- Reset clears distance, time, home bearing, min altitude
|
||||
cfg = { w = 240, h = 276, caps = { touch = true, keyboard = false, sdk_ext = true } }
|
||||
local step = 0
|
||||
cfg.gps = function()
|
||||
step = step + 1
|
||||
return { lat = 37.75 + step * 0.001, lon = -122.45, sats = 6, alt_m = 50, speed_kmh = 20.0 }
|
||||
-- no speed_kmh → derived from positions; ensures avg ≤ max always
|
||||
return { lat = 37.75 + step * 0.0002, lon = -122.45, sats = 6, alt_m = 50 }
|
||||
end
|
||||
storekv = {}; wada = build_wada()
|
||||
local app = load_app()
|
||||
guarded(BUDGET, app.on_open, cfg.w, cfg.h)
|
||||
buttons[1].fn(); tick(app, 10) -- accumulate some distance
|
||||
buttons[1].fn(); tick(app, 10) -- accumulate distance
|
||||
buttons[2].fn(); tick(app, 1) -- Reset
|
||||
local dump = label_dump()
|
||||
print(" after reset:", dump)
|
||||
assert(dump:find("0:00"), "expected zero time after reset")
|
||||
assert(dump:find("| 0 m |") or dump:find("0.00"), "expected zero distance after reset")
|
||||
-- home bearing should be cleared (1.1)
|
||||
assert(dump:find("| -- |") or dump:find("-- |") or dump:find("--"),
|
||||
"expected no home bearing after reset (1.1)")
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
end
|
||||
|
||||
@@ -1952,7 +2005,7 @@ local function pt_has_text(s)
|
||||
end
|
||||
|
||||
scenarios.protreck_tdeck = function()
|
||||
cfg = { w = 320, h = 240, caps = { touch = true, keyboard = false, sdk_ext = true } }
|
||||
cfg = { w = 320, h = 196, caps = { touch = true, keyboard = false, sdk_ext = true } }
|
||||
cfg.gps = function() return { lat = 48.85, lon = 2.35, sats = 8, alt_m = 35, speed_kmh = 0, course = 0 } end
|
||||
storekv = {}
|
||||
wada = build_wada()
|
||||
@@ -1961,7 +2014,8 @@ scenarios.protreck_tdeck = function()
|
||||
assert(widgets.canvases == 1, "expected 1 canvas")
|
||||
assert(widgets.timer_ms == 500, "tick rate must be 500 ms, got " .. tostring(widgets.timer_ms))
|
||||
tick(app, 5)
|
||||
-- swipe through all 4 tabs
|
||||
-- swipe through all 5 tabs (ASTRO→CMPAS→CHRONO→TIMER→ALTI→ASTRO)
|
||||
swipe(app, "left"); tick(app, 2) -- CMPAS
|
||||
swipe(app, "left"); tick(app, 2) -- CHRONO
|
||||
swipe(app, "left"); tick(app, 2) -- TIMER
|
||||
swipe(app, "left"); tick(app, 2) -- ALTI
|
||||
@@ -1978,9 +2032,11 @@ scenarios.protreck_v4 = function()
|
||||
local app = load_app()
|
||||
assert(guarded(BUDGET, app.on_open, cfg.w, cfg.h))
|
||||
tick(app, 3)
|
||||
swipe(app, "left"); tick(app, 2) -- CMPAS
|
||||
swipe(app, "left"); tick(app, 2) -- CHRONO
|
||||
swipe(app, "right"); tick(app, 2) -- back CMPAS
|
||||
swipe(app, "right"); tick(app, 2) -- back ASTRO
|
||||
swipe(app, "right"); tick(app, 2) -- wrap to ALTI
|
||||
swipe(app, "right"); tick(app, 2) -- wrap to ALTI (tab 5)
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
print(" v4: OK")
|
||||
end
|
||||
@@ -1993,7 +2049,8 @@ scenarios.protreck_no_gps = function()
|
||||
local app = load_app()
|
||||
assert(guarded(BUDGET, app.on_open, cfg.w, cfg.h))
|
||||
tick(app, 5)
|
||||
-- must render without crash even when GPS is absent
|
||||
-- must render without crash even when GPS is absent on all tabs
|
||||
swipe(app, "left"); tick(app, 2) -- CMPAS (no heading)
|
||||
swipe(app, "left"); tick(app, 2) -- CHRONO
|
||||
swipe(app, "left"); tick(app, 2) -- TIMER
|
||||
swipe(app, "left"); tick(app, 2) -- ALTI
|
||||
@@ -2008,17 +2065,19 @@ scenarios.protreck_chrono = function()
|
||||
wada = build_wada()
|
||||
local app = load_app()
|
||||
assert(guarded(BUDGET, app.on_open, cfg.w, cfg.h))
|
||||
-- switch to CHRONO tab
|
||||
swipe(app, "left"); tick(app, 1)
|
||||
-- start chrono
|
||||
-- switch to CHRONO tab (tab 3: skip CMPAS first)
|
||||
swipe(app, "left"); swipe(app, "left"); tick(app, 1)
|
||||
-- start chrono (tap = down + up at same position)
|
||||
local ops0 = drawlog.ops
|
||||
guarded(BUDGET, app.on_input, { type = "down", x = 0, y = 0 })
|
||||
guarded(BUDGET, app.on_input, { type = "up", x = 0, y = 0 })
|
||||
tick(app, 3)
|
||||
-- lap
|
||||
swipe(app, "up"); tick(app, 1)
|
||||
assert(pt_has_text("Lap 1"), "lap must appear in draw after swipe-up")
|
||||
-- stop
|
||||
guarded(BUDGET, app.on_input, { type = "down", x = 0, y = 0 })
|
||||
guarded(BUDGET, app.on_input, { type = "up", x = 0, y = 0 })
|
||||
tick(app, 1)
|
||||
-- reset
|
||||
swipe(app, "down"); tick(app, 1)
|
||||
@@ -2033,15 +2092,17 @@ scenarios.protreck_timer = function()
|
||||
wada = build_wada()
|
||||
local app = load_app()
|
||||
assert(guarded(BUDGET, app.on_open, cfg.w, cfg.h))
|
||||
-- switch to TIMER
|
||||
swipe(app, "left"); swipe(app, "left"); tick(app, 1)
|
||||
-- switch to TIMER (tab 4)
|
||||
swipe(app, "left"); swipe(app, "left"); swipe(app, "left"); tick(app, 1)
|
||||
-- cycle preset a few times
|
||||
swipe(app, "up"); swipe(app, "up"); tick(app, 1)
|
||||
-- start timer
|
||||
-- start timer (tap = down + up at same position)
|
||||
guarded(BUDGET, app.on_input, { type = "down", x = 0, y = 0 })
|
||||
guarded(BUDGET, app.on_input, { type = "up", x = 0, y = 0 })
|
||||
tick(app, 3)
|
||||
-- stop
|
||||
guarded(BUDGET, app.on_input, { type = "down", x = 0, y = 0 })
|
||||
guarded(BUDGET, app.on_input, { type = "up", x = 0, y = 0 })
|
||||
tick(app, 1)
|
||||
-- reset
|
||||
swipe(app, "down"); tick(app, 1)
|
||||
@@ -2061,8 +2122,8 @@ scenarios.protreck_alti = function()
|
||||
wada = build_wada()
|
||||
local app = load_app()
|
||||
assert(guarded(BUDGET, app.on_open, cfg.w, cfg.h))
|
||||
-- switch to ALTI
|
||||
swipe(app, "left"); swipe(app, "left"); swipe(app, "left"); tick(app, 1)
|
||||
-- switch to ALTI (tab 5)
|
||||
swipe(app, "left"); swipe(app, "left"); swipe(app, "left"); swipe(app, "left"); tick(app, 1)
|
||||
tick(app, 20) -- accumulate samples
|
||||
assert(storekv.alti_max ~= nil, "alti_max must be saved to store after GPS samples")
|
||||
assert(type(storekv.alti_max) == "number", "alti_max must be a number")
|
||||
@@ -2080,8 +2141,8 @@ scenarios.protreck_cost = function()
|
||||
wada = build_wada()
|
||||
local app = load_app()
|
||||
assert(guarded(BUDGET, app.on_open, cfg.w, cfg.h))
|
||||
-- switch to ALTI (heaviest draw: graph over 60 samples)
|
||||
swipe(app, "left"); swipe(app, "left"); swipe(app, "left")
|
||||
-- switch to ALTI (heaviest draw: graph over 60 samples) - tab 5
|
||||
swipe(app, "left"); swipe(app, "left"); swipe(app, "left"); swipe(app, "left")
|
||||
local worst = 0
|
||||
for i = 1, 80 do
|
||||
local n = 0
|
||||
@@ -2097,6 +2158,220 @@ scenarios.protreck_cost = function()
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
end
|
||||
|
||||
scenarios.protreck_alarm = function()
|
||||
cfg = { w = 320, h = 240, caps = { touch = true, keyboard = false, sdk_ext = true } }
|
||||
cfg.gps = function() return nil end
|
||||
storekv = {}
|
||||
wada = build_wada()
|
||||
local app = load_app()
|
||||
assert(guarded(BUDGET, app.on_open, cfg.w, cfg.h))
|
||||
tick(app, 2)
|
||||
|
||||
-- short press: cycle alarm slot A1 → A2
|
||||
guarded(BUDGET, app.on_input, { type = "down", x = 0, y = 0 })
|
||||
guarded(BUDGET, app.on_input, { type = "up", x = 0, y = 0 }) -- same clock → dur=0 → cycle
|
||||
tick(app, 1)
|
||||
assert(pt_has_text("A2"), "A2 must be highlighted after first tap (cycle to slot 2)")
|
||||
|
||||
-- swipe up: +15 min on A2 (default 08:00 → 08:15)
|
||||
swipe(app, "up"); tick(app, 1)
|
||||
assert(storekv.al2h == 8 and storekv.al2m == 15,
|
||||
string.format("A2 must be 08:15 after swipe-up, got %s:%s", tostring(storekv.al2h), tostring(storekv.al2m)))
|
||||
|
||||
-- swipe down: −15 min on A2 (08:15 → 08:00)
|
||||
swipe(app, "down"); tick(app, 1)
|
||||
assert(storekv.al2h == 8 and storekv.al2m == 0, "A2 must be 08:00 after swipe-down")
|
||||
|
||||
-- long press (hold ≥500ms): toggle A2 on/off
|
||||
guarded(BUDGET, app.on_input, { type = "down", x = 0, y = 0 })
|
||||
clock_ms = clock_ms + 600 -- simulate 600ms hold
|
||||
guarded(BUDGET, app.on_input, { type = "up", x = 0, y = 0 })
|
||||
tick(app, 1)
|
||||
assert(storekv.al2on == 1, "A2 must be ON after long-press toggle")
|
||||
|
||||
-- long press again: toggle A2 off
|
||||
guarded(BUDGET, app.on_input, { type = "down", x = 0, y = 0 })
|
||||
clock_ms = clock_ms + 600
|
||||
guarded(BUDGET, app.on_input, { type = "up", x = 0, y = 0 })
|
||||
tick(app, 1)
|
||||
assert(storekv.al2on == 0, "A2 must be OFF after second long-press toggle")
|
||||
|
||||
print(" alarm: 3-slot cycle + long-press toggle + swipe ±15min OK")
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
end
|
||||
|
||||
scenarios.protreck_compass = function()
|
||||
-- x=1, y=1 → atan(c.y, -c.x) = atan(1,-1) = 135°
|
||||
cfg = { w = 320, h = 240, caps = { touch = true, keyboard = false, sdk_ext = true, compass = true } }
|
||||
cfg.gps = function() return { lat = 45.0, lon = 9.0, sats = 7, alt_m = 200, speed_kmh = 4, course = 135 } end
|
||||
cfg.compass = function() return { x = 1, y = 1, z = 0 } end
|
||||
storekv = {}
|
||||
wada = build_wada()
|
||||
local app = load_app()
|
||||
assert(guarded(BUDGET, app.on_open, cfg.w, cfg.h))
|
||||
tick(app, 2)
|
||||
|
||||
-- switch to CMPAS (tab 2)
|
||||
swipe(app, "left"); tick(app, 2)
|
||||
assert(drawlog.ops > 10, "CMPAS tab must perform draw ops")
|
||||
|
||||
-- heading readout must appear (135 → text "135")
|
||||
local found_hdg = false
|
||||
for i = #drawlog.text, math.max(1, #drawlog.text-20), -1 do
|
||||
if drawlog.text[i] == "135" then found_hdg = true; break end
|
||||
end
|
||||
assert(found_hdg, "heading value '135' must appear in draw text")
|
||||
|
||||
-- tap: set waypoint from current GPS position (tap = down + up at same position)
|
||||
guarded(BUDGET, app.on_input, { type = "down", x = 0, y = 0 })
|
||||
guarded(BUDGET, app.on_input, { type = "up", x = 0, y = 0 })
|
||||
tick(app, 1)
|
||||
assert(storekv.wpt_lat ~= nil and storekv.wpt_lat ~= "",
|
||||
"tap on CMPAS must save waypoint to store")
|
||||
assert(math.abs(tonumber(storekv.wpt_lat) - 45.0) < 0.01, "saved waypoint lat must match GPS")
|
||||
|
||||
-- bearing line must now appear (wpt_lat is set)
|
||||
tick(app, 2)
|
||||
-- tap again: clear waypoint
|
||||
guarded(BUDGET, app.on_input, { type = "down", x = 0, y = 0 })
|
||||
guarded(BUDGET, app.on_input, { type = "up", x = 0, y = 0 })
|
||||
tick(app, 1)
|
||||
assert(storekv.wpt_lat == "" or storekv.wpt_lat == nil, "second tap must clear waypoint")
|
||||
|
||||
print(" compass: rose draw + heading readout + waypoint set/clear OK")
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
end
|
||||
|
||||
scenarios.protreck_deniv = function()
|
||||
-- 8 GPS calls are consumed by on_open+swipes (draw_status_bar etc.) before the
|
||||
-- first on_tick altimeter sample arrives at step 9; put 110 there so alti_max
|
||||
-- is captured, then let the sequence end and return nil for the reset check.
|
||||
local alt_seq = { 100, 102, 105, 103, 101, 104, 108, 106, 110, 104 }
|
||||
local step = 0
|
||||
cfg = { w = 320, h = 240, caps = { touch = true, keyboard = false, sdk_ext = true } }
|
||||
cfg.gps = function()
|
||||
step = step + 1
|
||||
if step > #alt_seq then return nil end -- stop providing GPS after the sequence
|
||||
return { lat = 46.0, lon = 7.0, sats = 9, alt_m = alt_seq[step], speed_kmh = 2, course = 0 }
|
||||
end
|
||||
storekv = {}
|
||||
wada = build_wada()
|
||||
local app = load_app()
|
||||
assert(guarded(BUDGET, app.on_open, cfg.w, cfg.h))
|
||||
-- switch to ALTI
|
||||
swipe(app, "left"); swipe(app, "left"); swipe(app, "left"); swipe(app, "left")
|
||||
tick(app, #alt_seq + 2)
|
||||
-- déniv+ should be positive (we climbed overall: 100→110)
|
||||
-- we can check that alti_max was saved and min/max differ
|
||||
assert(storekv.alti_max ~= nil and tonumber(storekv.alti_max) >= 108,
|
||||
"alti_max should be at least 108, got " .. tostring(storekv.alti_max))
|
||||
-- reset clears state
|
||||
swipe(app, "down"); tick(app, 2)
|
||||
assert(tonumber(storekv.alti_min) >= 99990, "alti_min must be reset to ~99999")
|
||||
print(" deniv: sampling + reset OK max=" .. tostring(storekv.alti_max))
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
end
|
||||
|
||||
-- ---- MapFetch scenarios -----------------------------------------------------
|
||||
|
||||
local function mapfetch_open(w, h, gps_lat, gps_lon, map_tiles)
|
||||
cfg = {
|
||||
w = w, h = h,
|
||||
caps = { sdk_ext = true, keyboard = true, touch = true },
|
||||
map_tiles = map_tiles or 0,
|
||||
}
|
||||
if gps_lat then cfg.gps = function() return { lat = gps_lat, lon = gps_lon, lat_e6 = 0, lon_e6 = 0 } end end
|
||||
storekv = {}; wada = build_wada()
|
||||
local app = load_app()
|
||||
assert(guarded(BUDGET, app.on_open, w, h))
|
||||
return app
|
||||
end
|
||||
|
||||
scenarios.mapfetch_tdeck = function()
|
||||
local app = mapfetch_open(320, 240, 48.8566, 2.3522) -- T-Deck, GPS Paris
|
||||
assert(widgets.labels >= 2, "need top+bottom labels")
|
||||
assert(map_obj ~= nil, "map view must be created")
|
||||
local found = false
|
||||
for _, l in ipairs(labels) do if l.text:find("5km") then found = true end end
|
||||
assert(found, "setup label should show default 5km radius")
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
end
|
||||
|
||||
scenarios.mapfetch_no_map = function()
|
||||
cfg = { w = 320, h = 240, caps = { sdk_ext = false, keyboard = false, touch = true } }
|
||||
storekv = {}; wada = build_wada()
|
||||
local app = load_app()
|
||||
guarded(BUDGET, app.on_open, 320, 240)
|
||||
assert(map_obj == nil, "no map view on non-ext board")
|
||||
local found = false
|
||||
for _, l in ipairs(labels) do if l.text:find("No map") or l.text:find("not available") then found = true end end
|
||||
assert(found, "should show 'no map support' label when caps.map is false")
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
end
|
||||
|
||||
scenarios.mapfetch_no_gps = function()
|
||||
local app = mapfetch_open(320, 240, nil, nil) -- no GPS
|
||||
send(app, { type = "down", x = 160, y = 120 })
|
||||
local found = false
|
||||
for _, l in ipairs(labels) do if l.text:find("GPS") or l.text:find("fix") then found = true end end
|
||||
assert(found, "should warn about missing GPS when starting")
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
end
|
||||
|
||||
scenarios.mapfetch_swipe = function()
|
||||
local app = mapfetch_open(320, 240, 48.8566, 2.3522)
|
||||
local lbl_before = labels[1] and labels[1].text or ""
|
||||
swipe(app, "up") -- increase radius
|
||||
local lbl_after = labels[1] and labels[1].text or ""
|
||||
assert(lbl_before ~= lbl_after, "swipe up should change radius display")
|
||||
swipe(app, "down") -- decrease radius back
|
||||
swipe(app, "left") -- increase max zoom
|
||||
swipe(app, "right")-- decrease max zoom back
|
||||
assert(map_obj ~= nil, "map view still present after swipes")
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
end
|
||||
|
||||
scenarios.mapfetch_logic = function()
|
||||
local app = mapfetch_open(320, 240, 48.8566, 2.3522, 1) -- map_tiles=1 (all cached)
|
||||
send(app, { type = "down", x = 160, y = 120 }) -- start download
|
||||
local prev = #map_centers
|
||||
for _ = 1, 5 do tick(app, 1, 3000) end
|
||||
assert(#map_centers > prev, "map:center should be called as tiles are visited")
|
||||
for _, c in ipairs(map_centers) do
|
||||
assert(type(c.lat) == "number" and type(c.lon) == "number", "tile center must be numeric")
|
||||
assert(c.z >= 10 and c.z <= 15, "zoom must be in expected range")
|
||||
end
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
end
|
||||
|
||||
scenarios.mapfetch_done = function()
|
||||
local app = mapfetch_open(320, 240, 48.8566, 2.3522, 1)
|
||||
swipe(app, "down") -- radius: 5km → 2km
|
||||
swipe(app, "right") -- zoom: z14 → z13
|
||||
send(app, { type = "down", x = 160, y = 120 }) -- start
|
||||
local done = false
|
||||
for _ = 1, 200 do
|
||||
tick(app, 1, 3000)
|
||||
for _, l in ipairs(labels) do if l.text:find("Done") then done = true end end
|
||||
if done then break end
|
||||
end
|
||||
assert(done, "app should reach Done state after all tiles visited")
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
assert(not cfg.awake, "keep_awake must be off after done")
|
||||
end
|
||||
|
||||
scenarios.mapfetch_cost = function()
|
||||
local app = mapfetch_open(320, 240, 48.8566, 2.3522)
|
||||
send(app, { type = "down", x = 160, y = 120 })
|
||||
local ops0 = drawlog.ops
|
||||
tick(app, 1, 3000)
|
||||
local cost = drawlog.ops - ops0
|
||||
assert(cost < 500, string.format("on_tick draw ops %d >= 500", cost))
|
||||
if app.on_close then guarded(BUDGET, app.on_close) end
|
||||
end
|
||||
|
||||
-- ---- MapFetch scenarios end --------------------------------------------------
|
||||
|
||||
-- True (and says so) when this app has no scenarios of its own. Returning a
|
||||
-- truthy value makes the dispatch below pick an EMPTY list rather than running
|
||||
-- someone else's tests against it.
|
||||
@@ -2117,9 +2392,11 @@ local order = APP_PATH:find("/sdscan/", 1, true)
|
||||
or APP_PATH:find("/tripodometer/", 1, true)
|
||||
and { "trip_v4", "trip_tdeck", "trip_moving", "trip_persist", "trip_reset", "trip_cost" }
|
||||
or APP_PATH:find("/protreck/", 1, true)
|
||||
and { "protreck_tdeck", "protreck_v4", "protreck_no_gps", "protreck_chrono", "protreck_timer", "protreck_alti", "protreck_cost" }
|
||||
and { "protreck_tdeck", "protreck_v4", "protreck_no_gps", "protreck_chrono", "protreck_timer", "protreck_alti", "protreck_alarm", "protreck_compass", "protreck_deniv", "protreck_cost" }
|
||||
or APP_PATH:find("/ping/", 1, true)
|
||||
and { "ping_tdeck", "ping_v4", "ping_no_contacts", "ping_round_trip", "ping_auto_reply", "ping_cost" }
|
||||
or APP_PATH:find("/mapfetch/", 1, true)
|
||||
and { "mapfetch_tdeck", "mapfetch_no_map", "mapfetch_no_gps", "mapfetch_swipe", "mapfetch_logic", "mapfetch_done", "mapfetch_cost" }
|
||||
-- The compass list below is gpscompass's own, NOT a default. An app with no
|
||||
-- branch used to fall through to it and "fail" on tests written for a
|
||||
-- different app -- six of the store apps do, so a full run came back red and
|
||||
|
||||
@@ -38,6 +38,34 @@ else
|
||||
ARCH="$OUT/releases/BETA"; FEED="$OUT/latest-beta"
|
||||
fi
|
||||
|
||||
# 0. Web build gate (beta only, optional): the browser build compiles the same UI
|
||||
# against the web stand-in (web/mesh/WebMesh.*), so a UI change the stand-in lacks
|
||||
# breaks it, and its tours have caught memory bugs the boards hide. Runs when emcc
|
||||
# is installed and fails the release on a broken web build; skipped, with a note,
|
||||
# without emcc. The browser tour needs Playwright (web/node_modules) and is skipped
|
||||
# without it. Nothing here is published: web/dist is deployed separately
|
||||
# (scripts/deploy-webclient.sh). WADA_SKIP_WEB=1 skips the whole gate.
|
||||
if [ "$MODE" = "beta" ] && [ -z "${WADA_SKIP_WEB:-}" ]; then
|
||||
if command -v emcc >/dev/null && command -v emcmake >/dev/null; then
|
||||
echo "web gate: web/build.sh web"
|
||||
"$ROOT/web/build.sh" web || { echo "ERROR: the web build is broken (web/build.sh web); WADA_SKIP_WEB=1 skips this gate"; exit 1; }
|
||||
# The native headless tour (same UI, host build): it is what caught the #629 double
|
||||
# free as a crash, without a browser.
|
||||
echo "web gate: web/build.sh smoke"
|
||||
"$ROOT/web/build.sh" smoke || { echo "ERROR: the headless UI tour failed (web/build.sh smoke); WADA_SKIP_WEB=1 skips this gate"; exit 1; }
|
||||
if [ -d "$ROOT/web/node_modules/playwright" ]; then
|
||||
WEB_SHOTS="$(mktemp -d)" # keep the tracked web/docs shots out of a release run
|
||||
echo "web gate: web/build.sh webtest"
|
||||
"$ROOT/web/build.sh" webtest --shots "$WEB_SHOTS" || { echo "ERROR: the web acceptance tour failed (web/build.sh webtest); WADA_SKIP_WEB=1 skips this gate"; exit 1; }
|
||||
rm -rf "$WEB_SHOTS"
|
||||
else
|
||||
echo "note: web gate: Playwright not installed (web/node_modules), browser tour skipped"
|
||||
fi
|
||||
else
|
||||
echo "note: web gate skipped: emcc not found (brew install emscripten to check the web build)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 1. Pull the current published tree so listings stay complete across tags.
|
||||
if [ -n "$DEST" ]; then
|
||||
mkdir -p "$OUT"
|
||||
|
||||
+3
-1
@@ -116,7 +116,9 @@ class Link:
|
||||
end = time.time() + timeout
|
||||
out = []
|
||||
while time.time() < end and not out:
|
||||
chunk = self.ser.read(4096)
|
||||
# what has arrived, or block for the first byte: read(4096) alone waited out
|
||||
# the 50 ms timeout on every short reply (half of each upload's time)
|
||||
chunk = self.ser.read(max(1, self.ser.in_waiting))
|
||||
if chunk:
|
||||
self.buf.extend(chunk)
|
||||
out.extend(self._frames())
|
||||
|
||||
Reference in new issue
Block a user