Compare commits
No files matched your search
@@ -21,6 +21,21 @@ Everything below only fires while you're **not** actively typing into a text
|
||||
field — if a field is focused and you're typing into it, letters type
|
||||
normally and these combos step out of the way.
|
||||
|
||||
## Bottom menu shortcuts
|
||||
|
||||
The five main screens show their keyboard shortcuts directly in the bottom
|
||||
bar:
|
||||
|
||||
```text
|
||||
[mail] M [contacts] C [home] H [map] A [settings] S
|
||||
```
|
||||
|
||||
Press **M**, **C**, **H**, **A**, or **S** to jump to that screen. These
|
||||
shortcuts only work while one of the five main screens is at its top level.
|
||||
They do nothing inside an open chat, settings detail, app page, or popup, so a
|
||||
letter cannot unexpectedly close the inner screen. They also remain inactive
|
||||
while editing a text field, where the keys type normally.
|
||||
|
||||
## Rotary encoder
|
||||
|
||||
| Gesture | Action | Keyboard equivalent |
|
||||
@@ -28,16 +43,16 @@ normally and these combos step out of the way.
|
||||
| Turn | Move focus to the next/previous item on screen | **Fn (Alt) tapped alone** moves forward one step (NEXT only — no keyboard way to go backward) |
|
||||
| Short click | Select / confirm the focused item | **Enter** |
|
||||
| Hold ~1 s, then release | **Back**: closes a popup → closes an open chat → goes Home → Esc (whichever applies first) | **Backspace held ~1 s** |
|
||||
| **Fn (Alt) + turn**, on a main tab | Jump directly between the 5 main tabs (Chats / Contacts / Home / Map / Settings) | none — encoder only |
|
||||
| **Fn (Alt) + turn**, on a main tab | Move between the 5 main tabs (Mail / Contacts / Home / Map / Settings) | **M / C / H / A / S** jumps directly |
|
||||
| **Fn (Alt) + turn**, inside a settings page or chat | Scroll the page up/down | none — encoder only |
|
||||
| Turn, with a dropdown open | Scroll through the dropdown's options | none — encoder only |
|
||||
| Turn, with the accent picker open | Cycle through the accent variants (see below) | none — encoder only (Fn+Space to enter is shared) |
|
||||
| Turn, with the @-mention list open | Cycle through matching contacts (see below) | none — encoder only |
|
||||
| Turn, at the oldest/newest **loaded** message in an open chat | Loads more history in that direction and keeps going — see [Chat screen](#chat-screen) | none — encoder only |
|
||||
|
||||
The knob doubles as your only way to reach the bottom tab bar — since
|
||||
there's no touch to tap an icon, **Fn+turn** while on any main tab is the
|
||||
fastest way to switch sections.
|
||||
Since there's no touch to tap the bottom bar, use its visible **M / C / H / A /
|
||||
S** shortcuts to jump directly or **Fn+turn** to move through the tabs in
|
||||
order.
|
||||
|
||||
## Chat screen
|
||||
|
||||
@@ -193,9 +208,13 @@ While the **Map** tab is active:
|
||||
|
||||
- **W** — pan north
|
||||
- **A** — pan west
|
||||
- **S** — pan south
|
||||
- **X** — pan south
|
||||
- **D** — pan east
|
||||
|
||||
`X` replaces the usual `S` position because **S** is the visible Settings
|
||||
shortcut. Pressing **A** on another main screen opens Map; once Map is already
|
||||
active, **A** pans west.
|
||||
|
||||
Each press shifts the view a quarter-screen and reloads tiles/markers as
|
||||
needed.
|
||||
|
||||
@@ -216,6 +235,7 @@ dimmable brightness curve.
|
||||
|
||||
| Input | Action |
|
||||
|---|---|
|
||||
| M / C / H / A / S (top-level main screens only) | Open Mail / Contacts / Home / Map / Settings |
|
||||
| Turn encoder (or Fn tap = NEXT only) | Move focus |
|
||||
| Turn, at the loaded edge of a chat | Load more history, keep moving — only exits the list at the true oldest/newest message |
|
||||
| Click encoder (or Enter) | Select / confirm |
|
||||
@@ -236,4 +256,4 @@ dimmable brightness curve.
|
||||
| Space (double-tap) | Switch keyboard language |
|
||||
| Space (hold 1s) | Lock screen |
|
||||
| Q / E (slider focused) | Decrease / increase |
|
||||
| W / A / S / D (Map tab) | Pan map north/west/south/east |
|
||||
| W / A / X / D (Map tab) | Pan map north/west/south/east |
|
||||
@@ -1,5 +1,6 @@
|
||||
{"apps":[
|
||||
{"id":"monitor","name":"RF Monitor","ver":"1.3","desc":"Scrollable: RSSI/noise chart with dBm axis, link margin, RX rate, radio params, heard feed."},
|
||||
{"id":"airtime","name":"Airtime","ver":"1.3","desc":"Utilization bars with a % axis, rx/tx split, duty ceiling, budget. Reset button top-right."},
|
||||
{"id":"snake","name":"Snake","ver":"1.0","desc":"The classic, in Lua. Swipe to steer."}
|
||||
{"id":"snake","name":"Snake","ver":"1.0","desc":"The classic, in Lua. Swipe to steer."},
|
||||
{"id":"sdktest","name":"SDK Test","ver":"1.1","desc":"Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, file access and its limits, keyboard input. Has a button that TRANSMITS a test message to Public (asks permission first)."}
|
||||
]}
|
||||
@@ -973,3 +973,15 @@ zoom %d мащаб %d
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
Save FAIL x%u
|
||||
Save migrating
|
||||
Saved %s
|
||||
Sort discovered
|
||||
Sort by
|
||||
Peers
|
||||
Favorites
|
||||
Has location
|
||||
Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
@@ -973,3 +973,15 @@ zoom %d Zoom %d
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
Save FAIL x%u
|
||||
Save migrating
|
||||
Saved %s
|
||||
Sort discovered
|
||||
Sort by
|
||||
Peers
|
||||
Favorites
|
||||
Has location
|
||||
Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
@@ -973,3 +973,15 @@ zoom %d ζουμ %d
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
Save FAIL x%u
|
||||
Save migrating
|
||||
Saved %s
|
||||
Sort discovered
|
||||
Sort by
|
||||
Peers
|
||||
Favorites
|
||||
Has location
|
||||
Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
@@ -973,3 +973,15 @@ zoom %d zoom %d
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
Save FAIL x%u
|
||||
Save migrating
|
||||
Saved %s
|
||||
Sort discovered
|
||||
Sort by
|
||||
Peers
|
||||
Favorites
|
||||
Has location
|
||||
Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
@@ -973,3 +973,15 @@ zoom %d zoom %d
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
Save FAIL x%u
|
||||
Save migrating
|
||||
Saved %s
|
||||
Sort discovered
|
||||
Sort by
|
||||
Peers
|
||||
Favorites
|
||||
Has location
|
||||
Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
@@ -993,3 +993,15 @@ zoom %d zoom %d
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
Save FAIL x%u
|
||||
Save migrating
|
||||
Saved %s
|
||||
Sort discovered
|
||||
Sort by
|
||||
Peers
|
||||
Favorites
|
||||
Has location
|
||||
Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
@@ -973,3 +973,15 @@ zoom %d zoom %d
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
Save FAIL x%u
|
||||
Save migrating
|
||||
Saved %s
|
||||
Sort discovered
|
||||
Sort by
|
||||
Peers
|
||||
Favorites
|
||||
Has location
|
||||
Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
@@ -973,3 +973,15 @@ zoom %d zoom %d
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
Save FAIL x%u
|
||||
Save migrating
|
||||
Saved %s
|
||||
Sort discovered
|
||||
Sort by
|
||||
Peers
|
||||
Favorites
|
||||
Has location
|
||||
Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
@@ -973,3 +973,15 @@ zoom %d zoom %d
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
Save FAIL x%u
|
||||
Save migrating
|
||||
Saved %s
|
||||
Sort discovered
|
||||
Sort by
|
||||
Peers
|
||||
Favorites
|
||||
Has location
|
||||
Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
@@ -973,3 +973,15 @@ zoom %d zoom %d
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
Save FAIL x%u
|
||||
Save migrating
|
||||
Saved %s
|
||||
Sort discovered
|
||||
Sort by
|
||||
Peers
|
||||
Favorites
|
||||
Has location
|
||||
Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
@@ -973,3 +973,15 @@ zoom %d масштаб %d
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
Save FAIL x%u
|
||||
Save migrating
|
||||
Saved %s
|
||||
Sort discovered
|
||||
Sort by
|
||||
Peers
|
||||
Favorites
|
||||
Has location
|
||||
Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
@@ -973,3 +973,15 @@ zoom %d зум %d
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
Save FAIL x%u
|
||||
Save migrating
|
||||
Saved %s
|
||||
Sort discovered
|
||||
Sort by
|
||||
Peers
|
||||
Favorites
|
||||
Has location
|
||||
Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
@@ -973,3 +973,15 @@ zoom %d масштаб %d
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
Save FAIL x%u
|
||||
Save migrating
|
||||
Saved %s
|
||||
Sort discovered
|
||||
Sort by
|
||||
Peers
|
||||
Favorites
|
||||
Has location
|
||||
Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"sdktest","name":"SDK Test","ver":"1.1","desc":"Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, file access and its limits, keyboard input. Has a button that TRANSMITS a test message to Public (asks permission first)."}
|
||||
@@ -0,0 +1,106 @@
|
||||
-- SDK self-test. Exercises everything added to the extended SDK so the results
|
||||
-- can be read off the screen instead of inferred from a build log.
|
||||
-- Published to the store as a developer/bench tool.
|
||||
local ui, sys, store, timer = wada.ui, wada.sys, wada.store, wada.timer
|
||||
local C = ui.colors
|
||||
|
||||
local app = {}
|
||||
local rows, keyline, sendline = {}, nil, nil
|
||||
local W = 300 -- replaced with the real body width in on_open
|
||||
|
||||
local function row(y, text, color)
|
||||
local l = ui.label(text, 6, y, 12, color or C.text)
|
||||
l:width(W - 12)
|
||||
rows[#rows + 1] = l
|
||||
return l
|
||||
end
|
||||
|
||||
local function yn(v) return v and "yes" or "NO" end
|
||||
|
||||
function app.on_open(w, h)
|
||||
W = w or 300
|
||||
ui.scroll(true)
|
||||
local y = 4
|
||||
|
||||
-- 1. capability report --------------------------------------------------
|
||||
local c = sys.caps()
|
||||
row(y, "caps: sdk_ext=" .. yn(c.sdk_ext) .. " kbd=" .. yn(c.keyboard) ..
|
||||
" touch=" .. yn(c.touch) .. " sd=" .. yn(c.sd), C.accent); y = y + 16
|
||||
|
||||
-- 2. time (#245) --------------------------------------------------------
|
||||
local ep = sys.epoch()
|
||||
if ep then
|
||||
local d = sys.datetime()
|
||||
row(y, string.format("time: %d %04d-%02d-%02d %02d:%02d:%02d",
|
||||
ep, d.year, d.month, d.day, d.hour, d.min, d.sec), C.good)
|
||||
else
|
||||
row(y, "time: nil (clock not set yet - expected with no GPS/NTP)", C.sub)
|
||||
end
|
||||
y = y + 16
|
||||
|
||||
if not c.sdk_ext then
|
||||
row(y, "extended SDK is OFF on this board - nothing further to test.", C.sub)
|
||||
return
|
||||
end
|
||||
|
||||
-- 3. battery ------------------------------------------------------------
|
||||
local b = sys.battery()
|
||||
row(y, string.format("battery: %d mV %d%% charging=%s", b.mv, b.pct, yn(b.charging)),
|
||||
b.mv > 0 and C.good or C.bad); y = y + 16
|
||||
|
||||
-- 4. gps ----------------------------------------------------------------
|
||||
local g = sys.gps()
|
||||
if g then
|
||||
row(y, string.format("gps: %.5f, %.5f sats=%d", g.lat, g.lon, g.sats), C.good)
|
||||
else
|
||||
row(y, "gps: nil (no fix - correct indoors)", C.sub)
|
||||
end
|
||||
y = y + 16
|
||||
|
||||
-- 5. filesystem round trip + rate limit ---------------------------------
|
||||
local stamp = tostring(sys.millis())
|
||||
local ok, err = wada.fs.write("selftest.txt", "hello " .. stamp)
|
||||
local back = ok and wada.fs.read("selftest.txt") or nil
|
||||
row(y, "fs write: " .. yn(ok) .. (err and (" (" .. err .. ")") or "") ..
|
||||
" read back: " .. (back == ("hello " .. stamp) and "MATCH" or tostring(back)),
|
||||
ok and C.good or C.bad); y = y + 16
|
||||
|
||||
local ok2, err2 = wada.fs.write("selftest.txt", "again") -- immediately: must be refused
|
||||
row(y, "fs rate limit: " .. (ok2 == false and err2 == "too fast"
|
||||
and "REFUSED as designed" or ("UNEXPECTED " .. tostring(ok2) .. " " .. tostring(err2))),
|
||||
(ok2 == false) and C.good or C.bad); y = y + 16
|
||||
|
||||
local bad = wada.fs.write("../identity", "x")
|
||||
row(y, "fs traversal '../identity': " .. (bad == false and "REJECTED" or "LEAKED - BUG"),
|
||||
bad == false and C.good or C.bad); y = y + 16
|
||||
|
||||
local files = wada.fs.list()
|
||||
row(y, "fs list: " .. #files .. " file(s)" ..
|
||||
(files[1] and (" first=" .. files[1].name .. " " .. files[1].size .. "B") or ""),
|
||||
C.text); y = y + 20
|
||||
|
||||
-- 6. keys ---------------------------------------------------------------
|
||||
keyline = row(y, "keys: press any key on the keyboard...", C.sub); y = y + 20
|
||||
|
||||
-- 7. mesh send (consent) -------------------------------------------------
|
||||
sendline = row(y, "mesh.send: not tried yet", C.sub); y = y + 22
|
||||
|
||||
ui.button("Send to Public", 6, y, 130, 30, function()
|
||||
local sok, serr = wada.mesh.send("Public", "wadamesh SDK self-test")
|
||||
sendline:set("mesh.send: " .. tostring(sok) .. " " .. tostring(serr))
|
||||
sendline:color(sok and C.good or C.bad)
|
||||
end)
|
||||
ui.button("Beep", 146, y, 70, 30, function()
|
||||
sys.beep()
|
||||
end)
|
||||
end
|
||||
|
||||
-- keys and touch both arrive here
|
||||
function app.on_input(ev)
|
||||
if ev.type == "key" and keyline then
|
||||
keyline:set("keys: got '" .. tostring(ev.key) .. "' code=" .. tostring(ev.code))
|
||||
keyline:color(C.good)
|
||||
end
|
||||
end
|
||||
|
||||
return app
|
||||
@@ -17,19 +17,19 @@
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {
|
||||
"beta_50": {
|
||||
"notes": "Fixed the on-device updater downloading the wrong firmware on several boards. The V4-R8, ThinkNode M9, RAK Tap V2, T-LoRa Pager and Attaky now each pull their own build instead of the Heltec V4 TFT one.\nT-Display P4: the \"Get test builds (beta)\" toggle and the on-device update now appear. They were hidden before, so the P4 could only be updated over USB. Newly enabled, please test and report.\nLock screen fixed: tap Backspace to peek the screen without unlocking, hold Backspace to unlock. The clock and text are repositioned to fit the wide screen and stay readable.\nOn-screen keyboard now works with the detachable keyboard module: the keys stay down while the module is attached, and the module's # key brings them back up for symbols. The POWER button toggles the screen and wakes the board.\nBoard fixes: correct LoRa clock (no false TCXO), GPS baud, and sensor probing that was colliding with the touch and keyboard chips.",
|
||||
"beta_63": {
|
||||
"notes": "Hungarian devices no longer reboot when logging in to a repeater. The warning shown when your clock disagrees with a server is assembled from a translated sentence, and the Hungarian translation had reordered the two values inside it. The firmware then read the contact name as a number and a number as a memory address, and followed it - an instant reboot, every login, for Hungarian users only, because English happened to match the order the code expected. Fixed at the root rather than in the one file: a translation whose value placeholders do not match the original is now ignored and the English is shown instead, so no language file can crash a device, including one you write yourself. The four affected Hungarian lines are corrected and already published. Reported by pisti87.\nThe T-Lora Pager no longer crashes while scrolling back through chat history. The chat list rebuilds itself as you scroll, and the timer that drives that rebuild was being destroyed and recreated on every boundary crossing; a fast scroll could reach it after it was gone. It is now kept alive and paused instead, and the keyboard focus is detached before the old rows are torn down rather than during. By PixPMusic (PR #234), from two crash reports that decoded to the same fault. Reported by AtomAlex12 and PixPMusic.\nMessages no longer arrive in a nameless \"#unknown\" thread. A channel slot that was never set up holds an all-zero key, and the firmware treated it as a real channel: an app asking to send on a channel the device did not have would transmit a message encrypted with that all-zero key, and every other device in range could decrypt it against its own unused slots and file it under a channel with no name. Both halves are fixed - a device will no longer send on an unconfigured slot, and will no longer decrypt anything against one. The same change fixes a rarer fault where an unused slot could crowd a real channel out of the decryption attempt entirely and the message was silently dropped. Reported by D-Melhede, and seen by Marshal W7TER and PixPMusic.\nThe right-arrow character now renders instead of an empty box. It was never present in the bundled fonts: the font build asked Noto Sans for it, and Noto Sans does not contain it, and the tool omits a missing character rather than complaining. It is now taken from Montserrat, which does have it and is already the interface typeface. This affected every language, not only Hungarian, and around 137 places in the interface. Reported by pisti87.\nA message's Info panel no longer labels a per-message value as \"Scope\". That four-digit number is a checksum computed from the message itself under the sender's region key, so it is different for every message and cannot be read as a region - which is exactly how it was being read. It now names your region when the value verifies against your own region key, says another region when it does not, and keeps the raw number in brackets. Reported by D-Melhede.\nThere is now a written procedure for submitting your own app to the store, covering the file layout, versioning, and what the review checks for. Every submission is read before it is added. See deploy/apps/README.md.\nThe user guide explains the floppy-disk chip on the command centre: it is the time your chat history last saved successfully, not a clock, and it turns red or amber when saving is failing or still migrating.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-heltec-v4-tft-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_50/wadamesh-heltec-v4-tft-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_63/wadamesh-heltec-v4-tft-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-heltec-v4-tft.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_50/wadamesh-heltec-v4-tft.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_63/wadamesh-heltec-v4-tft.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
@@ -49,19 +49,19 @@
|
||||
"role": "gui",
|
||||
"title": "Touch UI",
|
||||
"version": {
|
||||
"beta_50": {
|
||||
"notes": "Fixed the on-device updater downloading the wrong firmware on several boards. The V4-R8, ThinkNode M9, RAK Tap V2, T-LoRa Pager and Attaky now each pull their own build instead of the Heltec V4 TFT one.\nT-Display P4: the \"Get test builds (beta)\" toggle and the on-device update now appear. They were hidden before, so the P4 could only be updated over USB. Newly enabled, please test and report.\nLock screen fixed: tap Backspace to peek the screen without unlocking, hold Backspace to unlock. The clock and text are repositioned to fit the wide screen and stay readable.\nOn-screen keyboard now works with the detachable keyboard module: the keys stay down while the module is attached, and the module's # key brings them back up for symbols. The POWER button toggles the screen and wakes the board.\nBoard fixes: correct LoRa clock (no false TCXO), GPS baud, and sensor probing that was colliding with the touch and keyboard chips.",
|
||||
"beta_63": {
|
||||
"notes": "Hungarian devices no longer reboot when logging in to a repeater. The warning shown when your clock disagrees with a server is assembled from a translated sentence, and the Hungarian translation had reordered the two values inside it. The firmware then read the contact name as a number and a number as a memory address, and followed it - an instant reboot, every login, for Hungarian users only, because English happened to match the order the code expected. Fixed at the root rather than in the one file: a translation whose value placeholders do not match the original is now ignored and the English is shown instead, so no language file can crash a device, including one you write yourself. The four affected Hungarian lines are corrected and already published. Reported by pisti87.\nThe T-Lora Pager no longer crashes while scrolling back through chat history. The chat list rebuilds itself as you scroll, and the timer that drives that rebuild was being destroyed and recreated on every boundary crossing; a fast scroll could reach it after it was gone. It is now kept alive and paused instead, and the keyboard focus is detached before the old rows are torn down rather than during. By PixPMusic (PR #234), from two crash reports that decoded to the same fault. Reported by AtomAlex12 and PixPMusic.\nMessages no longer arrive in a nameless \"#unknown\" thread. A channel slot that was never set up holds an all-zero key, and the firmware treated it as a real channel: an app asking to send on a channel the device did not have would transmit a message encrypted with that all-zero key, and every other device in range could decrypt it against its own unused slots and file it under a channel with no name. Both halves are fixed - a device will no longer send on an unconfigured slot, and will no longer decrypt anything against one. The same change fixes a rarer fault where an unused slot could crowd a real channel out of the decryption attempt entirely and the message was silently dropped. Reported by D-Melhede, and seen by Marshal W7TER and PixPMusic.\nThe right-arrow character now renders instead of an empty box. It was never present in the bundled fonts: the font build asked Noto Sans for it, and Noto Sans does not contain it, and the tool omits a missing character rather than complaining. It is now taken from Montserrat, which does have it and is already the interface typeface. This affected every language, not only Hungarian, and around 137 places in the interface. Reported by pisti87.\nA message's Info panel no longer labels a per-message value as \"Scope\". That four-digit number is a checksum computed from the message itself under the sender's region key, so it is different for every message and cannot be read as a region - which is exactly how it was being read. It now names your region when the value verifies against your own region key, says another region when it does not, and keeps the raw number in brackets. Reported by D-Melhede.\nThere is now a written procedure for submitting your own app to the store, covering the file layout, versioning, and what the review checks for. Every submission is read before it is added. See deploy/apps/README.md.\nThe user guide explains the floppy-disk chip on the command centre: it is the time your chat history last saved successfully, not a clock, and it turns red or amber when saving is failing or still migrating.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-tdeck-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_50/wadamesh-tdeck-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_63/wadamesh-tdeck-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-tdeck.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_50/wadamesh-tdeck.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_63/wadamesh-tdeck.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
@@ -81,19 +81,19 @@
|
||||
"role": "gui",
|
||||
"title": "[LR1121] Touch UI",
|
||||
"version": {
|
||||
"beta_50": {
|
||||
"notes": "Fixed the on-device updater downloading the wrong firmware on several boards. The V4-R8, ThinkNode M9, RAK Tap V2, T-LoRa Pager and Attaky now each pull their own build instead of the Heltec V4 TFT one.\nT-Display P4: the \"Get test builds (beta)\" toggle and the on-device update now appear. They were hidden before, so the P4 could only be updated over USB. Newly enabled, please test and report.\nLock screen fixed: tap Backspace to peek the screen without unlocking, hold Backspace to unlock. The clock and text are repositioned to fit the wide screen and stay readable.\nOn-screen keyboard now works with the detachable keyboard module: the keys stay down while the module is attached, and the module's # key brings them back up for symbols. The POWER button toggles the screen and wakes the board.\nBoard fixes: correct LoRa clock (no false TCXO), GPS baud, and sensor probing that was colliding with the touch and keyboard chips.",
|
||||
"beta_63": {
|
||||
"notes": "Hungarian devices no longer reboot when logging in to a repeater. The warning shown when your clock disagrees with a server is assembled from a translated sentence, and the Hungarian translation had reordered the two values inside it. The firmware then read the contact name as a number and a number as a memory address, and followed it - an instant reboot, every login, for Hungarian users only, because English happened to match the order the code expected. Fixed at the root rather than in the one file: a translation whose value placeholders do not match the original is now ignored and the English is shown instead, so no language file can crash a device, including one you write yourself. The four affected Hungarian lines are corrected and already published. Reported by pisti87.\nThe T-Lora Pager no longer crashes while scrolling back through chat history. The chat list rebuilds itself as you scroll, and the timer that drives that rebuild was being destroyed and recreated on every boundary crossing; a fast scroll could reach it after it was gone. It is now kept alive and paused instead, and the keyboard focus is detached before the old rows are torn down rather than during. By PixPMusic (PR #234), from two crash reports that decoded to the same fault. Reported by AtomAlex12 and PixPMusic.\nMessages no longer arrive in a nameless \"#unknown\" thread. A channel slot that was never set up holds an all-zero key, and the firmware treated it as a real channel: an app asking to send on a channel the device did not have would transmit a message encrypted with that all-zero key, and every other device in range could decrypt it against its own unused slots and file it under a channel with no name. Both halves are fixed - a device will no longer send on an unconfigured slot, and will no longer decrypt anything against one. The same change fixes a rarer fault where an unused slot could crowd a real channel out of the decryption attempt entirely and the message was silently dropped. Reported by D-Melhede, and seen by Marshal W7TER and PixPMusic.\nThe right-arrow character now renders instead of an empty box. It was never present in the bundled fonts: the font build asked Noto Sans for it, and Noto Sans does not contain it, and the tool omits a missing character rather than complaining. It is now taken from Montserrat, which does have it and is already the interface typeface. This affected every language, not only Hungarian, and around 137 places in the interface. Reported by pisti87.\nA message's Info panel no longer labels a per-message value as \"Scope\". That four-digit number is a checksum computed from the message itself under the sender's region key, so it is different for every message and cannot be read as a region - which is exactly how it was being read. It now names your region when the value verifies against your own region key, says another region when it does not, and keeps the raw number in brackets. Reported by D-Melhede.\nThere is now a written procedure for submitting your own app to the store, covering the file layout, versioning, and what the review checks for. Every submission is read before it is added. See deploy/apps/README.md.\nThe user guide explains the floppy-disk chip on the command centre: it is the time your chat history last saved successfully, not a clock, and it turns red or amber when saving is failing or still migrating.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-tlora-pager-lr1121-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_50/wadamesh-tlora-pager-lr1121-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_63/wadamesh-tlora-pager-lr1121-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-tlora-pager-lr1121.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_50/wadamesh-tlora-pager-lr1121.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_63/wadamesh-tlora-pager-lr1121.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
@@ -104,19 +104,19 @@
|
||||
"role": "gui",
|
||||
"title": "[SX1262] Touch UI",
|
||||
"version": {
|
||||
"beta_50": {
|
||||
"notes": "Fixed the on-device updater downloading the wrong firmware on several boards. The V4-R8, ThinkNode M9, RAK Tap V2, T-LoRa Pager and Attaky now each pull their own build instead of the Heltec V4 TFT one.\nT-Display P4: the \"Get test builds (beta)\" toggle and the on-device update now appear. They were hidden before, so the P4 could only be updated over USB. Newly enabled, please test and report.\nLock screen fixed: tap Backspace to peek the screen without unlocking, hold Backspace to unlock. The clock and text are repositioned to fit the wide screen and stay readable.\nOn-screen keyboard now works with the detachable keyboard module: the keys stay down while the module is attached, and the module's # key brings them back up for symbols. The POWER button toggles the screen and wakes the board.\nBoard fixes: correct LoRa clock (no false TCXO), GPS baud, and sensor probing that was colliding with the touch and keyboard chips.",
|
||||
"beta_63": {
|
||||
"notes": "Hungarian devices no longer reboot when logging in to a repeater. The warning shown when your clock disagrees with a server is assembled from a translated sentence, and the Hungarian translation had reordered the two values inside it. The firmware then read the contact name as a number and a number as a memory address, and followed it - an instant reboot, every login, for Hungarian users only, because English happened to match the order the code expected. Fixed at the root rather than in the one file: a translation whose value placeholders do not match the original is now ignored and the English is shown instead, so no language file can crash a device, including one you write yourself. The four affected Hungarian lines are corrected and already published. Reported by pisti87.\nThe T-Lora Pager no longer crashes while scrolling back through chat history. The chat list rebuilds itself as you scroll, and the timer that drives that rebuild was being destroyed and recreated on every boundary crossing; a fast scroll could reach it after it was gone. It is now kept alive and paused instead, and the keyboard focus is detached before the old rows are torn down rather than during. By PixPMusic (PR #234), from two crash reports that decoded to the same fault. Reported by AtomAlex12 and PixPMusic.\nMessages no longer arrive in a nameless \"#unknown\" thread. A channel slot that was never set up holds an all-zero key, and the firmware treated it as a real channel: an app asking to send on a channel the device did not have would transmit a message encrypted with that all-zero key, and every other device in range could decrypt it against its own unused slots and file it under a channel with no name. Both halves are fixed - a device will no longer send on an unconfigured slot, and will no longer decrypt anything against one. The same change fixes a rarer fault where an unused slot could crowd a real channel out of the decryption attempt entirely and the message was silently dropped. Reported by D-Melhede, and seen by Marshal W7TER and PixPMusic.\nThe right-arrow character now renders instead of an empty box. It was never present in the bundled fonts: the font build asked Noto Sans for it, and Noto Sans does not contain it, and the tool omits a missing character rather than complaining. It is now taken from Montserrat, which does have it and is already the interface typeface. This affected every language, not only Hungarian, and around 137 places in the interface. Reported by pisti87.\nA message's Info panel no longer labels a per-message value as \"Scope\". That four-digit number is a checksum computed from the message itself under the sender's region key, so it is different for every message and cannot be read as a region - which is exactly how it was being read. It now names your region when the value verifies against your own region key, says another region when it does not, and keeps the raw number in brackets. Reported by D-Melhede.\nThere is now a written procedure for submitting your own app to the store, covering the file layout, versioning, and what the review checks for. Every submission is read before it is added. See deploy/apps/README.md.\nThe user guide explains the floppy-disk chip on the command centre: it is the time your chat history last saved successfully, not a clock, and it turns red or amber when saving is failing or still migrating.",
|
||||
"files": [
|
||||
{
|
||||
"type": "flash-wipe",
|
||||
"name": "wadamesh-tlora-pager-sx1262-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_50/wadamesh-tlora-pager-sx1262-merged.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_63/wadamesh-tlora-pager-sx1262-merged.bin",
|
||||
"title": "Full install (bootloader + firmware)"
|
||||
},
|
||||
{
|
||||
"type": "flash-update",
|
||||
"name": "wadamesh-tlora-pager-sx1262.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_50/wadamesh-tlora-pager-sx1262.bin",
|
||||
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_63/wadamesh-tlora-pager-sx1262.bin",
|
||||
"title": "Update (app only)"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -172,13 +172,13 @@
|
||||
</ul>
|
||||
<p>The bar along the bottom is always there — it switches the five main tabs (Chat, Contacts, Home, Map, Settings) from anywhere. The top bar shows unread count, time, signal and battery.</p>
|
||||
<h3>The save chip <span class="tag">Cmdr</span></h3>
|
||||
<p>On the command centre, the TX/RX traffic chart has a legend row underneath it: packet totals on the left, and on the right a small chip with a <b>floppy-disk icon</b> and a time. That chip is not about the radio — it is about your <b>chat history</b>, and it answers one question: are your messages actually reaching storage?</p>
|
||||
<p>On the command centre, the TX/RX traffic chart has a legend row underneath it: packet totals on the left, and on the right a small chip reading <b>Saved</b> and a time. That chip is not about the radio — it is about your <b>chat history</b>, and it answers one question: are your messages actually reaching storage?</p>
|
||||
<ul>
|
||||
<li><code>14:32</code> — the moment history last saved successfully, in 12- or 24-hour form to match your clock setting. It is a timestamp, not a running clock, so it only moves when a save happens.</li>
|
||||
<li><code>14:32-2D</code> — that last successful save was <b>two days ago</b>. Saving has quietly stopped; the suffix exists so a stale store cannot pass for a fresh one at a glance.</li>
|
||||
<li><code>--:--</code> — nothing has saved yet, or the save happened before the clock was set, so there is no meaningful time to show.</li>
|
||||
<li><code>FAIL x3</code>, in red — writes are failing right now, with a count. Recent messages are at risk of being lost on reboot.</li>
|
||||
<li><code>migrating</code>, in amber — an older history file is still being converted to the current segmented format. This is normal after an update and clears itself.</li>
|
||||
<li><code>Saved 14:32</code> — the moment history last saved successfully, in 12- or 24-hour form to match your clock setting. It is a timestamp, not a running clock, so it only moves when a save happens.</li>
|
||||
<li><code>Saved 14:32-2D</code> — that last successful save was <b>two days ago</b>. Saving has quietly stopped; the suffix exists so a stale store cannot pass for a fresh one at a glance.</li>
|
||||
<li><code>Saved --:--</code> — nothing has saved yet, or the save happened before the clock was set, so there is no meaningful time to show.</li>
|
||||
<li><code>Save FAIL x3</code>, in red — writes are failing right now, with a count. Recent messages are at risk of being lost on reboot.</li>
|
||||
<li><code>Save migrating</code>, in amber — an older history file is still being converted to the current segmented format. This is normal after an update and clears itself.</li>
|
||||
</ul>
|
||||
<p>A time that keeps pace with your conversations means history is safe. Red, amber, or a time that has stopped moving is worth a look at <b>Settings → About → Chat store</b>, which names the storage backend in use and the exact stage and error a failing save is hitting.</p>
|
||||
</div>
|
||||
|
||||
+67
-20
@@ -140,8 +140,10 @@
|
||||
<a href="#mesh">wada.mesh</a>
|
||||
<a href="#net">wada.net</a>
|
||||
<a href="#store">wada.store</a>
|
||||
<a href="#fs">wada.fs</a>
|
||||
<a href="#sys">wada.sys</a>
|
||||
<a href="#timer">wada.timer</a>
|
||||
<a href="#perms">Permissions</a>
|
||||
<div class="grp">Shipping</div>
|
||||
<a href="#format">App format</a>
|
||||
<a href="#install">Install yours</a>
|
||||
@@ -162,7 +164,7 @@
|
||||
<p>An app is one Lua chunk. The firmware gives it a screen, an input stream, read access to the mesh, a little key/value storage and an HTTP fetch. That is deliberately the whole surface — roughly thirty functions, all under the <code>wada.</code> table.</p>
|
||||
<p>LVGL is <b>not</b> exposed. Binding a UI toolkit wholesale would freeze us out of ever changing it, and would hand every app enough rope to wedge the device. Instead there is a small set of widgets we own and can keep stable across firmware versions.</p>
|
||||
<p>Two of the apps that ship in the Store — <b>RF Monitor</b> and <b>Airtime</b> — are written against exactly this API and nothing else. They are the reference implementations; read them if a doc paragraph is ambiguous.</p>
|
||||
<div class="note"><p><b>API v1 is read-only where the mesh is concerned.</b> An app can see contacts, the packet log and radio statistics. It cannot transmit. That line is deliberate: a buggy or hostile app must not be able to put packets on the air in your name.</p></div>
|
||||
<div class="note"><p><b>An app cannot put packets on the air behind your back.</b> Reading the mesh — contacts, the packet log, radio statistics — is free. Transmitting is not: <code>wada.mesh.send</code> exists, but the first call from a given app stops and asks you, by name, and a refusal is remembered. Same for reading your incoming messages. See <a href="#perms">Permissions</a>.</p></div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
@@ -170,25 +172,29 @@
|
||||
<h2>Your first app</h2>
|
||||
<div class="row"><div class="prose">
|
||||
<p>Save this as <code>hello.lua</code>:</p>
|
||||
<pre><code>local n = 0
|
||||
<pre><code>local app = {}
|
||||
local status
|
||||
local n = 0
|
||||
|
||||
function on_start()
|
||||
wada.ui.label("Hello from Lua", 16)
|
||||
status = wada.ui.label("tick 0", 12)
|
||||
status:color(wada.ui.colors.sub)
|
||||
function app.on_open(w, h)
|
||||
wada.ui.label("Hello from Lua", 6, 4, 16, wada.ui.colors.text)
|
||||
status = wada.ui.label("tick 0", 6, 26, 12, wada.ui.colors.sub)
|
||||
wada.timer.every(1000)
|
||||
end
|
||||
|
||||
function on_tick()
|
||||
function app.on_tick()
|
||||
n = n + 1
|
||||
status:set("tick " .. n)
|
||||
end
|
||||
|
||||
function on_input(ev)
|
||||
function app.on_input(ev)
|
||||
if ev.type == "key" then
|
||||
wada.sys.toast("you pressed " .. tostring(ev.name))
|
||||
wada.sys.toast("you pressed " .. tostring(ev.key))
|
||||
end
|
||||
end</code></pre>
|
||||
end
|
||||
|
||||
return app</code></pre>
|
||||
<div class="note"><p><b>The last line matters.</b> An app is a table you build and <code>return</code>; the firmware calls the <code>on_*</code> fields on that table. Callbacks defined as plain globals are never found, so the app opens to a blank page with no error. If nothing happens, check for <code>return app</code> first.</p></div>
|
||||
<p>Copy it to <code>/apps/hello.lua</code> on the device's SD card (or internal storage on boards without a card), open the app drawer, and it is there. No manifest is required for a file you side-load yourself — see <a href="#install">Install yours</a>.</p>
|
||||
</div></div>
|
||||
</section>
|
||||
@@ -196,16 +202,17 @@ end</code></pre>
|
||||
<section class="doc" id="lifecycle">
|
||||
<h2>App lifecycle</h2>
|
||||
<div class="row"><div class="prose">
|
||||
<p>Define the callbacks you need as globals. All four are optional.</p>
|
||||
<p>Build a table, put the callbacks you need on it, and <code>return</code> it from the file. All of them are optional.</p>
|
||||
<table class="api">
|
||||
<tr><th>Callback</th><th>When it runs</th></tr>
|
||||
<tr><td>on_start()</td><td>Once, when the app opens. Build your UI here.</td></tr>
|
||||
<tr><td>on_tick()</td><td>On the cadence set by <code>wada.timer.every(ms)</code>. Never faster than 33 ms.</td></tr>
|
||||
<tr><td>on_input(ev)</td><td>For every touch, key or trackball event. See <a href="#input">wada.input</a>.</td></tr>
|
||||
<tr><td>on_stop()</td><td>Once, when the app closes. Persist anything you care about here.</td></tr>
|
||||
<tr><td>app.on_open(w, h)</td><td>Once, when the app opens. Build your UI here. <code>w</code> / <code>h</code> are the usable body size in pixels — use them instead of assuming a screen size, since the boards differ.</td></tr>
|
||||
<tr><td>app.on_tick()</td><td>On the cadence set by <code>wada.timer.every(ms)</code>. Never faster than 33 ms.</td></tr>
|
||||
<tr><td>app.on_input(ev)</td><td>For every touch, key or trackball event. See <a href="#input">wada.input</a>.</td></tr>
|
||||
<tr><td>app.on_message(m)</td><td>An incoming mesh message, if the user granted the app the read permission. See <a href="#perms">Permissions</a>.</td></tr>
|
||||
<tr><td>app.on_close()</td><td>Once, when the app closes. Persist anything you care about here.</td></tr>
|
||||
</table>
|
||||
<p>Every callback runs inside a guarded call on the UI thread. If your code raises an error the app is closed with a toast rather than taking the firmware down with it — but it does mean a slow callback stalls the whole UI, so keep them short. There is an instruction budget: an app that spins forever is stopped.</p>
|
||||
<p>Widgets you create in <code>on_start</code> are destroyed for you when the app closes. You do not free anything.</p>
|
||||
<p>Widgets you create in <code>on_open</code> are destroyed for you when the app closes. You do not free anything.</p>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
@@ -215,8 +222,8 @@ end</code></pre>
|
||||
<p>Widgets are created in order, top to bottom, on the app's page. Every constructor returns a handle with methods you can call later.</p>
|
||||
<table class="api">
|
||||
<tr><th>Call</th><th>Returns / does</th></tr>
|
||||
<tr><td>wada.ui.label(text, size)</td><td>A text line. <code>size</code> is a class, not a pixel height: 12, 14 or 16.</td></tr>
|
||||
<tr><td>wada.ui.button(text, fn)</td><td>A tappable button; <code>fn</code> is called with no arguments.</td></tr>
|
||||
<tr><td>wada.ui.label(text, x, y, size, color)</td><td>A text line placed at <code>x, y</code>. <code>size</code> is a class, not a pixel height: 12, 14 or 16. <code>color</code> takes a <code>wada.ui.colors</code> value.</td></tr>
|
||||
<tr><td>wada.ui.button(text, x, y, w, h, fn)</td><td>A tappable button at <code>x, y</code> sized <code>w × h</code>; <code>fn</code> is called with no arguments.</td></tr>
|
||||
<tr><td>wada.ui.canvas(w, h)</td><td>A drawing surface. See the canvas methods below.</td></tr>
|
||||
<tr><td>wada.ui.chart(points)</td><td>A line chart sized to the page. The Airtime and RF Monitor primitive.</td></tr>
|
||||
<tr><td>wada.ui.scroll(on)</td><td>Allow the page to scroll when content is taller than the screen.</td></tr>
|
||||
@@ -258,7 +265,7 @@ end</code></pre>
|
||||
<table class="api">
|
||||
<tr><th>ev.type</th><th>Fields</th></tr>
|
||||
<tr><td>"touch"</td><td><code>ev.x</code>, <code>ev.y</code> in page coordinates.</td></tr>
|
||||
<tr><td>"key"</td><td><code>ev.name</code> — the key label on boards with a keyboard.</td></tr>
|
||||
<tr><td>"key"</td><td><code>ev.key</code> — a one-character string for printable keys (<code>"w"</code>), or a name for the rest: <code>up</code>, <code>down</code>, <code>left</code>, <code>right</code>, <code>enter</code>, <code>esc</code>, <code>backspace</code>. <code>ev.code</code> carries the raw value. Boards with a keyboard only; the key that closes the app is never delivered, so an app cannot trap you inside it.</td></tr>
|
||||
<tr><td>"dir"</td><td><code>ev.dir</code> — one of <code>up</code>, <code>down</code>, <code>left</code>, <code>right</code>, <code>select</code>. Trackball, D-pad and swipes all arrive here.</td></tr>
|
||||
</table>
|
||||
<p>Handle <code>"dir"</code> if you want your app to work on every board. A touch-only board sends swipes as directions, and a keyboard board sends its navigation keys the same way, so one branch covers both.</p>
|
||||
@@ -274,6 +281,7 @@ end</code></pre>
|
||||
<tr><td>wada.mesh.rx_log()</td><td>Recent packets: <code>{ago_ms, type, rssi, snr, hops}</code>. The RF Monitor feed.</td></tr>
|
||||
<tr><td>wada.mesh.stats()</td><td><code>{rssi, noise, rx_air_s, tx_air_s, rx_pkts, rx_err, tx_budget_ms, rx_events, rx_dropped, tx_pkts, freq, bw, sf, duty_pct}</code>.</td></tr>
|
||||
<tr><td>wada.mesh.self()</td><td>This node: <code>{name, lat, lon}</code>.</td></tr>
|
||||
<tr><td>wada.mesh.send(channel, text) <span class="chip">ext</span></td><td>Send to a channel by name. Returns <code>ok, err</code>. Needs the send permission — the first call prompts the user. <code>err</code> is <code>"denied"</code> if they refused, <code>"too fast"</code> inside the 5 second floor, <code>"too long"</code> past 180 characters.</td></tr>
|
||||
</table>
|
||||
<p>These are snapshots taken when you call them, not live views — call again on each tick to refresh.</p>
|
||||
</div></div>
|
||||
@@ -298,7 +306,23 @@ end</code></pre>
|
||||
<tr><td>wada.store.set(key, value)</td><td>Write a string.</td></tr>
|
||||
</table>
|
||||
<p>Keys are namespaced per app, so two apps cannot collide. Keep it small — the budget is about 2 KB per app, and the underlying store silently drops oversized values. High scores and settings, not logs.</p>
|
||||
<p>Writes hit flash. Do them in <code>on_stop()</code> or on a real user action, never on every tick.</p>
|
||||
<p>Writes hit flash. Do them in <code>app.on_close()</code> or on a real user action, never on every tick.</p>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="doc" id="fs">
|
||||
<h2>wada.fs <span class="chip">ext</span></h2>
|
||||
<div class="row"><div class="prose">
|
||||
<p>A private folder per app, for things too big or too structured for <a href="#store">wada.store</a> — a log, a track, a cache. Paths are plain names inside your own folder: an app cannot name a file outside it, and cannot see another app's files. <code>..</code> and absolute paths are rejected rather than sanitised.</p>
|
||||
<table class="api">
|
||||
<tr><th>Call</th><th>Returns / does</th></tr>
|
||||
<tr><td>wada.fs.read(name)</td><td>File contents as a string, or <code>nil</code>.</td></tr>
|
||||
<tr><td>wada.fs.write(name, data)</td><td>Replace a file. Returns <code>ok, err</code>.</td></tr>
|
||||
<tr><td>wada.fs.append(name, data)</td><td>Append. Returns <code>ok, err</code>.</td></tr>
|
||||
<tr><td>wada.fs.list()</td><td>Array of <code>{name, size}</code>.</td></tr>
|
||||
<tr><td>wada.fs.remove(name)</td><td>Delete a file.</td></tr>
|
||||
</table>
|
||||
<p>Writes are rate-limited to roughly one per second and files are capped at 32 KB. A write inside the window returns <code>false, "too fast"</code> — that is the normal answer, not a failure, so check it. The limit is not arbitrary: on a board with no card this lands on the same internal flash the mesh uses, and an app looping on writes would stall the whole device.</p>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
@@ -310,6 +334,12 @@ end</code></pre>
|
||||
<tr><td>wada.sys.board()</td><td><code>{w, h, touch, keyboard, trackball, gps}</code> — size and capabilities.</td></tr>
|
||||
<tr><td>wada.sys.toast(msg)</td><td>Brief on-screen message.</td></tr>
|
||||
<tr><td>wada.sys.random(n)</td><td>Integer in 1..n.</td></tr>
|
||||
<tr><td>wada.sys.epoch()</td><td>Unix time in seconds, or <code>nil</code> when the clock has not been set yet (no GPS fix and no NTP). Always handle the <code>nil</code>.</td></tr>
|
||||
<tr><td>wada.sys.datetime()</td><td><code>{year, month, day, hour, min, sec, wday}</code> in local time. <code>wday</code> is 0 for Sunday.</td></tr>
|
||||
<tr><td>wada.sys.beep()</td><td>Short beep on boards with a buzzer; silent elsewhere, and silent when the user has sound off.</td></tr>
|
||||
<tr><td>wada.sys.caps()</td><td><code>{sdk_ext, keyboard, touch, sd}</code>. Check <code>sdk_ext</code> before using anything marked <span class="chip">ext</span> below.</td></tr>
|
||||
<tr><td>wada.sys.battery() <span class="chip">ext</span></td><td><code>{mv, pct, charging}</code>.</td></tr>
|
||||
<tr><td>wada.sys.gps() <span class="chip">ext</span></td><td><code>{lat, lon, sats}</code>, or <code>nil</code> with no fix — which is the normal indoor case, so handle it.</td></tr>
|
||||
</table>
|
||||
</div></div>
|
||||
</section>
|
||||
@@ -325,6 +355,21 @@ end</code></pre>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="doc" id="perms">
|
||||
<h2>Permissions</h2>
|
||||
<div class="row"><div class="prose">
|
||||
<p>Two things an app can ask for reach past its own window, so they are not granted by installing it:</p>
|
||||
<table class="api">
|
||||
<tr><th>Permission</th><th>Unlocks</th></tr>
|
||||
<tr><td>Send</td><td><code>wada.mesh.send</code> — transmitting in your name.</td></tr>
|
||||
<tr><td>Read messages</td><td><code>app.on_message</code> — seeing messages as they arrive.</td></tr>
|
||||
</table>
|
||||
<p>They are separate: granting one does not grant the other. The prompt names the app and appears on the first attempt, not at install time, so you are asked at the moment it is obvious what the app wants it for. A refusal is remembered — the app does not get to ask again on a loop — and the call simply returns <code>false, "denied"</code>, which a well-written app should handle rather than break on.</p>
|
||||
<p><b>Settings → App permissions</b> lists every installed app and what it holds, and revokes with one switch. Granting happens where an app asks; that page is for review and for taking it back.</p>
|
||||
<div class="note"><p><b>Write for the denial.</b> The user can say no, or revoke later. Treat a permitted send as the lucky path, not the assumption.</p></div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="doc" id="format">
|
||||
<h2>App format</h2>
|
||||
<div class="row"><div class="prose">
|
||||
@@ -377,6 +422,8 @@ end</code></pre>
|
||||
<div class="row"><div class="prose">
|
||||
<p>Apps run in a restricted environment. These are removed: <code>io</code>, <code>os</code>, <code>require</code>, <code>dofile</code>, and loading new chunks at runtime. Available: <code>math</code>, <code>string</code>, <code>table</code>, and the usual <code>pairs</code>, <code>ipairs</code>, <code>select</code>, <code>pcall</code>, <code>tostring</code>, <code>tonumber</code>.</p>
|
||||
<p>Memory comes from a capped pool in PSRAM, so an app that allocates without bound fails its own allocation rather than starving the radio or the UI. There is an instruction budget on every callback for the same reason.</p>
|
||||
<p><b>The extended calls are not on every board.</b> Anything marked <span class="chip">ext</span> above — <code>wada.fs</code>, <code>wada.mesh.send</code>, <code>sys.battery</code>, <code>sys.gps</code> — needs a board with the memory to carry it, so the Heltec V4 keeps its RAM for the mesh instead. Call <code>wada.sys.caps().sdk_ext</code> and degrade gracefully rather than assuming; an app that hard-depends on them without checking simply errors out on the small boards.</p>
|
||||
<p>Rate limits are part of the contract, not a rainy-day guard: <code>wada.fs</code> writes are about one per second with a 32 KB file cap, and <code>wada.mesh.send</code> has a 5 second floor and a 180-character limit. They return <code>false, "too fast"</code> rather than throwing, and hitting them is expected — handle it.</p>
|
||||
<p>None of this makes a hostile app safe, which is why the catalog is curated. It makes an <i>honest</i> app that has a bug survivable: it gets closed, and the device keeps carrying traffic.</p>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# beta_63 promoted to STABLE — cumulative over beta_51..beta_63 (previous stable: beta_50)
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
# Curated from the 13 beta notes: test-build warnings dropped, statements that
|
||||
# later changed (the save chip, the core version) corrected to what now ships.
|
||||
|
||||
# --- Read this before updating -----------------------------------------------------------
|
||||
Read this one first if you are coming from beta_50: the chat-history format on the SD card changed during this run, and the change is ONE-WAY. Updating is safe and your history is converted for you, but if you later flash a firmware older than beta_52 it will not be able to read that history. Back up first if that matters to you.
|
||||
Chats now keep the newest 250 messages each by default. If you had a chat with more than that, the extra is trimmed on first run. The limit is yours to change under Settings > Chats > "Keep per chat (messages)" - 100, 250, 500, 1000 or off.
|
||||
If you ever picked a region preset, your radio was transmitting at roughly 9x less power than it should have been. That is fixed. Expect noticeably better range, and re-check your power setting if you had raised it to compensate.
|
||||
Your identity no longer lives on the SD card, so swapping or removing the card can never cost you your node identity again.
|
||||
|
||||
# --- The big changes ----------------------------------------------------------------------
|
||||
The MeshCore core moved from 1.16 to 1.17. Every board is rebased on it. This is the largest under-the-hood change since 1.16, and it lays the groundwork for listen-before-talk: the radio can check whether the channel is busy before transmitting. About reports v1.17.4-touch.
|
||||
WADAMESH runs apps written in Lua. They install from a Store on the device, appear in the app drawer, and can be updated or removed from there. RF Monitor and Airtime are now Lua apps rather than firmware pages, and both gained features as a result. You can write your own: drop a bare .lua file into /apps/ on the card and it shows up.
|
||||
Translations are no longer baked into the firmware - they download from the Store's Languages tab, so a language can be fixed or completed without a new firmware release. Every language got a full sweep of around 150 strings that had quietly stayed English, and Romanian and Hungarian joined as full interface languages.
|
||||
Major SD storage rework, contributed by the community: the device recovers from a wedged or removed card at runtime instead of playing dead until reboot, and chat history is stored in append-friendly segments. A large chat history no longer slows the whole interface down.
|
||||
Storage writes run on a dedicated background worker, so a big save can no longer make the interface stutter or the device freeze.
|
||||
|
||||
# --- Crashes and data loss fixed ----------------------------------------------------------
|
||||
Hungarian devices no longer reboot when logging in to a repeater. A translated sentence had its two values in the wrong order, and the firmware followed a number as if it were a memory address. Beyond fixing the text, a translation that does not match the original is now ignored rather than trusted - no language file can crash a device, including one you write yourself.
|
||||
The T-Lora Pager no longer crashes while scrolling back through chat history.
|
||||
Heltec V4 units no longer freeze in a busy mesh. Two separate causes: every advert heard used to trigger a storage write, and later, a full contact list made every new contact delete a stored record on the receive path. Both are off the packet path now.
|
||||
Settings can no longer silently reset themselves. An interrupted settings write could truncate the file and lose everything in it.
|
||||
Messages no longer arrive in a nameless "#unknown" thread, and channel messages can no longer be silently dropped by an unused channel slot.
|
||||
Fixed the 3-minute boot with an SD card inserted, and formatting a card on the V4-R8 or ThinkNode M9 no longer hangs forever.
|
||||
Blocking someone now actually works - three separate bugs, and unblocking now fully clears the block.
|
||||
|
||||
# --- Messaging and the interface ------------------------------------------------------------
|
||||
Sends retry until the mesh has actually heard them, under Settings > Radio & Mesh > "Retry sends until heard" (off by default).
|
||||
Message bubbles show the time on the top row next to the sender, so more messages fit on screen.
|
||||
The chat list preview no longer shows an old message and never updates.
|
||||
Accented and non-Latin characters render everywhere, and the arrow character that used to be an empty box now renders - it had never been in the bundled fonts, in any language.
|
||||
Message details show the region a message was scoped to, rather than a raw four-digit number that changed on every message.
|
||||
Files app shows each file's modified date, and Copy and Paste work on every board.
|
||||
The Discover app shows the distance to a node when both sides have a position, and no longer freezes on names containing emoji.
|
||||
The long-press accent picker gained ae/AE on the A key for Danish and Norwegian.
|
||||
The lock screen and the unlock animation carry the WADAMESH name.
|
||||
|
||||
# --- Per board -------------------------------------------------------------------------------
|
||||
T-Display P4: the clock, text size, profile export and backups all work again, and the whole-screen blue flash is fixed - it was SD card access delaying the display, not the display itself.
|
||||
T-Lora Pager: new UI size presets (Small, Medium, Large), Wi-Fi and Bluetooth hand off cleanly without a reboot, map tiles load reliably from SD, and the radio oscillator is driven at the voltage LilyGo's own reference specifies.
|
||||
Heltec V4: around 17 KB of internal memory reclaimed, measured step by step, which is what made the Store usable on a board with 2 MB of PSRAM.
|
||||
Heltec V4 Expansion Kit V2: the buzzer works.
|
||||
GPS is genuinely fixed - the firmware never actually read what the module was sending. The GPS settings page now shows live satellite count and signal strength.
|
||||
New LoRa antenna setting under Settings > Radio.
|
||||
|
||||
# --- Community ---------------------------------------------------------------------------------
|
||||
This run is heavily community work: PixPMusic, Vybo, pisti87, Yazutsu, scratchdiver, jacobpretorius, oumike, codemonkeybr, attakygit, Steve Glenner, Fr Cyril and mikecarper all contributed code or translations, alongside everyone who filed a report with enough detail to find the cause.
|
||||
@@ -0,0 +1,30 @@
|
||||
# beta_64 — test build. Stable stays beta_63.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# --- The big one: the Heltec V4 freeze, properly this time --------------------------------
|
||||
Heltec V4 units that freeze when a new contact arrives should be markedly better. beta_62 moved one storage delete off the packet path and that helped, but a reporter came back with the tell that mattered: the freezes got rarer AND longer. Rarer was the fix working; longer was the half nobody had measured.
|
||||
Here is what was actually costing the time. A contact record is 152 bytes and the table holds up to 2000 of them, so saving contacts rewrote the entire 304 KB file on every single change - and the crash-safe write keeps a second copy of the same size alongside it while it swaps. Roughly 600 KB of flash churn because one contact changed. On a board with no card that lands on internal flash, and its garbage collection suspends the flash cache, which stalls both CPU cores at once. That is why the whole device went away rather than just dropping a packet.
|
||||
The table never actually changes in bulk. Replacing the oldest contact overwrites one slot, and hearing from a node you already know touches one entry. So the firmware now compares each record against what is already stored and writes back only the ones that differ. A new contact replacing an old one writes 152 bytes instead of 304,000, and a save with nothing to change writes nothing at all.
|
||||
Because that is a new way of writing the file, it checks its own work: every patched record is read back and compared, and on any disagreement the firmware falls back to the old whole-file write instead. Your contact list cannot end up half-written by the new path.
|
||||
Contact deletes that overflowed the queue used to leak their stored record permanently. Fixed, and the queue is four times deeper.
|
||||
About > System info gained a Contact store section: how many contacts, how much space they use, how full internal flash is, and what the last save cost. If you report a freeze, that panel is the useful photograph.
|
||||
|
||||
# --- Apps can do much more now -------------------------------------------------------------
|
||||
Lua apps gained a substantially larger toolkit: the real date and time, the buzzer, battery state, GPS position, a scoped and rate-limited private folder each, hardware key presses, and the ability to send mesh messages and receive replies.
|
||||
Anything that reaches outside an app now asks you first, per app, once. Sending mesh messages and reading incoming messages are separate permissions, so granting one does not grant the other, and refusing is remembered rather than nagged.
|
||||
New page: Settings > App permissions. Every installed app and exactly what it may do, with a switch to revoke anything you granted. Granting happens where the app asks for it; this page is for reviewing and taking it back.
|
||||
The extended toolkit is limited to boards with the memory to carry it, so the Heltec V4 keeps its resources for the mesh. Apps see which features exist and can adapt.
|
||||
A "SDK Test" app is in the Store. It exercises every new call and reports pass or fail per line - useful if you are writing apps, or if you want to check your own board.
|
||||
|
||||
# --- Fixes ------------------------------------------------------------------------------------
|
||||
Accented and non-Latin characters showed as empty boxes in the grey hint text inside empty text fields - the Wi-Fi password box, the MQTT bridge fields, and about two dozen others. They render properly now, in every language.
|
||||
A translated line whose placeholders do not match the English original is ignored instead of used. A mismatch there let a language file crash the device, and language files now download from the Store, so this closes that off for good - including for files you write yourself.
|
||||
Messages on a channel could be silently dropped when an unused channel slot happened to match first. Channel sends also match by name at the moment of transmission, so a message can no longer go out encrypted to the wrong channel.
|
||||
The Store no longer jumps back to the top while you are scrolling it.
|
||||
"Overwrite oldest" now says plainly that favourites are never overwritten.
|
||||
A companion app can opt into receiving the full radio log over Bluetooth.
|
||||
More of the interface is translatable: the Discovered and Contacts sort menus, and the traffic panel.
|
||||
|
||||
# --- Community ------------------------------------------------------------------------------
|
||||
Pager keyboard shortcuts, portrait layout fixes, a theme-matching Wi-Fi icon and Heltec UI tweaks (oumike, PR #263).
|
||||
Thanks to Yoss101 and pisti87, whose reports drove the contact-store work - particularly Yoss101 for coming back to say a fix had only half worked, which is the report that found the rest of it. Thanks to pisti87 also for the character-rendering reports, and to istvan for finding the last text field with boxes in it.
|
||||
@@ -0,0 +1,14 @@
|
||||
# beta_65 — test build. Stable stays beta_63.
|
||||
# One user-facing note per non-blank, non-# line; # lines are section comments.
|
||||
|
||||
# --- Fixes for what beta_64 testers hit -------------------------------------------------
|
||||
Updating no longer leaves the device stuck on "LoRa radio not detected". On boards where the radio sits behind a peripheral power switch, an update ends in a software reset that drops that power and then checks for the radio before it has finished coming back up - so the first boot after an update could dead-end on the error screen until you power-cycled by hand. The firmware now does that power-cycle itself and tries again, so the update just works.
|
||||
Leaving an app by the back arrow no longer also pulls the status bar dropdown open. One tap did two things: it went back, and then the same tap fell through to the dropdown toggle.
|
||||
|
||||
# --- Position sharing, without broadcasting it ----------------------------------------------
|
||||
You can now share your position with specific contacts instead of putting it in your advert. An advert is broadcast in the clear to every node in range; this is sent only when a contact you chose asks for it, and it is encrypted to them. Settings > Mesh > "Share my location when asked" - Never, Chosen contacts only, or Anyone who asks - and you pick the contacts from each contact's own menu, where you can also take it back later. Requested by janmashat.
|
||||
|
||||
# --- For people writing apps -------------------------------------------------------------
|
||||
The Lua SDK documentation at wadamesh.com/sdk.html was wrong about the most basic thing and is now correct. It said to define your callbacks as globals named on_start and on_stop; the firmware has never called those. An app is a table you build and return, with on_open, on_tick, on_input, on_message and on_close. Following the old page gave you an app where nothing ran and nothing was reported - a blank screen. If you tried to write an app and gave up, that is why, and it is worth another go.
|
||||
The page also now documents everything added in beta_64: the clock, the buzzer, battery, GPS, per-app file storage, sending mesh messages, receiving them, and the permission model behind the last two.
|
||||
The app and language Store is served from a place nothing was publishing to, so translations sat at an older version than the ones in the project and a published app never appeared at all. Fixed, with the Hungarian, German and Dutch updates now live along with the SDK Test app.
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# wadamesh app+language store deploy: publish deploy/apps/ to the firmware VPS.
|
||||
#
|
||||
# The device reads BOTH catalogs from the firmware host, NOT the site host:
|
||||
# http://firmware.wadamesh.com/apps/apps.json <- Store > Apps
|
||||
# http://firmware.wadamesh.com/apps/langs.json <- Store > Languages
|
||||
# http://firmware.wadamesh.com/apps/lang/<ver>/<code>.lang
|
||||
# so this tree lives under the FIRMWARE web root, alongside releases/ and latest*/.
|
||||
#
|
||||
# WHY THIS SCRIPT EXISTS: there wasn't one. release.sh rsyncs out/firmware/ (which
|
||||
# does not contain apps/) and deploy-site.sh rsyncs deploy/site/ — so the store was
|
||||
# only ever updated by someone remembering to rsync it by hand, and twice they did
|
||||
# not. Translations sat at v8 on the server while the repo had v9/v10/v11, and a
|
||||
# published app never appeared at all. Editing deploy/apps/ is not publishing it;
|
||||
# running this is.
|
||||
#
|
||||
# Usage:
|
||||
# WADAMESH_VPS=user@your-vps scripts/deploy-apps.sh # deploy
|
||||
# WADAMESH_VPS=user@your-vps scripts/deploy-apps.sh --dry-run # preview only
|
||||
#
|
||||
# Optional:
|
||||
# WADAMESH_FW_PATH=/srv/wadamesh/firmware # override the firmware web root
|
||||
#
|
||||
# The VPS target comes from the environment — never commit it.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SRC="$ROOT/deploy/apps/"
|
||||
DEST="${WADAMESH_VPS:?set WADAMESH_VPS=user@host}"
|
||||
DEST_PATH="${WADAMESH_FW_PATH:-/srv/wadamesh/firmware}/apps"
|
||||
|
||||
DRY=""
|
||||
[ "${1:-}" = "--dry-run" ] && DRY="--dry-run"
|
||||
|
||||
# Sanity-check the catalogs before shipping them: a device that gets malformed JSON
|
||||
# shows an empty store with no clue why, and the versioned .lang dir each catalog
|
||||
# entry points at has to actually exist or the download 404s on the device.
|
||||
python3 - "$ROOT" <<'PY'
|
||||
import json, os, sys
|
||||
root = sys.argv[1]
|
||||
apps_dir = os.path.join(root, "deploy", "apps")
|
||||
langs = json.load(open(os.path.join(apps_dir, "langs.json")))["langs"]
|
||||
missing = [f"{l['code']} v{l['ver']}" for l in langs
|
||||
if not os.path.isfile(os.path.join(apps_dir, "lang", str(l["ver"]), l["code"] + ".lang"))]
|
||||
if missing:
|
||||
sys.exit("ABORT: langs.json points at files that do not exist: " + ", ".join(missing))
|
||||
json.load(open(os.path.join(apps_dir, "apps.json"))) # parse check
|
||||
print(f"catalogs ok: {len(langs)} languages, every referenced .lang present")
|
||||
PY
|
||||
|
||||
# --delete mirrors the repo: the store is generated content, the repo is the source
|
||||
# of truth, and stale versioned dirs left on the server are how a device ends up
|
||||
# offered a language file nothing points at any more.
|
||||
rsync -avz $DRY --delete "$SRC" "$DEST:$DEST_PATH/"
|
||||
|
||||
if [ -z "$DRY" ]; then
|
||||
echo
|
||||
echo "deployed deploy/apps/ -> $DEST:$DEST_PATH"
|
||||
echo "verify:"
|
||||
echo " curl -s http://firmware.wadamesh.com/apps/langs.json"
|
||||
echo " curl -s http://firmware.wadamesh.com/apps/apps.json"
|
||||
fi
|
||||
@@ -496,6 +496,140 @@ File file = openRead(_getContactsChannelsFS(), "/contacts3");
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(ESP32)
|
||||
// A full contacts write REWRITES THE WHOLE TABLE — 152 bytes per contact, so 304 KB at
|
||||
// MAX_CONTACTS=2000 — and the atomic swap below keeps a same-sized .tmp alongside it, so
|
||||
// the peak cost is ~608 KB of a card-less V4's 3.375 MB SPIFFS volume that is already
|
||||
// carrying one blob file per contact. SPIFFS garbage collection suspends the flash cache,
|
||||
// which stalls BOTH cores, and it gets dramatically worse the fuller the volume — that is
|
||||
// the #222 freeze, and taking the eviction blob delete off the packet path only removed
|
||||
// the smaller half of it (the reporter still saw stalls, and LONGER ones, because this
|
||||
// rewrite is what was left).
|
||||
//
|
||||
// But the table only ever CHANGES one slot at a time: eviction replaces contacts[oldest]
|
||||
// in place (the array is unsorted and never shifts), and an advert refresh touches a
|
||||
// single entry. So compare each record we would write against the record already on disk
|
||||
// and patch only the ones that actually differ — 152 bytes instead of 304 KB, with no
|
||||
// .tmp and no free-space spike. Reads never trigger GC, so the scan itself is cheap.
|
||||
//
|
||||
// Returns true only if the live file is now fully up to date. False means "not
|
||||
// applicable" — the caller must fall back to the full atomic rewrite. This never
|
||||
// truncates or renames, and the fallback rewrite repairs a partial patch, so a false
|
||||
// return always ends with a valid list on disk.
|
||||
bool DataStore::saveContactsInPlace(DataStoreHost* host, bool (*filter)(const ContactInfo& c)) {
|
||||
static const size_t REC = 152; // MUST match the record packing in saveContacts
|
||||
static const size_t CHUNK_RECS = 16;
|
||||
|
||||
FILESYSTEM* fs = _getContactsChannelsFS();
|
||||
if (!fs->exists(_rp("/contacts3"))) return false; // no live file yet -> full write
|
||||
|
||||
// Count first, with no I/O at all. A table that SHRANK needs records dropped off the
|
||||
// end and this FS API has no truncate, so bail BEFORE touching the file — that keeps
|
||||
// the fallback a clean rewrite instead of a half-patched one.
|
||||
uint32_t nrec = 0;
|
||||
{
|
||||
uint32_t idx = 0;
|
||||
ContactInfo c;
|
||||
while (host->getContactForSave(idx, c)) {
|
||||
if (!filter || filter(c)) nrec++;
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
File f = fs->open(_rp("/contacts3"), "r+");
|
||||
if (!f) return false;
|
||||
const size_t fsz = f.size();
|
||||
if (fsz == 0 || (fsz % REC) != 0 || (fsz / REC) > nrec) { f.close(); return false; }
|
||||
const uint32_t nfile = fsz / REC; // records currently on disk
|
||||
|
||||
uint8_t* wbuf = (uint8_t*)malloc(REC * CHUNK_RECS);
|
||||
uint8_t* rbuf = (uint8_t*)malloc(REC * CHUNK_RECS);
|
||||
if (!wbuf || !rbuf) { free(wbuf); free(rbuf); f.close(); return false; }
|
||||
|
||||
const uint32_t t0 = millis();
|
||||
bool ok = true;
|
||||
uint32_t base = 0; // record number of wbuf[0]
|
||||
size_t fill = 0; // records packed in wbuf
|
||||
uint32_t written = 0; // records actually re-written (the cost)
|
||||
|
||||
// Reconcile one slab: read what is there, and write back only the records that differ.
|
||||
// Records at or past the old end of file are appends (the table grew), written in
|
||||
// ascending order so each one extends the file contiguously.
|
||||
auto reconcile = [&](uint32_t at, size_t count) -> bool {
|
||||
const size_t off = (size_t)at * REC;
|
||||
const size_t bytes = count * REC;
|
||||
size_t in_file = (at < nfile) ? ((size_t)(nfile - at) * REC) : 0;
|
||||
if (in_file > bytes) in_file = bytes;
|
||||
if (in_file) {
|
||||
if (!f.seek(off)) return false;
|
||||
if ((size_t)f.read(rbuf, in_file) != in_file) return false;
|
||||
if (in_file == bytes && memcmp(rbuf, wbuf, bytes) == 0) return true; // slab unchanged
|
||||
}
|
||||
bool touched = false;
|
||||
for (size_t k = 0; k < count; k++) {
|
||||
const size_t ro = k * REC;
|
||||
if (ro + REC <= in_file && memcmp(rbuf + ro, wbuf + ro, REC) == 0) continue;
|
||||
if (!f.seek(off + ro)) return false;
|
||||
if (f.write(wbuf + ro, REC) != REC) return false;
|
||||
written++;
|
||||
touched = true;
|
||||
}
|
||||
// Read back what we just wrote and PROVE it landed. Positioned writes through
|
||||
// fopen("r+") are a far less travelled path than the whole-file rewrite this
|
||||
// replaces, it differs per filesystem (SPIFFS / FAT / LittleFS), and the data at
|
||||
// stake is the operator's contact list — the one thing worth more than the freeze
|
||||
// this fixes. If the read-back disagrees for any reason at all, bail and let the
|
||||
// caller do the full atomic rewrite, which is the known-good path. Costs one extra
|
||||
// read of only the slab that changed, and reads never trigger GC.
|
||||
if (touched) {
|
||||
if (!f.seek(off)) return false;
|
||||
if ((size_t)f.read(rbuf, bytes) != bytes) return false;
|
||||
if (memcmp(rbuf, wbuf, bytes) != 0) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
uint32_t idx = 0;
|
||||
ContactInfo c;
|
||||
uint8_t unused = 0;
|
||||
while (ok) {
|
||||
if (!host->getContactForSave(idx, c)) break;
|
||||
idx++;
|
||||
if (filter && !filter(c)) continue;
|
||||
|
||||
uint8_t* p = wbuf + fill * REC;
|
||||
memcpy(p, c.id.pub_key, 32); p += 32;
|
||||
memcpy(p, (uint8_t *)&c.name, 32); p += 32;
|
||||
*p++ = c.type;
|
||||
*p++ = c.flags;
|
||||
*p++ = unused;
|
||||
memcpy(p, (uint8_t *)&c.sync_since, 4); p += 4;
|
||||
memcpy(p, (uint8_t *)&c.out_path_len, 1); p += 1;
|
||||
memcpy(p, (uint8_t *)&c.last_advert_timestamp, 4); p += 4;
|
||||
memcpy(p, c.out_path, 64); p += 64;
|
||||
memcpy(p, (uint8_t *)&c.lastmod, 4); p += 4;
|
||||
memcpy(p, (uint8_t *)&c.gps_lat, 4); p += 4;
|
||||
memcpy(p, (uint8_t *)&c.gps_lon, 4); p += 4;
|
||||
fill++;
|
||||
|
||||
if (fill == CHUNK_RECS) { ok = reconcile(base, fill); base += fill; fill = 0; }
|
||||
}
|
||||
if (ok && fill > 0) { ok = reconcile(base, fill); base += fill; fill = 0; }
|
||||
|
||||
f.close();
|
||||
free(wbuf);
|
||||
free(rbuf);
|
||||
// base is the number of records reconciled; if it disagrees with the pre-count the
|
||||
// table changed underneath us — let the caller rewrite rather than trust the file.
|
||||
if (!(ok && base == nrec)) return false;
|
||||
_cs_recs = (uint16_t)(written > 0xFFFF ? 0xFFFF : written);
|
||||
_cs_ms = (uint16_t)((millis() - t0) > 0xFFFF ? 0xFFFF : (millis() - t0));
|
||||
_cs_in_place = true;
|
||||
_cs_any = true;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c)) {
|
||||
#if defined(HAS_TDISPLAY_P4)
|
||||
if (!p4OnStorageTask()) {
|
||||
@@ -514,6 +648,12 @@ void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactIn
|
||||
// that calls saveContacts stays balanced. On SD-routed devices this is cheap
|
||||
// (FAT has no such GC); it matters most for card-less SPIFFS devices.
|
||||
WdtHeavyGuard _wdt;
|
||||
|
||||
// Patch the live file in place when only individual records changed — the normal case
|
||||
// by far, and the one that was freezing card-less V4s (#222). Falls through to the full
|
||||
// atomic rewrite below whenever that is not provably safe.
|
||||
if (saveContactsInPlace(host, filter)) return;
|
||||
const uint32_t t0_full = millis(); // the expensive path — worth reporting when it runs
|
||||
#endif
|
||||
#if defined(ESP32)
|
||||
// Write to a TEMP file and swap it in only after it is FULLY written, so a partial
|
||||
@@ -528,6 +668,7 @@ void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactIn
|
||||
#endif
|
||||
File file = openWrite(_getContactsChannelsFS(), kContactsWriteTarget);
|
||||
bool wrote_ok = false; // true only if every record reached the file
|
||||
uint32_t recs_out = 0; // records written by this (full) rewrite
|
||||
if (file) {
|
||||
bool ok = true; // cleared on any short write (partial file)
|
||||
uint32_t idx = 0;
|
||||
@@ -563,6 +704,7 @@ void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactIn
|
||||
memcpy(p, (uint8_t *)&c.gps_lat, 4); p += 4;
|
||||
memcpy(p, (uint8_t *)&c.gps_lon, 4); p += 4;
|
||||
fill += REC;
|
||||
recs_out++;
|
||||
if (fill == REC * CHUNK_RECS) {
|
||||
ok = (file.write(buf, fill) == fill);
|
||||
fill = 0;
|
||||
@@ -592,6 +734,7 @@ void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactIn
|
||||
|
||||
if (!success) { ok = false; break; } // write failed -> keep the live file
|
||||
|
||||
recs_out++;
|
||||
idx++; // advance to next contact
|
||||
}
|
||||
}
|
||||
@@ -615,6 +758,13 @@ void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactIn
|
||||
cfs->remove(tmp); // discard the partial temp; the live contact list is untouched
|
||||
}
|
||||
}
|
||||
{
|
||||
const uint32_t el = millis() - t0_full;
|
||||
_cs_recs = (uint16_t)(recs_out > 0xFFFF ? 0xFFFF : recs_out);
|
||||
_cs_ms = (uint16_t)(el > 0xFFFF ? 0xFFFF : el);
|
||||
_cs_in_place = false;
|
||||
_cs_any = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,21 @@ public:
|
||||
bool savePrefs(const NodePrefs& prefs, double node_lat, double node_lon);
|
||||
void loadContacts(DataStoreHost* host);
|
||||
void saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL);
|
||||
#if defined(ESP32)
|
||||
// Patch only the contact records that changed, in place. True = live file is up to
|
||||
// date; false = caller must do the full atomic rewrite. See the definition for why.
|
||||
bool saveContactsInPlace(DataStoreHost* host, bool (*filter)(const ContactInfo& c));
|
||||
// Cost of the most recent contacts save, for the About diagnostics (#222). A full
|
||||
// rewrite here is the expensive path that stalls both cores on a card-less board.
|
||||
uint16_t lastContactsSaveRecs() const { return _cs_recs; }
|
||||
uint16_t lastContactsSaveMs() const { return _cs_ms; }
|
||||
bool lastContactsSaveInPlace() const { return _cs_in_place; }
|
||||
bool lastContactsSaveValid() const { return _cs_any; }
|
||||
private:
|
||||
uint16_t _cs_recs = 0, _cs_ms = 0;
|
||||
bool _cs_in_place = false, _cs_any = false;
|
||||
public:
|
||||
#endif
|
||||
void loadChannels(DataStoreHost* host);
|
||||
void saveChannels(DataStoreHost* host);
|
||||
void migrateToSecondaryFS();
|
||||
|
||||
+20
-1
@@ -2329,7 +2329,12 @@ void MyMesh::onContactOverwrite(const uint8_t* pub_key) {
|
||||
// it, so a burst of evictions cannot chain GC passes back to back.
|
||||
if (_pending_del_n < PENDING_DEL_MAX) {
|
||||
memcpy(_pending_del[_pending_del_n++], pub_key, PUB_KEY_SIZE);
|
||||
} // queue full: the blob is orphaned, harmless — reclaimed on the next wipe
|
||||
} else {
|
||||
// Overflow orphans the blob for good — no other path deletes it — so it is not the
|
||||
// "harmless" case the first cut of this assumed. Count it so a device that is
|
||||
// actually hitting the wall can say so instead of silently leaking flash.
|
||||
if (_orphaned_blobs < 0xFFFF) _orphaned_blobs++;
|
||||
}
|
||||
if (_serial->isConnected()) {
|
||||
out_frame[0] = PUSH_CODE_CONTACT_DELETED;
|
||||
memcpy(&out_frame[1], pub_key, PUB_KEY_SIZE);
|
||||
@@ -4559,6 +4564,20 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
char *np = strchr(sp, ':'); // look for separator char
|
||||
if (np) {
|
||||
*np++ = 0; // modify 'cmd_frame', replace ':' with null
|
||||
// #256: "ble.rxlog:1" opts THIS session's BLE companion into the full
|
||||
// per-packet RX log (0x88). It is not a sensor setting, so intercept it
|
||||
// before the sensor dispatch below. Coverage/region apps need it because
|
||||
// the transport codes and the full relay path exist only in that frame —
|
||||
// RESP_CODE_CHANNEL_MSG_RECV_V3 carries neither. Off by default and never
|
||||
// persisted, so the #46/#54 fix stands for everyone who does not ask.
|
||||
// Requested by the KiekR author (marcelverdult), who traced it for us.
|
||||
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
|
||||
if (strcmp(sp, "ble.rxlog") == 0) {
|
||||
MultiTransportCompanionInterface::bleSetRxLogFirehose(np[0] == '1');
|
||||
writeOKFrame();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
bool success = sensors.setSettingValue(sp, np);
|
||||
if (success) {
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
|
||||
+33
-1
@@ -145,6 +145,8 @@ public:
|
||||
NodePrefs *getNodePrefs();
|
||||
uint32_t getBLEPin();
|
||||
bool setBLEPin(uint32_t pin); // user-chosen 6-digit pairing code (persisted; applies next boot)
|
||||
uint32_t getOrphanedBlobs() const { return _orphaned_blobs; } // see PENDING_DEL_MAX (#222)
|
||||
DataStore* getStore() { return _store; } // for the About diagnostics
|
||||
|
||||
// Live device info accessors (used by the touch Settings → Device modal to
|
||||
// mirror the web client's "Device (live)" panel — public key prefix, channel
|
||||
@@ -265,10 +267,15 @@ protected:
|
||||
void onContactOverwrite(const uint8_t* pub_key) override;
|
||||
// Blob deletes queued by onContactOverwrite and drained from loop() — never
|
||||
// from packet handling, see #222 (SPIFFS GC stalls both cores).
|
||||
static const uint8_t PENDING_DEL_MAX = 8;
|
||||
// 8 was too shallow: a drop here does not just skip work, it ORPHANS the blob file
|
||||
// permanently (nothing else ever deletes it), and orphans accumulate on the one
|
||||
// resource that drives GC cost — how full the volume is. 32 costs 1 KB of RAM and
|
||||
// needs a 16-second eviction burst to overflow.
|
||||
static const uint8_t PENDING_DEL_MAX = 32;
|
||||
uint8_t _pending_del[PENDING_DEL_MAX][PUB_KEY_SIZE];
|
||||
uint8_t _pending_del_n = 0;
|
||||
uint32_t _next_pending_del_at = 0;
|
||||
uint16_t _orphaned_blobs = 0; // queue overflows — each one leaks a blob file for good
|
||||
void drainPendingBlobDeletes();
|
||||
bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
|
||||
void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override;
|
||||
@@ -964,6 +971,31 @@ public:
|
||||
* evictable when the table filled (#178): the star lived only in
|
||||
* TouchPrefs, invisible to the core. Returns false if the contact is gone;
|
||||
* a no-op flag state is success without a save. */
|
||||
// Per-contact telemetry permissions (#266: share position with chosen contacts
|
||||
// instead of broadcasting it in every advert).
|
||||
//
|
||||
// MeshCore packs these into contact.flags ABOVE the favourite bit — the request
|
||||
// handler does `contact.flags >> 1` before masking with TELEM_PERM_* — so the bit
|
||||
// for a given TELEM_PERM_x lives at (x << 1) here. They only take effect for a
|
||||
// category whose telemetry_mode_* is TELEM_MODE_ALLOW_FLAGS; ALLOW_ALL ignores
|
||||
// them and answers everyone, DENY answers nobody.
|
||||
static uint8_t contactTelemBit(uint8_t telem_perm) { return (uint8_t)(telem_perm << 1); }
|
||||
|
||||
bool uiGetContactTelemetryPerm(const uint8_t pub_key[32], uint8_t telem_perm) {
|
||||
ContactInfo* slot = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
||||
return slot && (slot->flags & contactTelemBit(telem_perm)) != 0;
|
||||
}
|
||||
bool uiSetContactTelemetryPerm(const uint8_t pub_key[32], uint8_t telem_perm, bool on) {
|
||||
ContactInfo* slot = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
||||
if (!slot) return false;
|
||||
const uint8_t bit = contactTelemBit(telem_perm);
|
||||
const uint8_t nf = on ? (uint8_t)(slot->flags | bit) : (uint8_t)(slot->flags & ~bit);
|
||||
if (nf == slot->flags) return true;
|
||||
slot->flags = nf;
|
||||
saveContacts();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool uiSetContactFavorite(const uint8_t pub_key[32], bool fav) {
|
||||
ContactInfo* slot = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
||||
if (!slot) return false;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#define PUSH_CODE_LOG_RX_DATA 0x88
|
||||
|
||||
bool MultiTransportCompanionInterface::s_ble_rxlog_once = false;
|
||||
bool MultiTransportCompanionInterface::s_ble_rxlog_all = false; // #256: opt-in, per session
|
||||
|
||||
MultiTransportCompanionInterface::MultiTransportCompanionInterface()
|
||||
: _tcp_port(0), _ws_port(0), _tcp_started(false), _ws_started(false), _tcp_enabled(true), _isEnabled(false), _broadcast(false), _last_reply_target(REPLY_TARGET_USB), _ota_tcp_suspended(false), _ota_ws_suspended(false), _ota_ws_listen_paused(false)
|
||||
@@ -548,7 +549,11 @@ size_t MultiTransportCompanionInterface::writeFrameToAll(const uint8_t src[], si
|
||||
// blanket skip landed in beta_23 — issue #94), and a few echoes per send are
|
||||
// nowhere near the flood that caused #46/#54.
|
||||
const bool rxlog = (len > 0 && src[0] == PUSH_CODE_LOG_RX_DATA);
|
||||
const bool ble_pass = rxlog && s_ble_rxlog_once;
|
||||
// #256: a companion that wants the whole firehose (coverage/region mapping —
|
||||
// region and the full path exist ONLY in this frame) can ask for it per
|
||||
// session with CMD_SET_CUSTOM_VAR "ble.rxlog:1". Default stays off, so the
|
||||
// #46/#54 behaviour above is unchanged for every app that does not opt in.
|
||||
const bool ble_pass = rxlog && (s_ble_rxlog_once || s_ble_rxlog_all);
|
||||
if (rxlog) s_ble_rxlog_once = false; // consume the one-shot either way
|
||||
const bool skip_ble = rxlog && !ble_pass;
|
||||
if (!skip_ble && _ble_begun && _ble_enabled && _ble.isConnected() && _ble.writeFrame(src, len) != len)
|
||||
|
||||
@@ -94,6 +94,13 @@ public:
|
||||
* gets exactly the few frames the app needs and none of the flood (#94).
|
||||
* Same-thread set-then-consume (both in the mesh loop), so no atomics. */
|
||||
static void bleAllowNextRxLog() { s_ble_rxlog_once = true; }
|
||||
/** Opt a BLE companion into the FULL per-packet RX log (#256).
|
||||
* Off by default and NOT persisted: an app asks for it after connecting, via
|
||||
* CMD_SET_CUSTOM_VAR "ble.rxlog:1". Deliberately session-scoped — a stored
|
||||
* flag would silently reinstate the #46/#54 flood for someone who tried a
|
||||
* coverage app once and moved on, on a link that cannot afford it. */
|
||||
static void bleSetRxLogFirehose(bool on) { s_ble_rxlog_all = on; }
|
||||
static bool bleRxLogFirehose() { return s_ble_rxlog_all; }
|
||||
bool companionUnsolicitedPushesBroadcastToAll() const override { return _broadcast; }
|
||||
size_t checkRecvFrame(uint8_t dest[]) override;
|
||||
|
||||
@@ -105,6 +112,7 @@ public:
|
||||
|
||||
private:
|
||||
static bool s_ble_rxlog_once; // one-shot BLE pass for the next RX-log frame (#94)
|
||||
static bool s_ble_rxlog_all; // app opted into the whole RX log over BLE (#256); session-scoped
|
||||
public:
|
||||
|
||||
private:
|
||||
|
||||
@@ -740,6 +740,32 @@ void setup() {
|
||||
|
||||
bool radio_ok = radio_init();
|
||||
if (!radio_ok) { delay(150); radio_ok = radio_init(); } // one retry: transient SPI/reset flakes
|
||||
#if defined(PIN_PERF_POWERON)
|
||||
// Last resort: do what the error screen would otherwise ask the USER to do —
|
||||
// power-cycle the module — by bouncing the rail that feeds it.
|
||||
//
|
||||
// On boards with a peripheral power gate (T-Deck, GPIO10) the LoRa module is only
|
||||
// powered once that pin is driven high in Board::begin(), a few milliseconds before
|
||||
// it gets probed. A cold start survives that because the rail was already settled;
|
||||
// a SOFTWARE reset does not. An OTA update ends in ESP.restart(), which releases the
|
||||
// pin, collapses the rail, and re-drives it immediately — so the radio is probed
|
||||
// while it is still coming up, and the boot dead-ends on "LoRa radio not detected"
|
||||
// until the user power-cycles by hand (reported on a T-Deck straight after an
|
||||
// update; a reboot cleared it). A brief, deliberate LOW gives the module a clean
|
||||
// power-on reset regardless of how this boot was reached.
|
||||
//
|
||||
// Only reached when the alternative is halting with the fatal screen, so it cannot
|
||||
// regress a healthy boot: a board that probes fine never runs any of this.
|
||||
if (!radio_ok) {
|
||||
Serial.println("[BOOT] radio not detected — power-cycling the peripheral rail and retrying");
|
||||
digitalWrite(PIN_PERF_POWERON, LOW);
|
||||
delay(120); // long enough for the rail to actually drain
|
||||
digitalWrite(PIN_PERF_POWERON, HIGH);
|
||||
delay(250); // POR + crystal startup before the module answers SPI
|
||||
radio_ok = radio_init();
|
||||
if (radio_ok) Serial.println("[BOOT] radio came up after the power-cycle");
|
||||
}
|
||||
#endif
|
||||
if (!radio_ok) {
|
||||
// A dead or absent LoRa module used to halt behind the frozen boot logo,
|
||||
// with the only clue on serial (#244). Say it on the panel instead.
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
#include <lvgl.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <FS.h>
|
||||
#include <time.h>
|
||||
#include <math.h> // isnan for optional sensor fields // wada.sys.epoch/datetime (#245)
|
||||
#include "AppPage.h"
|
||||
#include "device_caps.h"
|
||||
#include "helpers/esp32/WdtHeavyGuard.h" // wada.fs writes can trigger SPIFFS GC
|
||||
|
||||
extern "C" {
|
||||
#include "lua.h"
|
||||
@@ -22,6 +26,7 @@ extern "C" {
|
||||
// to keep this TU decoupled from the 47k-line UITask.cpp).
|
||||
extern const lv_font_t* luaHostFontForSize(int size_class); // 12/14/16 -> g_font_*
|
||||
extern void luaHostToast(const char* msg, int ms); // showAlert passthrough
|
||||
extern bool luaHostBeep(); // notification chime; false = no sounder / muted
|
||||
extern fs::FS* luaHostAppFs(); // /apps storage root FS (may be null)
|
||||
extern void luaHostAppPath(char* out, size_t cap, const char* rel); // prefixes the store root
|
||||
extern int luaHostContactAt(int idx, char* name, size_t name_cap, int* type, uint32_t* secs_ago,
|
||||
@@ -32,6 +37,16 @@ extern void luaHostRadioStats(float* rssi, float* noise, uint32_t* rx_air_s, uin
|
||||
extern void luaHostRadioStats2(uint32_t* rx_evt, uint32_t* rx_drop, uint32_t* tx_pkts,
|
||||
float* freq, float* bw, int* sf, int* duty_pct);
|
||||
extern void luaHostSelfInfo(char* name, size_t name_cap, double* lat, double* lon);
|
||||
#if CAP_LUA_SDK_EXT
|
||||
extern void luaHostBattery(uint16_t* mv, int* pct, bool* charging);
|
||||
// No HDOP: MeshCore's LocationProvider interface does not expose one, and a
|
||||
// fabricated accuracy figure is worse than none for anything that would use it.
|
||||
extern bool luaHostGps(double* lat, double* lon, int* sats);
|
||||
extern int luaHostSendPerm(const char* app_id); // 1 grant, -1 refused, 0 unasked
|
||||
extern bool luaHostReadPerm(const char* app_id); // may be shown incoming messages
|
||||
extern void luaHostRequestSendPerm(const char* app_id, const char* app_name); // raises the consent prompt
|
||||
extern bool luaHostMeshSendChannel(const char* chan_name, const char* text);
|
||||
#endif
|
||||
// DO NOT name the network client type here. What "WiFiClient" means depends on the
|
||||
// board: a real class on the S3 envs, a `using WiFiClient = NetworkClient` alias
|
||||
// under arduino-esp32 3.x (Tanmatsu), and a #define to C6Client on the T-Display P4
|
||||
@@ -95,6 +110,8 @@ struct Host {
|
||||
int ref_app = LUA_NOREF; // the table the chunk returned
|
||||
int ref_btncb = LUA_NOREF; // { [btn lightuserdata] = fn } button callbacks
|
||||
bool store_dirty = false;
|
||||
uint32_t fs_last_write_ms = 0; // wada.fs write rate limit (#222 lesson)
|
||||
uint32_t mesh_last_send_ms = 0; // wada.mesh.send airtime rate limit
|
||||
bool in_lua = false; // re-entrancy guard (dismiss from inside a callback)
|
||||
bool want_close = false;
|
||||
char id[24] = "";
|
||||
@@ -439,6 +456,82 @@ int uiTextH(lua_State* L) {
|
||||
}
|
||||
|
||||
int sysMillis(lua_State* L) { lua_pushinteger(L, (lua_Integer)millis()); return 1; }
|
||||
// wada.sys.epoch() -> Unix seconds, or nil when the clock has never been set.
|
||||
// nil rather than 0 on purpose: an app that stamps a log wants to be able to
|
||||
// tell "no clock yet" from "1970", and the device genuinely boots without one
|
||||
// until GPS, NTP or a mesh peer supplies the time (#245).
|
||||
int sysEpoch(lua_State* L) {
|
||||
const time_t now = time(nullptr);
|
||||
if (now < 1700000000) { lua_pushnil(L); return 1; } // same sane-clock floor the mesh uses
|
||||
lua_pushinteger(L, (lua_Integer)now);
|
||||
return 1;
|
||||
}
|
||||
// wada.sys.datetime() -> table in LOCAL time (the timezone the user picked),
|
||||
// because every use for it is display. Fields match pisti87's request in #245.
|
||||
// nil under the same unset-clock rule as epoch().
|
||||
int sysDatetime(lua_State* L) {
|
||||
const time_t now = time(nullptr);
|
||||
struct tm tmv;
|
||||
if (now < 1700000000 || !localtime_r(&now, &tmv)) { lua_pushnil(L); return 1; }
|
||||
lua_createtable(L, 0, 7);
|
||||
lua_pushinteger(L, tmv.tm_year + 1900); lua_setfield(L, -2, "year");
|
||||
lua_pushinteger(L, tmv.tm_mon + 1); lua_setfield(L, -2, "month");
|
||||
lua_pushinteger(L, tmv.tm_mday); lua_setfield(L, -2, "day");
|
||||
lua_pushinteger(L, tmv.tm_hour); lua_setfield(L, -2, "hour");
|
||||
lua_pushinteger(L, tmv.tm_min); lua_setfield(L, -2, "min");
|
||||
lua_pushinteger(L, tmv.tm_sec); lua_setfield(L, -2, "sec");
|
||||
lua_pushinteger(L, tmv.tm_wday); lua_setfield(L, -2, "wday"); // 0 = Sunday
|
||||
return 1;
|
||||
}
|
||||
// wada.sys.beep() -> true if a sound was actually produced. #245 asked for a
|
||||
// melody player; what the firmware has is one notification chime, and only some
|
||||
// boards have a sounder at all. Exposing the truthful primitive beats inventing
|
||||
// a melody engine an app could not rely on: the return value lets an app fall
|
||||
// back to a visual cue instead of silently doing nothing.
|
||||
int sysBeep(lua_State* L) { lua_pushboolean(L, luaHostBeep()); return 1; }
|
||||
|
||||
// wada.sys.caps() -> what THIS board actually offers. Apps must branch on this
|
||||
// rather than assume: the extended SDK is absent on low-resource boards (see
|
||||
// CAP_LUA_SDK_EXT in device_caps.h), and a store app runs on all of them.
|
||||
int sysCaps(lua_State* L) {
|
||||
lua_createtable(L, 0, 4);
|
||||
lua_pushboolean(L, CAP_LUA_SDK_EXT); lua_setfield(L, -2, "sdk_ext");
|
||||
lua_pushboolean(L, CAP_KEYBOARD); lua_setfield(L, -2, "keyboard");
|
||||
lua_pushboolean(L, CAP_TOUCH); lua_setfield(L, -2, "touch");
|
||||
lua_pushboolean(L, CAP_SD); lua_setfield(L, -2, "sd");
|
||||
return 1;
|
||||
}
|
||||
|
||||
#if CAP_LUA_SDK_EXT
|
||||
// wada.sys.battery() -> { mv, pct, charging }
|
||||
int sysBattery(lua_State* L) {
|
||||
uint16_t mv = 0; int pct = -1; bool chg = false;
|
||||
luaHostBattery(&mv, &pct, &chg);
|
||||
lua_createtable(L, 0, 3);
|
||||
lua_pushinteger(L, mv); lua_setfield(L, -2, "mv");
|
||||
lua_pushinteger(L, pct); lua_setfield(L, -2, "pct");
|
||||
lua_pushboolean(L, chg); lua_setfield(L, -2, "charging");
|
||||
return 1;
|
||||
}
|
||||
// wada.sys.gps() -> { lat, lon, sats } or nil with NO fix.
|
||||
// nil rather than stale coordinates: wada.mesh.self() already hands out the last
|
||||
// known position, and an app plotting a track needs to know the difference.
|
||||
int sysGps(lua_State* L) {
|
||||
double lat = 0, lon = 0; int sats = 0;
|
||||
if (!luaHostGps(&lat, &lon, &sats)) { lua_pushnil(L); return 1; }
|
||||
lua_createtable(L, 0, 3);
|
||||
lua_pushnumber(L, lat); lua_setfield(L, -2, "lat");
|
||||
lua_pushnumber(L, lon); lua_setfield(L, -2, "lon");
|
||||
lua_pushinteger(L, sats); lua_setfield(L, -2, "sats");
|
||||
return 1;
|
||||
}
|
||||
// NOT exposed: wada.sys.sensors(). The only environment-sensor source in the
|
||||
// firmware is LocalEnvSnapshot, which is #if defined(HAS_EXPANSION_KIT) -- the
|
||||
// Heltec V4 Expansion Kit. That is precisely the board CAP_LUA_SDK_EXT excludes,
|
||||
// so the function would return nil on every board that could call it. Sensor
|
||||
// access belongs on a HARDWARE gate (does this board have the rail?) rather than
|
||||
// this MEMORY gate, and wants its own caps() entry when it lands.
|
||||
#endif // CAP_LUA_SDK_EXT
|
||||
int sysToast(lua_State* L) { luaHostToast(luaL_checkstring(L, 1), (int)luaL_optinteger(L, 2, 1500)); return 0; }
|
||||
int sysRandom(lua_State* L) {
|
||||
// esp_random-backed: apps should not have to seed math.random for games
|
||||
@@ -492,6 +585,51 @@ int timerStop(lua_State* L) {
|
||||
// Store: a Lua table mirrored to /apps/<id>.sav as "k=v" lines (strings and
|
||||
// numbers only). Loaded at launch, flushed on dismiss when dirty — tiny, rare,
|
||||
// and off the hot path (touch_perf: never spam flash from the UI thread).
|
||||
#if CAP_LUA_SDK_EXT
|
||||
// wada.mesh.send(channel, text) -> true | false, reason
|
||||
//
|
||||
// The ONLY write path an app has into the mesh, and the one API where a bad app
|
||||
// costs other people rather than just its own device: LoRa is a shared channel,
|
||||
// and anything sent goes out under the user's node name.
|
||||
//
|
||||
// Three gates, in order:
|
||||
// 1. per-app consent, granted by the user once and bound to THIS app id
|
||||
// 2. an airtime rate limit, far stricter than the filesystem one
|
||||
// 3. a length cap, so one call cannot occupy the channel
|
||||
//
|
||||
// The app never sees a prompt API and cannot pre-approve itself: the first call
|
||||
// without a grant fails AND raises the prompt, so consent is always something
|
||||
// the user did, not something the app asked for at a convenient moment.
|
||||
static const uint32_t kMeshSendMinGapMs = 5000; // per app
|
||||
static const size_t kMeshSendMaxLen = 180; // one LoRa text payload
|
||||
|
||||
int meshSend(lua_State* L) {
|
||||
const char* chan = luaL_checkstring(L, 1);
|
||||
size_t len = 0;
|
||||
const char* text = luaL_checklstring(L, 2, &len);
|
||||
if (!s_h) { lua_pushboolean(L, 0); lua_pushstring(L, "no app"); return 2; }
|
||||
if (len == 0 || len > kMeshSendMaxLen) {
|
||||
lua_pushboolean(L, 0); lua_pushstring(L, "bad length"); return 2;
|
||||
}
|
||||
const int perm = luaHostSendPerm(s_h->id);
|
||||
if (perm != 1) {
|
||||
if (perm == 0) luaHostRequestSendPerm(s_h->id, s_h->id); // ask once, never in a loop
|
||||
lua_pushboolean(L, 0);
|
||||
lua_pushstring(L, perm == 0 ? "permission requested" : "permission denied");
|
||||
return 2;
|
||||
}
|
||||
const uint32_t now = millis();
|
||||
if (s_h->mesh_last_send_ms && (uint32_t)(now - s_h->mesh_last_send_ms) < kMeshSendMinGapMs) {
|
||||
lua_pushboolean(L, 0); lua_pushstring(L, "too fast"); return 2;
|
||||
}
|
||||
const bool ok = luaHostMeshSendChannel(chan, text);
|
||||
if (ok) s_h->mesh_last_send_ms = now; // only a real transmission starts the clock
|
||||
lua_pushboolean(L, ok);
|
||||
if (!ok) { lua_pushstring(L, "no such channel"); return 2; }
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
int storeGet(lua_State* L) {
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "wada.storetab");
|
||||
lua_pushvalue(L, 1);
|
||||
@@ -511,6 +649,124 @@ int storeSet(lua_State* L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if CAP_LUA_SDK_EXT
|
||||
// ---- wada.fs: scoped file access -------------------------------------------
|
||||
// Every app gets ONE directory, /apps/<id>.d/, and cannot address anything
|
||||
// outside it. Store apps are user-submitted, so the name check is a security
|
||||
// boundary rather than tidiness: no '/', no '..', no leading dot, and a strict
|
||||
// [A-Za-z0-9._-] whitelist. Rejecting outright beats sanitising -- a sanitiser
|
||||
// that "fixes" ../../identity into something valid is how these go wrong.
|
||||
static const size_t kFsMaxFile = 32u * 1024u; // per file
|
||||
static const uint32_t kFsMinGapMs = 1000; // between writes, per app
|
||||
|
||||
static bool fsSafeName(const char* n) {
|
||||
if (!n || !*n || n[0] == '.') return false;
|
||||
size_t len = 0;
|
||||
for (const char* p = n; *p; ++p, ++len) {
|
||||
const char c = *p;
|
||||
const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-';
|
||||
if (!ok) return false;
|
||||
}
|
||||
return len <= 32;
|
||||
}
|
||||
static bool fsPath(char* out, size_t cap, const char* name) {
|
||||
if (!s_h || !fsSafeName(name)) return false;
|
||||
char rel[96];
|
||||
snprintf(rel, sizeof rel, "/apps/%s.d/%s", s_h->id, name);
|
||||
luaHostAppPath(out, cap, rel);
|
||||
return true;
|
||||
}
|
||||
static void fsEnsureDir() {
|
||||
fs::FS* fs = luaHostAppFs();
|
||||
if (!fs || !s_h) return;
|
||||
char d[80], rel[64];
|
||||
luaHostAppPath(d, sizeof d, "/apps"); fs->mkdir(d);
|
||||
snprintf(rel, sizeof rel, "/apps/%s.d", s_h->id);
|
||||
luaHostAppPath(d, sizeof d, rel); fs->mkdir(d);
|
||||
}
|
||||
// Shared by write and append. Rate-limited on purpose: on internal flash a burst
|
||||
// of small writes triggers garbage collection, and a GC pass suspends the flash
|
||||
// cache and stalls BOTH cores -- the fault behind #222 and the beta_25 bootloop.
|
||||
// An app that ignores the advice in deploy/apps/README.md and writes every frame
|
||||
// gets `false, "too fast"` instead of wedging the device. The guard covers the
|
||||
// case where a write DOES trigger GC, so it stalls briefly instead of rebooting.
|
||||
static int fsWriteCommon(lua_State* L, const char* mode) {
|
||||
const char* name = luaL_checkstring(L, 1);
|
||||
size_t len = 0;
|
||||
const char* data = luaL_checklstring(L, 2, &len);
|
||||
char path[128];
|
||||
if (!fsPath(path, sizeof path, name)) { lua_pushboolean(L, 0); lua_pushstring(L, "bad name"); return 2; }
|
||||
if (len > kFsMaxFile) { lua_pushboolean(L, 0); lua_pushstring(L, "too big"); return 2; }
|
||||
const uint32_t now = millis();
|
||||
if (s_h->fs_last_write_ms && (uint32_t)(now - s_h->fs_last_write_ms) < kFsMinGapMs) {
|
||||
lua_pushboolean(L, 0); lua_pushstring(L, "too fast"); return 2;
|
||||
}
|
||||
fs::FS* fs = luaHostAppFs();
|
||||
if (!fs) { lua_pushboolean(L, 0); lua_pushstring(L, "no storage"); return 2; }
|
||||
fsEnsureDir();
|
||||
bool ok = false;
|
||||
{
|
||||
WdtHeavyGuard guard; // a GC pass here must stall, not reboot
|
||||
File f = fs->open(path, mode);
|
||||
if (f) { ok = (f.write((const uint8_t*)data, len) == len); f.close(); }
|
||||
}
|
||||
s_h->fs_last_write_ms = now;
|
||||
lua_pushboolean(L, ok);
|
||||
if (!ok) { lua_pushstring(L, "write failed"); return 2; }
|
||||
return 1;
|
||||
}
|
||||
int fsWrite(lua_State* L) { return fsWriteCommon(L, "w"); }
|
||||
int fsAppend(lua_State* L) { return fsWriteCommon(L, "a"); }
|
||||
|
||||
int fsRead(lua_State* L) {
|
||||
const char* name = luaL_checkstring(L, 1);
|
||||
char path[128];
|
||||
if (!fsPath(path, sizeof path, name)) { lua_pushnil(L); return 1; }
|
||||
fs::FS* fs = luaHostAppFs();
|
||||
if (!fs) { lua_pushnil(L); return 1; }
|
||||
File f = fs->open(path, "r");
|
||||
if (!f) { lua_pushnil(L); return 1; }
|
||||
size_t n = f.size();
|
||||
if (n > kFsMaxFile) n = kFsMaxFile; // never hand Lua an unbounded buffer
|
||||
luaL_Buffer b;
|
||||
char* dst = luaL_buffinitsize(L, &b, n);
|
||||
const size_t got = f.read((uint8_t*)dst, n);
|
||||
f.close();
|
||||
luaL_pushresultsize(&b, got);
|
||||
return 1;
|
||||
}
|
||||
int fsRemove(lua_State* L) {
|
||||
const char* name = luaL_checkstring(L, 1);
|
||||
char path[128];
|
||||
fs::FS* fs = luaHostAppFs();
|
||||
if (!fs || !fsPath(path, sizeof path, name)) { lua_pushboolean(L, 0); return 1; }
|
||||
lua_pushboolean(L, fs->remove(path));
|
||||
return 1;
|
||||
}
|
||||
int fsList(lua_State* L) {
|
||||
lua_newtable(L);
|
||||
fs::FS* fs = luaHostAppFs();
|
||||
if (!fs || !s_h) return 1;
|
||||
char d[80], rel[64];
|
||||
snprintf(rel, sizeof rel, "/apps/%s.d", s_h->id);
|
||||
luaHostAppPath(d, sizeof d, rel);
|
||||
File dir = fs->open(d);
|
||||
if (!dir || !dir.isDirectory()) return 1;
|
||||
int i = 0;
|
||||
for (File e = dir.openNextFile(); e; e = dir.openNextFile()) {
|
||||
if (e.isDirectory()) continue;
|
||||
lua_createtable(L, 0, 2);
|
||||
const char* nm = strrchr(e.name(), '/');
|
||||
lua_pushstring(L, nm ? nm + 1 : e.name()); lua_setfield(L, -2, "name");
|
||||
lua_pushinteger(L, (lua_Integer)e.size()); lua_setfield(L, -2, "size");
|
||||
lua_rawseti(L, -2, ++i);
|
||||
}
|
||||
dir.close();
|
||||
return 1;
|
||||
}
|
||||
#endif // CAP_LUA_SDK_EXT
|
||||
|
||||
void storePath(char* out, size_t cap) {
|
||||
char rel[40];
|
||||
snprintf(rel, sizeof rel, "/apps/%s.sav", s_h->id);
|
||||
@@ -587,6 +843,38 @@ void sendInput(const char* type, const char* dir, int x, int y) {
|
||||
serviceDeferredClose();
|
||||
}
|
||||
|
||||
// Key delivery reuses on_input rather than adding an on_key callback: an app
|
||||
// already branches on ev.type, and one entry point keeps a Lua app's input model
|
||||
// to a single place. type="key", with `key` as a one-character string for
|
||||
// printable input and a name ("up", "enter", ...) for the rest, so an app can
|
||||
// write ev.key == "w" without knowing scancodes.
|
||||
void sendKey(int key) {
|
||||
if (!s_h || !s_h->L) return;
|
||||
if (!pushCallback(s_h, "on_input")) return;
|
||||
lua_State* L = s_h->L;
|
||||
lua_createtable(L, 0, 2);
|
||||
lua_pushstring(L, "key"); lua_setfield(L, -2, "type");
|
||||
char buf[8];
|
||||
const char* named = nullptr;
|
||||
switch (key) {
|
||||
case LV_KEY_UP: named = "up"; break;
|
||||
case LV_KEY_DOWN: named = "down"; break;
|
||||
case LV_KEY_LEFT: named = "left"; break;
|
||||
case LV_KEY_RIGHT: named = "right"; break;
|
||||
case LV_KEY_ENTER: named = "enter"; break;
|
||||
case LV_KEY_ESC: named = "esc"; break;
|
||||
case '\b': case 127: named = "backspace"; break;
|
||||
default: break;
|
||||
}
|
||||
if (named) { lua_pushstring(L, named); }
|
||||
else if (key >= 32 && key < 127) { buf[0] = (char)key; buf[1] = '\0'; lua_pushstring(L, buf); }
|
||||
else { lua_pushnil(L); }
|
||||
lua_setfield(L, -2, "key");
|
||||
lua_pushinteger(L, key); lua_setfield(L, -2, "code"); // raw, for anything unmapped
|
||||
guardedCall(s_h, 1);
|
||||
serviceDeferredClose();
|
||||
}
|
||||
|
||||
void gestureCb(lv_event_t* e) {
|
||||
lv_dir_t d = lv_indev_get_gesture_dir(lv_indev_get_act());
|
||||
const char* dir = (d == LV_DIR_TOP) ? "up" : (d == LV_DIR_BOTTOM) ? "down"
|
||||
@@ -745,6 +1033,14 @@ void openWada(lua_State* L) {
|
||||
lua_pushcfunction(L, sysToast); lua_setfield(L, -2, "toast");
|
||||
lua_pushcfunction(L, sysBoard); lua_setfield(L, -2, "board");
|
||||
lua_pushcfunction(L, sysRandom); lua_setfield(L, -2, "random");
|
||||
lua_pushcfunction(L, sysEpoch); lua_setfield(L, -2, "epoch"); // #245
|
||||
lua_pushcfunction(L, sysDatetime); lua_setfield(L, -2, "datetime"); // #245
|
||||
lua_pushcfunction(L, sysBeep); lua_setfield(L, -2, "beep"); // #245
|
||||
lua_pushcfunction(L, sysCaps); lua_setfield(L, -2, "caps"); // feature detection
|
||||
#if CAP_LUA_SDK_EXT
|
||||
lua_pushcfunction(L, sysBattery); lua_setfield(L, -2, "battery");
|
||||
lua_pushcfunction(L, sysGps); lua_setfield(L, -2, "gps");
|
||||
#endif
|
||||
lua_setfield(L, -2, "sys");
|
||||
|
||||
lua_newtable(L); // wada.timer
|
||||
@@ -757,11 +1053,24 @@ void openWada(lua_State* L) {
|
||||
lua_pushcfunction(L, storeSet); lua_setfield(L, -2, "set");
|
||||
lua_setfield(L, -2, "store");
|
||||
|
||||
#if CAP_LUA_SDK_EXT
|
||||
lua_newtable(L); // wada.fs (scoped to /apps/<id>.d)
|
||||
lua_pushcfunction(L, fsRead); lua_setfield(L, -2, "read");
|
||||
lua_pushcfunction(L, fsWrite); lua_setfield(L, -2, "write");
|
||||
lua_pushcfunction(L, fsAppend); lua_setfield(L, -2, "append");
|
||||
lua_pushcfunction(L, fsList); lua_setfield(L, -2, "list");
|
||||
lua_pushcfunction(L, fsRemove); lua_setfield(L, -2, "remove");
|
||||
lua_setfield(L, -2, "fs");
|
||||
#endif
|
||||
|
||||
lua_newtable(L); // wada.mesh (read-only)
|
||||
lua_pushcfunction(L, meshContacts); lua_setfield(L, -2, "contacts");
|
||||
lua_pushcfunction(L, meshRxLog); lua_setfield(L, -2, "rx_log");
|
||||
lua_pushcfunction(L, meshStats); lua_setfield(L, -2, "stats");
|
||||
lua_pushcfunction(L, meshSelf); lua_setfield(L, -2, "self");
|
||||
#if CAP_LUA_SDK_EXT
|
||||
lua_pushcfunction(L, meshSend); lua_setfield(L, -2, "send"); // consent-gated
|
||||
#endif
|
||||
lua_setfield(L, -2, "mesh");
|
||||
|
||||
lua_newtable(L); // wada.net
|
||||
@@ -821,6 +1130,38 @@ void hostTeardown(); // fwd
|
||||
// ---------------------------------------------------------------------------
|
||||
bool luaAppIsOpen() { return s_h != nullptr; }
|
||||
|
||||
// Returns true when the running app took the key. False lets the firmware keep
|
||||
// its own handling, so an app that does not implement on_input changes nothing.
|
||||
#if CAP_LUA_SDK_EXT
|
||||
// Incoming channel message -> app.on_message{channel, sender, text}. Permission
|
||||
// checked HERE rather than at subscribe time, so revoking it in Settings stops
|
||||
// delivery immediately instead of at the next launch.
|
||||
static void deliverMessage(const char* channel, const char* sender, const char* text) {
|
||||
if (!s_h || !s_h->L) return;
|
||||
if (!luaHostReadPerm(s_h->id)) return;
|
||||
if (!pushCallback(s_h, "on_message")) return;
|
||||
lua_State* L = s_h->L;
|
||||
lua_createtable(L, 0, 3);
|
||||
lua_pushstring(L, channel ? channel : ""); lua_setfield(L, -2, "channel");
|
||||
lua_pushstring(L, sender ? sender : ""); lua_setfield(L, -2, "sender");
|
||||
lua_pushstring(L, text ? text : ""); lua_setfield(L, -2, "text");
|
||||
guardedCall(s_h, 1);
|
||||
serviceDeferredClose();
|
||||
}
|
||||
#endif
|
||||
|
||||
bool luaAppKey(int key) {
|
||||
if (!s_h || !s_h->L || key <= 0) return false;
|
||||
sendKey(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
#if CAP_LUA_SDK_EXT
|
||||
void luaAppMessage(const char* channel, const char* sender, const char* text) {
|
||||
deliverMessage(channel, sender, text);
|
||||
}
|
||||
#endif
|
||||
|
||||
void luaAppSteer(int dx, int dy) {
|
||||
if (!s_h) return;
|
||||
const char* dir = (dy < 0) ? "up" : (dy > 0) ? "down" : (dx < 0) ? "left" : (dx > 0) ? "right" : nullptr;
|
||||
|
||||
@@ -20,8 +20,15 @@ bool luaAppLaunchFile(const char* id, const char* title, const char* embedded, s
|
||||
bool luaAppIsOpen();
|
||||
void luaAppDismiss(); // THE close path (AppPage back hook)
|
||||
void luaAppSteer(int dx, int dy); // trackball/keypad direction -> on_input
|
||||
bool luaAppKey(int key); // hardware key -> on_input; true = app consumed it
|
||||
void luaAppMessage(const char* channel, const char* sender, const char* text); // -> on_message
|
||||
// Per-app permission bits, stored as a decimal mask in /apps/perms.kv.
|
||||
#define LUA_PERM_SEND 1
|
||||
#define LUA_PERM_READ 2
|
||||
#else
|
||||
inline bool luaAppIsOpen() { return false; }
|
||||
inline void luaAppDismiss() {}
|
||||
inline void luaAppSteer(int, int) {}
|
||||
inline bool luaAppKey(int) { return false; }
|
||||
inline void luaAppMessage(const char*, const char*, const char*) {}
|
||||
#endif
|
||||
+677
-92
File diff suppressed because it is too large.
Load diff
@@ -490,6 +490,7 @@ public:
|
||||
void setPathHashMode(uint8_t mode);
|
||||
void setExperimentalFlags(uint8_t multi_acks, uint8_t client_repeat, uint8_t rx_boosted);
|
||||
void setTelemetryAllow(bool on); // answer mesh telemetry requests (battery+env; location stays separate)
|
||||
void setLocationTelemetryMode(uint8_t mode); // TELEM_MODE_* — position on request (#266)
|
||||
/** Meshcomod CLI on device: `wifi on` / `wifi off`. */
|
||||
bool setWifiRadio(bool on);
|
||||
bool isTcpEnabled() const { return _serial && _serial->isTcpEnabled(); }
|
||||
|
||||
@@ -251,6 +251,36 @@
|
||||
#define CAP_LUA_APPS 1
|
||||
#endif
|
||||
|
||||
// ---- Extended Lua SDK ------------------------------------------------------
|
||||
// The BASE SDK (drawing, timers, key/value store, read-only mesh + radio stats,
|
||||
// http_get) is board-agnostic and ships everywhere CAP_LUA_APPS is on.
|
||||
//
|
||||
// The EXTENDED SDK is the part with real running cost, and it is gated:
|
||||
// wada.sys.battery/gps/sensors live device state
|
||||
// wada.fs.* scoped file read/write/list under /apps
|
||||
// wada.net.http_post + wifi_scan
|
||||
// wada.mesh.send + on_message the only WRITE path into the mesh
|
||||
// wada.ui.input/list + on_key text entry, lists, key events
|
||||
//
|
||||
// Why it is not everywhere: these add per-app RAM (an inbound message queue, a
|
||||
// scan buffer, retained Lua callbacks) and invite apps that hold buffers and do
|
||||
// I/O. The 2 MB-PSRAM Heltec V4 already runs at ~95% internal RAM with Wi-Fi up
|
||||
// — the same headroom problem that gates CAP_WEB_BROWSER and forces
|
||||
// CAP_BUILTIN_LANGS there — so it gets the base SDK only.
|
||||
//
|
||||
// TO GATE A FUTURE LOW-RESOURCE BOARD, pick either:
|
||||
// * define WADA_LOW_RESOURCE_BOARD in the board's block (also gates future extras), or
|
||||
// * define CAP_LUA_SDK_EXT 0 in the board's block (gates only this)
|
||||
// Apps must feature-detect with wada.sys.caps().sdk_ext rather than assume.
|
||||
#ifndef CAP_LUA_SDK_EXT
|
||||
#if !CAP_LUA_APPS || defined(WADA_LOW_RESOURCE_BOARD) || \
|
||||
(defined(HELTEC_LORA_V4_TFT) && !defined(HELTEC_LORA_V4_R8))
|
||||
#define CAP_LUA_SDK_EXT 0 // 2 MB V4 (and anything marked low-resource)
|
||||
#else
|
||||
#define CAP_LUA_SDK_EXT 1 // 8 MB boards incl. the V4-R8
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Compile the translations into the image (i18n_builtin.h, generated from
|
||||
// deploy/apps/lang/*.lang) instead of relying on downloading a .lang file.
|
||||
// ON for boards where the Lua Store is not dependable — the V4 runs at ~95%
|
||||
|
||||
Reference in new issue
Block a user