merge upstream beta_99; fork v1.1, Russian v34

This commit is contained in:
shade committed 2026-10-22 21:45:36 +03:00
commit 2e2cccc5ef
300 files changed
+72599 -16487

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
+12 -5
View File
@@ -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__':
+2 -1
View File
@@ -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
View File
@@ -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
+28
View File
@@ -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
View File
@@ -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())