Compare commits
No files matched your search
+10
@@ -11,6 +11,10 @@
|
||||
.vscode/extensions.json
|
||||
out/*
|
||||
!out/README.md
|
||||
|
||||
# host Lua harness: the compiled binary, not the sources (scripts/lua-harness)
|
||||
scripts/lua-harness/luah
|
||||
scripts/lua-harness/*.o
|
||||
.DS_Store
|
||||
.idea
|
||||
cmake-*
|
||||
@@ -36,3 +40,9 @@ CLAUDE.md
|
||||
|
||||
# Local reconstruction notes (scratch, never publish)
|
||||
/.notes/
|
||||
|
||||
# Tanmatsu sideload tooling: fetched/dumped per developer, never committed.
|
||||
# appfs-src is badge.team's repo (fetch-appfs.sh); dev_appfs.bin is a dump of YOUR
|
||||
# device's AppFS partition and is both large and device-specific.
|
||||
tanmatsu/tools/appfs-src/
|
||||
tanmatsu/tools/dev_appfs.bin
|
||||
@@ -0,0 +1,156 @@
|
||||
# Testing Lua audio playback
|
||||
|
||||
This guide tests issue #315 with a real WAV or MP3. The examples use the
|
||||
T-Deck environment and the provided `scripts/audio_test.lua` app.
|
||||
|
||||
## Build and flash
|
||||
|
||||
Build and upload the branch as usual:
|
||||
|
||||
```sh
|
||||
pio run -e LilyGo_TDeck_companion_radio_touch
|
||||
pio run -e LilyGo_TDeck_companion_radio_touch -t upload
|
||||
```
|
||||
|
||||
Keep the serial monitor available for `[LUAAPP]` or storage errors:
|
||||
|
||||
```sh
|
||||
pio device monitor -b 115200
|
||||
```
|
||||
|
||||
## Quick MP3 test from SD
|
||||
|
||||
1. Use a FAT32 SD card and create these paths on it:
|
||||
|
||||
```text
|
||||
/Music/test.mp3
|
||||
/meshcomod/apps/audio_test.lua
|
||||
```
|
||||
|
||||
2. Copy `scripts/audio_test.lua` from this repository to the second path. Rename
|
||||
your MP3 to `test.mp3`, or edit `SD_PATH` near the top of the Lua file.
|
||||
3. Insert the card before boot, then reboot the T-Deck.
|
||||
4. Enable **Settings > Sound** and set volume above zero.
|
||||
5. Open **Apps > audio_test**. The screen should report
|
||||
`audio=true`, `wav=true`, and `mp3=true`.
|
||||
6. Press **Play**. Expected status:
|
||||
|
||||
```text
|
||||
State: playing
|
||||
Source: sd Format: mp3
|
||||
```
|
||||
|
||||
7. Exercise **Pause**, **Resume**, and **Stop**. Pressing **Play** while already
|
||||
playing must restart/replace the current track without overlapping it.
|
||||
8. Start playback and leave the app. Audio must stop immediately when the app
|
||||
closes.
|
||||
|
||||
The direct SD API used by the test is:
|
||||
|
||||
```lua
|
||||
local ok, err = wada.audio.play("sd:/Music/test.mp3")
|
||||
```
|
||||
|
||||
`sd:` paths are absolute from the physical card root. Empty segments,
|
||||
backslashes, trailing slashes, `.` and `..` are rejected.
|
||||
|
||||
## Test app-local/internal storage
|
||||
|
||||
A plain filename is resolved inside the current app's private directory. For
|
||||
`audio_test.lua`, the logical files are:
|
||||
|
||||
```text
|
||||
/apps/audio_test.lua
|
||||
/apps/audio_test.d/test.mp3
|
||||
```
|
||||
|
||||
On T-Deck and Pager, internal app storage is SPIFFS. The least destructive way
|
||||
to populate it is the repository's serial uploader:
|
||||
|
||||
1. Power off, remove the SD card, and reboot. This forces T-Deck app storage to
|
||||
the internal SPIFFS backend.
|
||||
2. Make a short internal-storage fixture. T-Deck has a 3.375 MB SPIFFS partition
|
||||
shared with settings and history, so use the full song for SD testing and a
|
||||
small clip here:
|
||||
|
||||
```sh
|
||||
ffmpeg -y -i "/path/to/your/file.mp3" \
|
||||
-t 10 -ac 1 -ar 22050 -b:a 48k \
|
||||
/tmp/wadamesh-audio-test.mp3
|
||||
```
|
||||
|
||||
3. Find the serial port with `pio device list`.
|
||||
4. Install `pyserial` once if needed:
|
||||
|
||||
```sh
|
||||
python3 -m pip install pyserial
|
||||
```
|
||||
|
||||
5. Upload the app and short MP3. Replace the port as needed:
|
||||
|
||||
```sh
|
||||
python3 scripts/sideload_app.py \
|
||||
--port /dev/cu.usbmodem101 \
|
||||
--remote /apps/audio_test.lua \
|
||||
scripts/audio_test.lua
|
||||
|
||||
python3 scripts/sideload_app.py \
|
||||
--port /dev/cu.usbmodem101 \
|
||||
--remote /apps/audio_test.d/test.mp3 \
|
||||
--reboot \
|
||||
/tmp/wadamesh-audio-test.mp3
|
||||
```
|
||||
|
||||
6. Open **Apps > audio_test** after reboot. With no card inserted, the app
|
||||
selects `test.mp3`; press **Play**.
|
||||
7. Expected status is `Source: app Format: mp3`.
|
||||
|
||||
The app-local API is simply:
|
||||
|
||||
```lua
|
||||
local ok, err = wada.audio.play("test.mp3")
|
||||
```
|
||||
|
||||
Tanmatsu uses internal FFat and T-Display P4 uses internal LittleFS. The same
|
||||
serial destination `/apps/audio_test.d/test.mp3` follows the active internal
|
||||
backend, so no Lua code changes are needed. Direct `sd:` playback is shown only
|
||||
when `wada.sys.caps().audio_sd` is true.
|
||||
|
||||
Do not use `uploadfs` on a device with data you need to keep: a filesystem-image
|
||||
upload replaces the internal storage partition, including settings and history.
|
||||
|
||||
## Error and lifecycle checks
|
||||
|
||||
- Disable Sound, then press **Play**: expect `Command: muted`.
|
||||
- Rename the file or remove the card: expect `not found` or `no sd`.
|
||||
- Try a non-WAV/MP3 extension: expect `unsupported format`.
|
||||
- Remove the card during SD playback only after the baseline test. Playback must
|
||||
enter `error` or stop without rebooting; reinserting the card should allow a
|
||||
later play after the normal SD recovery delay.
|
||||
- Receive a mesh notification during playback: it must not start a second audio
|
||||
stream over the media track.
|
||||
- Repeatedly open/play/close the app: each close must release the worker, codec,
|
||||
amplifier, file handle, and storage lease.
|
||||
|
||||
## Host decoder regression test
|
||||
|
||||
The standalone test validates the pinned decoder and the same rolling 16 KB
|
||||
input algorithm used by firmware:
|
||||
|
||||
```sh
|
||||
c++ -std=c++17 -O2 test/test_minimp3_decoder.cpp -o /tmp/test_minimp3_decoder
|
||||
/tmp/test_minimp3_decoder /path/to/your/file.mp3
|
||||
```
|
||||
|
||||
A successful run prints decoded frames, samples per channel, sample rate,
|
||||
channels, and bitrate changes. The implementation has also been checked with
|
||||
CBR, VBR, leading ID3v2, trailing ID3v1, and trailing APEv2 metadata.
|
||||
|
||||
## Supported formats
|
||||
|
||||
- WAV: PCM, 16-bit, mono or stereo, 8-48 kHz.
|
||||
- MP3: MPEG Layer III, mono or stereo, 8-48 kHz, CBR or VBR, with common ID3 and
|
||||
APEv2 metadata.
|
||||
|
||||
Stereo is downmixed to the device's mono speaker path. `status()` reports
|
||||
`stopped`, `playing`, `paused`, `ended`, or `error`.
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
# Console mode
|
||||
|
||||
A boot mode where the device runs with **no LVGL**: a text console driven by
|
||||
typed commands, with core mesh functionality and Lua apps. For boards that
|
||||
struggle under the UI, and for people who would rather type than tap.
|
||||
|
||||
Regular mode is unaffected and you can go back at any time.
|
||||
|
||||
---
|
||||
|
||||
## Short answer
|
||||
|
||||
**Possible: yes, and considerably more of it already exists than you would
|
||||
expect.** Four of the five pieces are built and shipping today.
|
||||
|
||||
**A lot of work: no, not for a usable console.** The work is not the console
|
||||
itself, it is everything that currently assumes the UI is there. Realistic
|
||||
shape: a useful console in a few days of work, a polished one that runs Lua
|
||||
apps in a couple of weeks.
|
||||
|
||||
**Saves resources: yes, but not the resource you might think.** See
|
||||
[What it actually saves](#what-it-actually-saves) below, because this changes
|
||||
how the feature should be sold and to whom.
|
||||
|
||||
---
|
||||
|
||||
## What already exists
|
||||
|
||||
This is the reason the estimate is not larger.
|
||||
|
||||
| Piece | Where | State |
|
||||
|---|---|---|
|
||||
| **Command surface** | `CommonCLI::handleCommand` (MeshCore core) | Done. The full command set the node already answers over serial and over the mesh. |
|
||||
| **Run a command locally, capture its output** | `MyMesh::runLocalCli()` + `MyMesh::setTerminalSink()` | Done. Feed it a line, get reply lines back through a callback. |
|
||||
| **Text rendering with no LVGL** | `DisplayDriver` (core): `startFrame`, `setCursor`, `print`, `printWordWrap`, `getTextWidth`, `fillRect`, `drawTextEllipsized`, `endFrame`, UTF-8 to blocks | Done, and linked into the touch build already. |
|
||||
| **A mode where LVGL does not drive the panel** | Remote mode (`s_remote_mode` in `UITask.cpp`) | Done and shipping. It paints the physical screen with `display.drawTextCentered(...)` directly while LVGL renders elsewhere. This is the exact precedent. |
|
||||
| **An on-device terminal** | `homeTerminalCb` / `s_term_log_box` in `UITask.cpp` | Done, but built out of LVGL widgets. The *behaviour* is the model; the widgets are what console mode replaces. |
|
||||
|
||||
So the console does not need a new command language, a new renderer, or a new
|
||||
way of talking to the mesh. It needs a different **front end** over machinery
|
||||
that is already there and already tested.
|
||||
|
||||
---
|
||||
|
||||
## What it actually saves
|
||||
|
||||
Worth being precise, because the obvious assumption is wrong in one direction
|
||||
and right in another.
|
||||
|
||||
### PSRAM: a real saving, and it is the object tree, not the draw buffer
|
||||
|
||||
Everything LVGL allocates is PSRAM: `include/lv_conf.h` sets `LV_MEM_CUSTOM 1`
|
||||
with `LV_MEM_CUSTOM_ALLOC lvglPsramAlloc`, so every object, style and font cache
|
||||
entry is a PSRAM allocation, and the draw buffer is `MALLOC_CAP_SPIRAM` too.
|
||||
|
||||
**The draw buffer is not the prize.** `LV_DRAW_BUF_LINES` is 24, so it is
|
||||
37.5 KB on the 800 px panels and **11.2 KB on the V4**. Worth having back, not
|
||||
worth building a mode for.
|
||||
|
||||
The saving is the **object tree**: every screen, tab, list row, label, style and
|
||||
cached glyph, all of it live for the life of the session. That is the number
|
||||
Phase 0 exists to measure, because it is the one nobody here can quote from
|
||||
memory.
|
||||
|
||||
For scale on the V4, its PSRAM is also carrying up to four 128 KB map tiles, so
|
||||
whatever LVGL is holding sits alongside that. Console mode frees the LVGL half
|
||||
outright.
|
||||
|
||||
**This matters most exactly where you said it hurts.** The Heltec V4 has 2 MB of
|
||||
PSRAM, and PSRAM pressure there is a recurring source of real bugs: map tiles
|
||||
fragmenting the heap, the tile pool needing a cull and a cap, the extended Lua
|
||||
SDK gated off the board entirely for want of memory. Console mode gives that
|
||||
board its PSRAM back.
|
||||
|
||||
### Internal DRAM: a smaller saving than it looks
|
||||
|
||||
The scarce resource on these boards is internal DRAM, and LVGL is largely not
|
||||
in it. What *is* in it is UITask's static data: the contact cache, message
|
||||
rings, layout tables. Most of that is data the console still needs to do the
|
||||
same job, so it does not simply disappear.
|
||||
|
||||
Two caveats worth keeping honest:
|
||||
|
||||
- There is a fallback at boot: if the PSRAM draw-buffer allocation fails,
|
||||
`malloc()` puts it in internal DRAM instead. On a board already under
|
||||
pressure that is exactly when it happens, so for the worst-off devices the
|
||||
DRAM saving is real after all.
|
||||
- A meaningful DRAM number needs measuring, not guessing. See Phase 0.
|
||||
|
||||
### CPU: a real saving, and probably the one users feel
|
||||
|
||||
`lv_timer_handler()` runs on every pass of the main loop, alongside input-device
|
||||
polling and any animation or scroll in flight, and a redraw blits through SPI.
|
||||
A console redraws only when a line is added.
|
||||
|
||||
This is also the mechanism behind several bugs we have already fixed: the loop
|
||||
being stalled by the UI is what starved the GPS UART ring, and what made SPIFFS
|
||||
garbage collection visible as a freeze. Less time in the UI is less exposure to
|
||||
that whole class of problem.
|
||||
|
||||
**How to describe the feature honestly:** it buys back PSRAM and CPU. On the
|
||||
V4 that is the difference between comfortable and not. It is not a fix for
|
||||
internal-DRAM exhaustion on its own.
|
||||
|
||||
---
|
||||
|
||||
## The hard parts
|
||||
|
||||
Not the console. These three.
|
||||
|
||||
### 1. Everything that assumes the UI exists
|
||||
|
||||
`src/main.cpp` calls `ui_task.showAlert(...)` in roughly twenty places to report
|
||||
Wi-Fi and Bluetooth outcomes, and `ui_task.loop()` unconditionally. UITask is
|
||||
also the owner of things the console still wants: the storage root, the Lua app
|
||||
host, the update check.
|
||||
|
||||
The wrong fix is `#if` sprinkled through main. The right one is that UITask
|
||||
keeps its role as the front end and gains a **headless personality**: the same
|
||||
entry points, LVGL never initialised, `showAlert` becoming a console line.
|
||||
|
||||
### 2. Lua apps: the `wada.ui` translator
|
||||
|
||||
Decided: `wada.ui` gets a **console backend** behind the same names, so an app
|
||||
keeps working without knowing which mode it is in.
|
||||
|
||||
This turns out to be far more achievable than "no UI" suggests, because console
|
||||
mode is **not a text-only terminal**. It still owns a real pixel panel. The
|
||||
thing being removed is LVGL's object tree, not the ability to draw.
|
||||
|
||||
The enabler is a raw framebuffer blit that already exists outside LVGL:
|
||||
|
||||
```
|
||||
ST7789LCDDisplay::writePixelsRGB565(x, y, w, h, const uint16_t* pixels)
|
||||
```
|
||||
|
||||
Present today on `ST7789LCDDisplay` (V4 TFT, T-Deck), `TanmatsuDisplay`,
|
||||
`RM69A10Display` and `HI8561Display`. Missing only on `LGFXDisplay` (V4-R8) and
|
||||
`ST7796LCDDisplay` (the pagers), and LovyanGFX has `pushImage` underneath, so
|
||||
those are a wrapper method rather than new work.
|
||||
|
||||
That changes the answer on the two APIs that looked impossible:
|
||||
|
||||
| API | Console backend | Why it works |
|
||||
|---|---|---|
|
||||
| `label` | Text at a cursor. | `print` / `printWordWrap` / `drawTextEllipsized`. |
|
||||
| `button` | A highlighted line with a selection index; activated by key or tap. | Text plus `fillRect` for the highlight. |
|
||||
| `list` | Numbered lines, one selected. | Same. |
|
||||
| `input` | A prompt, using the console's own keypad. | Phase 1 already builds it. |
|
||||
| `text_w` / `text_lines` | `getTextWidth`, honestly measured. | Already the right shape. |
|
||||
| `chart` | Drawn directly. | It is bars and lines; `fillRect` does it. |
|
||||
| **`canvas`** | **Works.** The app already gets an RGB565 buffer and draws into it; console mode blits that buffer instead of handing it to LVGL. | `writePixelsRGB565`. |
|
||||
| **`map`** | **Works.** Tiles are already decoded to RGB565 buffers (`MapTile.rgb565`); the projection and cache are ours and have no LVGL in them. | Same blit, plus markers as `fillRect`. |
|
||||
| Everything non-UI | Unchanged. | `mesh`, `fs`, `net`, `crypto`, `geo`, `sys`, `timer`, `store` never touched LVGL. |
|
||||
|
||||
**What is genuinely lost is interaction, not drawing.** LVGL supplies scrolling
|
||||
containers, a focus group, flex layout and event bubbling. The console backend
|
||||
has to provide its own much simpler version: a line cursor, a selection index,
|
||||
a scroll offset. That is the real work in this phase, and it is bounded.
|
||||
|
||||
So the honest expectation is that **most apps work**, including the graphical
|
||||
ones, and a few that lean on LVGL behaviour we do not reimplement will not.
|
||||
Apps already feature-detect through `wada.sys.caps()`, so anything that cannot
|
||||
be supported is reported rather than broken, and `caps().console` lets an app
|
||||
adapt its layout deliberately.
|
||||
|
||||
### 3. Getting back out
|
||||
|
||||
A boot flag that lands you in a console with no way back is a device that feels
|
||||
bricked, and it will happen to somebody. This needs to be designed, not bolted
|
||||
on. See [Safety](#safety).
|
||||
|
||||
---
|
||||
|
||||
## Plan
|
||||
|
||||
Each phase is shippable and useful on its own. Stop after any of them and
|
||||
nothing is half-built.
|
||||
|
||||
### Phase 0: measure first
|
||||
|
||||
Before building anything, get the numbers the feature is being justified by.
|
||||
|
||||
- Free internal DRAM and free PSRAM at the home screen, on a V4 and a T-Deck.
|
||||
- The same with `lv_timer_handler()` stubbed out and nothing built.
|
||||
- Loop iterations per second, both ways.
|
||||
|
||||
Cheap, and it either confirms the case or changes what to build. If the DRAM
|
||||
saving turns out to be negligible on the V4 and the PSRAM saving large, that is
|
||||
worth knowing before the docs promise anything.
|
||||
|
||||
**Output:** real numbers in this file.
|
||||
|
||||
### Phase 1: the console front end
|
||||
|
||||
A `ConsoleUI` that owns the panel through `DisplayDriver`, with no LVGL.
|
||||
|
||||
- Scrollback ring in PSRAM, N lines, prune oldest.
|
||||
- Prompt line, cursor, word wrap via `printWordWrap`.
|
||||
- Input from the hardware keyboard where there is one.
|
||||
- **An on-screen keypad on touch boards.** Decided: the V4 is the board that
|
||||
most needs its resources back, so console mode has to be usable there without
|
||||
a keyboard. The existing on-screen keyboard is LVGL and cannot be reused, so
|
||||
the console draws its own: `fillRect` + `drawRect` + `print` for the keys, and
|
||||
touch read straight from the board driver (`heltecV4CapTouchCheck()` and its
|
||||
equivalents), which is already independent of LVGL. `lvglTouchRead` is only an
|
||||
adapter feeding LVGL, so nothing needs to be untangled first.
|
||||
- Commands go straight to `the_mesh.runLocalCli()`; output arrives through
|
||||
`setTerminalSink`. This part is close to free.
|
||||
- One touch affordance, as you said: a back/exit target.
|
||||
|
||||
At the end of this phase the device is usable over the mesh from a console.
|
||||
|
||||
### Phase 2: boot into it, and back out
|
||||
|
||||
- A pref, `touchPrefsGetBootMode()`, read early in `setup()`.
|
||||
- In console mode: never call `lv_init()`, never allocate the draw buffer, never
|
||||
build the UI. This is where the saving comes from, so it has to be a genuine
|
||||
skip and not a hidden LVGL instance.
|
||||
- `ui_task.showAlert()` and friends route to the console.
|
||||
- Commands to switch: `ui` reboots into the graphical mode, `console` reboots
|
||||
into this one. Also a Settings toggle in normal mode.
|
||||
|
||||
### Phase 3: the things the CLI does not cover
|
||||
|
||||
`CommonCLI` is a node CLI, not a messenger. Console mode needs conversational
|
||||
commands, some of which the existing on-device terminal already has (`to <name>`
|
||||
sets a recipient):
|
||||
|
||||
- `msg`, `to`, `reply`, unread counts, reading a thread's scrollback
|
||||
- `contacts`, `chans`, `discover` with the results table
|
||||
- `apps`, `run <id>`
|
||||
|
||||
Most of this is a thin layer over calls that already exist for the UI.
|
||||
|
||||
### Phase 4: Lua apps in the console
|
||||
|
||||
- A `WADA_UI_CONSOLE` backend behind the same `wada.ui` names, per the table
|
||||
above.
|
||||
- `caps()` gains `console`, `canvas`, `map` so apps can adapt, and the docs say
|
||||
which apps work.
|
||||
- Store apps audited; those that cannot work say so on launch rather than
|
||||
failing oddly.
|
||||
|
||||
### Phase 5: polish
|
||||
|
||||
- Command history and tab completion.
|
||||
- Colour, where the panel has it.
|
||||
- A `help` that is actually good, since there is no discoverable UI.
|
||||
- Docs page, and a section in the user guide.
|
||||
|
||||
---
|
||||
|
||||
## Safety
|
||||
|
||||
The failure that matters is a device that boots to a console the owner cannot
|
||||
leave. Three independent ways out, because one is not enough:
|
||||
|
||||
1. **A command.** `ui` then reboot.
|
||||
2. **A key held at boot.** Any boot with the key down forces graphical mode
|
||||
regardless of the pref, the same shape as the existing recovery paths.
|
||||
3. **The web flasher and the serial CLI** can always clear the pref.
|
||||
|
||||
And the pref should be stored so that a corrupt or unreadable value means
|
||||
*graphical*, never console. Fail toward the mode everyone can use.
|
||||
|
||||
---
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Containment |
|
||||
|---|---|
|
||||
| Console mode rots because nobody builds it | It compiles into every build from Phase 1, and SDK Test runs in it. A broken console fails the build, not a user. |
|
||||
| Two front ends drift apart | The console consumes the same `runLocalCli` and the same MyMesh accessors the UI does. Do not let it grow a private copy of anything. |
|
||||
| Touch-only boards get a console they cannot type into | Decide in Phase 1. Requiring a keyboard is a legitimate answer. |
|
||||
| It quietly degrades normal mode | Everything new is behind the boot pref, and the graphical path keeps its current code path unchanged. Phase 0's numbers are the regression check. |
|
||||
| Lua apps half-work and look broken | `caps()` first, refuse to launch second, document third. Less likely now that canvas and map can actually be drawn. |
|
||||
| Two display drivers lack a raw blit | `LGFXDisplay` and `ST7796LCDDisplay` need a `writePixelsRGB565`; LovyanGFX's `pushImage` does the work. Until then `caps().canvas` is false on those boards and apps adapt. |
|
||||
|
||||
---
|
||||
|
||||
## Effort
|
||||
|
||||
Honest, assuming no surprises:
|
||||
|
||||
| Phase | Effort |
|
||||
|---|---|
|
||||
| 0 measure | Half a day |
|
||||
| 1 console front end (incl. the drawn keypad) | 3 to 4 days |
|
||||
| 2 boot mode and back out | 1 to 2 days |
|
||||
| 3 messaging commands | 2 to 3 days |
|
||||
| 4 `wada.ui` console backend | 4 to 6 days |
|
||||
| 5 polish and docs | 2 days |
|
||||
|
||||
**A console you can actually use on the mesh: Phases 0 to 3, about a week.**
|
||||
Phase 4 is the one that can grow, because it is the one with a design question
|
||||
in it rather than just code.
|
||||
|
||||
---
|
||||
|
||||
## Open decisions
|
||||
|
||||
Worth settling before Phase 1 rather than during it.
|
||||
|
||||
1. ~~Keyboard required?~~ **Decided: no.** Touch boards get a drawn on-screen
|
||||
keypad, because the V4 is exactly the board this feature is for.
|
||||
2. **Do Lua apps matter in v1?** Phases 0 to 3 are worth shipping without them.
|
||||
The translator is now a known quantity rather than a question mark, so it can
|
||||
follow safely.
|
||||
3. **Does this replace the Terminal app** in graphical mode eventually, or do
|
||||
both stay?
|
||||
|
||||
---
|
||||
|
||||
## Audit: every UI surface, and its console equivalent
|
||||
|
||||
Done by enumerating the real thing rather than from memory: the tab list, the
|
||||
app-drawer action enum (`APPACT_*`), the 19 settings categories, and the node
|
||||
CLI's own command table in `CommonCLI.cpp`.
|
||||
|
||||
**The single most useful finding: the node CLI already covers far more than
|
||||
expected.** `advert`, `board`, `clock`, `clock sync`, `erase`, `get`/`set`,
|
||||
`gps` (on/off/sync/setloc/advert), `log`, `neighbors`, `ota url`, `password`,
|
||||
`poweroff`, `powersaving`, `reboot`, `region`, `sensor list/get/set`,
|
||||
`shutdown`, `start ota`, `tempradio`, `time`, `ver`. All of that arrives free,
|
||||
which is why the console needed far fewer new commands than the UI has screens.
|
||||
|
||||
### Tabs and apps
|
||||
|
||||
| UI surface | Console | State |
|
||||
|---|---|---|
|
||||
| Chats: read a thread | `chat <name>` | **done** |
|
||||
| Chats: send | `to <name>` + `msg <text>` | **done** |
|
||||
| Chats: unread badges | `unread`, `read <name>` | **done**, and the monitor never clears them |
|
||||
| Live incoming messages | the monitor, `monitor on\|off` | **done** |
|
||||
| Contacts list | `contacts` | **done** |
|
||||
| Contacts: favourite / block / delete | none | gap |
|
||||
| Channels list | `chans` | **done** |
|
||||
| Discover | `discover`, `discovered` | **done**, with both link directions |
|
||||
| Advertise | CLI `advert` | free |
|
||||
| Signal / traffic | `stat` | **done** |
|
||||
| Map | none | needs the `wada.map` blit path; not console-shaped yet |
|
||||
| Terminal | this *is* the console | n/a |
|
||||
| Files | none | gap |
|
||||
| Spectrum | none | gap, and heavy |
|
||||
| Store / Lua apps | none | Phase 4 |
|
||||
| Web reader | none | gap |
|
||||
| VNC / Remote | none | deliberate: another front end, not a console feature |
|
||||
| Power menu | CLI `poweroff` / `reboot` | free |
|
||||
| Mentions | `unread` shows counts | partial |
|
||||
|
||||
### Settings, all 19 categories
|
||||
|
||||
| Category | Console | State |
|
||||
|---|---|---|
|
||||
| Radio & Mesh | CLI `get`/`set`, `region`, `tempradio` | free |
|
||||
| GPS | CLI `gps *` | free |
|
||||
| Clock & time | CLI `clock`, `time` | free |
|
||||
| Sensors | CLI `sensor *` | free |
|
||||
| About | CLI `ver`, `board`; `mem` | free |
|
||||
| Battery | `batt` | **done** |
|
||||
| Wi-Fi | `wifi` (state only) | partial |
|
||||
| Bluetooth | none | gap |
|
||||
| Auto-add | none | gap |
|
||||
| Display / Keyboard / Sound / Lock screen | none | not meaningful without the UI |
|
||||
| Quick replies | none | gap |
|
||||
| Backups | none | gap |
|
||||
| Language | none | gap |
|
||||
| MQTT bridge | none | gap |
|
||||
| App permissions | none | with Phase 4 |
|
||||
| General | `console`/`ui` toggle | partial |
|
||||
|
||||
### What the gaps have in common
|
||||
|
||||
Almost everything still missing is either **Phase 4** (anything Lua), **not
|
||||
console-shaped** (Display, Keyboard, Lock screen: settings for a UI that is not
|
||||
running), or **a list-and-act flow** (files, backups, quick replies, contact
|
||||
actions) that all wants the same small pattern: number the list, act on the
|
||||
number. That pattern is worth building once rather than six times.
|
||||
|
||||
The two genuinely interesting gaps are **Files** and **contact actions**
|
||||
(favourite, block, delete), because both are things people do reach for.
|
||||
+22
-3
@@ -13,12 +13,20 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
|
||||
| Tanmatsu | ESP32-P4 + ESP32-C6, SX1262 | 4" 800x480, 69-key keyboard (no touch) | Tanmatsu app store on the device (runs under the badge.team launcher, not web-flashable) | Store tracks the test channel | Fully supported; LoRa + Wi-Fi + Bluetooth simultaneously, standalone and companion in one |
|
||||
| Elecrow ThinkNode M9 | ESP32-S3, LR1110 | 2.4" 240x320 (no touch), I2C QWERTY + d-pad | Web flasher | Beta (new in beta_38) | Hardware-complete community port by ded (#138): GPS, microSD, buzzer, lock screen, d-pad navigation |
|
||||
| RAK WisMesh Tap V2 (RAK3312) | ESP32-S3, SX1262 | Touch display (LovyanGFX, 30+ fps) | Web flasher | Beta (new in beta_38) | Early community port by Ethac.chen (#136); core mesh, chat and map working |
|
||||
| LilyGo T-Lora Pager | ESP32-S3, LR1121 or SX1262 | 2.33" IPS LCD, 480x222 (no touch), QWERTY + rotary encoder | Web flasher (pick the build matching your radio) | Beta (new in beta_45) | Fully supported; keyboard-first navigation, microSD, map tile packs |
|
||||
| Heltec V4-R8 + Expansion Kit V2 | ESP32-S3 (8 MB octal PSRAM), SX1262 | 2.4" touch (CHSC6x) | Web flasher | Beta (new in beta_45) | As the V4 plus microSD and the Expansion Kit sensors; buzzer supported |
|
||||
| LilyGo T-Display P4 | ESP32-P4 + ESP32-C6, SX1262 | AMOLED or TFT-LCD, touch | Web flasher (AMOLED) or the .bin for the LCD SKU | Beta (new in beta_45) | Two screen SKUs; fuel-gauge battery reporting, full TX power |
|
||||
| Attaky Mesh Series | ESP32-S3, SX1262 | Touch display | Web flasher | Beta (new in beta_47) | Community port by attakygit (#158/#169); detachable keyboard supported |
|
||||
|
||||
## Feature notes per board
|
||||
|
||||
- **T-Deck**: the everything device: touch, physical keyboard, trackball cursor
|
||||
or d-pad navigation, microSD (deep 5000-message chat history, map tile packs,
|
||||
data storage), GPS on the Plus, notification sounds through the I2S speaker.
|
||||
Tap Sym or Alt for one symbol, or double-tap either to lock the symbol layer;
|
||||
this needs [LilyGO keyboard-controller firmware with raw matrix mode](https://github.com/Xinyuan-LilyGO/T-Deck/tree/master/examples/Keyboard_ESP32C3)
|
||||
(June 2025 or newer). Older controller firmware keeps normal typing and
|
||||
reports the unavailable latch mode on Serial.
|
||||
- **Heltec V4 + TFT**: touch UI with the on-screen keyboard; the optional
|
||||
Expansion Kit adds environment sensors (home-screen chart) and a piezo
|
||||
buzzer. V4.3 boards get the switchable high-gain receive LNA toggle.
|
||||
@@ -26,9 +34,21 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
|
||||
keys mapped to tabs, ALT accent picker, UI scaling (Normal/Large/Huge),
|
||||
microSD for all persistent data. Ships through the Tanmatsu launcher store,
|
||||
updates arrive as store updates.
|
||||
- **T-Lora Pager**: no touchscreen at all — the QWERTY keyboard and the rotary
|
||||
encoder drive everything (Alt+turn free-scrolls a page; see
|
||||
[TLORA_PAGER_SHORTCUTS.md](TLORA_PAGER_SHORTCUTS.md) for the full key map).
|
||||
Tap Fn/Alt for one symbol or double-tap it to lock the symbol layer; physical
|
||||
hold combinations keep their existing behavior.
|
||||
GPS, microSD, keyboard backlight, lock screen and notification sound through
|
||||
the onboard codec and amp all work. Two builds, one per radio: LR1121 and
|
||||
SX1262 — flashing the wrong one leaves you with no radio, so check the label
|
||||
on your unit. A card the Pager detects but cannot read shows up in File
|
||||
Manager greyed, and tapping it retries the mount; formatting is deliberately
|
||||
left to a computer (FAT32) rather than done on-device.
|
||||
- **ThinkNode M9**: keyboard plus d-pad navigation (no touch), same feature set
|
||||
as the other boards where the hardware allows. New in beta_38; expect rough
|
||||
edges and report them.
|
||||
as the other boards where the hardware allows. Every key and mode is covered
|
||||
in the [keyboard & d-pad guide](THINKNODE_M9_SHORTCUTS.md). New in beta_38;
|
||||
report anything that feels off.
|
||||
- **RAK WisMesh Tap V2**: newest port, touch-driven. The browser-flash path is
|
||||
fresh; if the flasher cannot open the serial port, put the board in download
|
||||
mode manually and retry, and please report it.
|
||||
@@ -40,7 +60,6 @@ Open hardware requests, roughly in demand order. Ports are welcome, see
|
||||
PRs.
|
||||
|
||||
- LilyGo T-Deck Pro Max: [#62](https://github.com/ALLFATHER-BV/wadamesh/issues/62)
|
||||
- LilyGo T-pager: [#60](https://github.com/ALLFATHER-BV/wadamesh/issues/60)
|
||||
- SenseCAP Indicator D1L: [#14](https://github.com/ALLFATHER-BV/wadamesh/issues/14)
|
||||
|
||||
## Channels
|
||||
|
||||
+38
-1
@@ -50,7 +50,8 @@ We do NOT bind LVGL wholesale. We bind a stable, versioned surface we own
|
||||
- `wada.store` — per-app KV: `get/set(key, value)` under an `app.<id>.` prefix
|
||||
in the file-mode prefs. Quota 2 KB/app (respects the SdNvsPrefs blob cap).
|
||||
- `wada.sys` — `millis()`, `board()` (id, screen w/h, caps: keyboard,
|
||||
trackball, gps), `version()`, `toast(msg)`.
|
||||
trackball, gps), `version()`, `toast(msg)`, `tr(s)` (the firmware's own
|
||||
translation table, so an app is not stuck in English).
|
||||
- `wada.timer` — `every(ms)` drives `on_tick` cadence (min 33 ms).
|
||||
|
||||
Sandbox env (no `io`, `os`, `require`, `dofile`, `load` of new chunks;
|
||||
@@ -190,3 +191,39 @@ file loads into PSRAM and overlays TR() (bsearch; file → built-in column →
|
||||
EN). Users can sideload/edit their own .lang files. Built-in table stays as
|
||||
fallback this beta; dropping its columns for the flash win is the follow-up
|
||||
once files are field-proven.
|
||||
|
||||
- 2026-08-22: **wada.sys.gps() widened + wada.sys.compass() + GPS Compass
|
||||
app.** gps() now carries `alt` (m) everywhere and `speed_kmh`/`course` on
|
||||
boards that build their GPS on `src/helpers/WadaNmeaLocationProvider.h`
|
||||
(a Wadamesh-owned copy of the core provider exposing the RMC motion fields
|
||||
it keeps private; M9 first, flag `HAS_GPS_MOTION`), and it returns nil while
|
||||
the user has GPS switched off. compass() is on a new HARDWARE gate
|
||||
`CAP_COMPASS` (the M9's QMC6309, driver `variants/thinknode_m9/M9Compass.*`,
|
||||
exposed in `caps().compass`) and hands out the raw field vector in Gauss —
|
||||
calibration, axis mapping and the heading maths live in the app so they can
|
||||
be adjusted per user without a firmware cut. `deploy/apps/gpscompass/1.0`
|
||||
is that app: rotating rose, live fix readout, bearing/range to a contact,
|
||||
C to calibrate (a 3D sphere fit that also calibrates the accelerometer's
|
||||
zero-g offset from the same sweep), tilt compensation off the M9's QMI8658,
|
||||
GPS-course fallback on every other board. It carries a real WMM2025
|
||||
declination model (`scripts/wmm/`) so the dial and the bearings share a
|
||||
north: the magnetometer measures magnetic north, every bearing computed from
|
||||
coordinates is true, and drawing one against the other put every waypoint
|
||||
~22° out. Headings and bearings are labelled `T` or an amber `M` accordingly.
|
||||
Verified on the host harness in `scripts/lua-harness` (same vendored Lua,
|
||||
same LUA_32BITS numeric model): calibration recovers a simulated hard-iron
|
||||
bias exactly, heading error 0° at six test angles, bearings checked against
|
||||
absolute compass directions, declination within 0.0002° of NOAA, worst tick
|
||||
≈12k of the 100k instruction budget. Hardware validation (axis orientation,
|
||||
real bias magnitude) is in M9_PORT.md. Not seeded into lua_builtin.h on
|
||||
purpose: CAP_BUILTIN_LUA_APPS also hides the Store > Apps tab.
|
||||
|
||||
- 2026-08-22: **manifest `icon` implemented.** The drawer gave every Lua app
|
||||
the same generic glyph while `LUA_APPS.md` had promised manifests an icon
|
||||
field since the plan was written. `icon` is now read from `<id>.json` and
|
||||
mapped to a glyph by NAME (`gps`, `radio`, `chart`, `game`, ...) — names,
|
||||
not codepoints, so a store submission stays reviewable and an app can never
|
||||
ship a missing-glyph box; anything unrecognised falls back to the generic
|
||||
symbol. `deploy/site/sdk.html`'s manifest table was corrected at the same
|
||||
time: it documented `version` / `min_api` / `description` / `boards`, none
|
||||
of which the device has ever parsed.
|
||||
@@ -54,6 +54,13 @@ ed25519 (bundled in lib/ed25519)
|
||||
Orson Peters / orlp — public domain (Creative Commons Zero / zlib)
|
||||
https://github.com/orlp/ed25519
|
||||
|
||||
minimp3 (bundled in lib/minimp3)
|
||||
lieff/minimp3 contributors — CC0 1.0 Universal
|
||||
https://github.com/lieff/minimp3
|
||||
Pinned at ea99364f61c14656440e8d77e9c233ccf3124633. The bundled header has
|
||||
a small opt-in scratch-storage hook so the decoder workspace can live in
|
||||
PSRAM; default upstream behavior is unchanged when that hook is not used.
|
||||
|
||||
AsyncElegantOTA (vendored in arch/esp32/AsyncElegantOTA)
|
||||
Copyright (c) Ayush Sharma — MIT License
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<p align="center"><b>A real touchscreen UI for your mesh radio.</b> · open source · GPL-3.0</p>
|
||||
|
||||
Touch-UI [MeshCore](https://github.com/meshcore-dev/MeshCore) companion-radio
|
||||
firmware for the **LilyGo T-Deck / T-Deck Plus** and **Heltec V4 + TFT**
|
||||
firmware for the **LilyGo T-Deck / T-Deck Plus**, **Heltec V4 + TFT** and eight other boards
|
||||
(ESP32-S3).
|
||||
|
||||
An LVGL touch UI — map, chat, contacts, channels, settings — split out of
|
||||
@@ -22,14 +22,18 @@ per-board status.
|
||||
|
||||
- LilyGo T-Deck / T-Deck Plus — env `LilyGo_TDeck_companion_radio_touch` (stable)
|
||||
- Heltec V4 + TFT + CHSC6x touch — env `heltec_v4_tft_companion_radio_usb_tcp_touch` (stable)
|
||||
- Tanmatsu (ESP32-P4) — built from `tanmatsu/` (ESP-IDF), ships via the Tanmatsu app store
|
||||
- Elecrow ThinkNode M9 — env `ThinkNode_M9_companion_radio_touch` (beta)
|
||||
- Tanmatsu (ESP32-P4) — built from `tanmatsu/` (ESP-IDF), ships via the Tanmatsu app store — [sideload guide](TANMATSU_SIDELOAD.md) for running your own build
|
||||
- Elecrow ThinkNode M9 — env `ThinkNode_M9_companion_radio_touch` (beta) — [keyboard & d-pad guide](THINKNODE_M9_SHORTCUTS.md)
|
||||
- RAK WisMesh Tap V2 (RAK3312) — env `rak_tap_v2_companion_radio_touch` (beta)
|
||||
- LilyGo T-Lora Pager — envs `tlora_pager_lr1121_companion_radio_touch` / `tlora_pager_sx1262_companion_radio_touch` (beta) — [keyboard shortcuts](TLORA_PAGER_SHORTCUTS.md)
|
||||
- Heltec V4-R8 + Expansion Kit V2 — env `heltec_v4_r8_tft_companion_radio_usb_tcp_touch` (beta)
|
||||
- LilyGo T-Display P4 — built from `tdisplay_p4/` (ESP-IDF); AMOLED by default, `WADA_P4_LCD=1` for the TFT-LCD SKU (beta)
|
||||
- Attaky Mesh Series — env `attaky_mesh_series_companion_radio_touch` (beta)
|
||||
|
||||
## Architecture
|
||||
|
||||
This repo holds only the **app**: the `companion_radio` glue, the `ui-touch`
|
||||
LVGL UI, the two boards' glue/variants, and `platformio.ini`. The **MeshCore
|
||||
LVGL UI, each board's glue/variants, and `platformio.ini`. The **MeshCore
|
||||
core is not vendored here** — it's pulled as a library via `lib_deps` from the
|
||||
[`ALLFATHER-BV/meshcomod`](https://github.com/ALLFATHER-BV/meshcomod) monorepo
|
||||
(the same repo as the non-touch firmware), pinned by a lean source-only `core-*`
|
||||
@@ -38,6 +42,10 @@ the transports, …) are dropped from the lib via `-DMC_VENDORED_TOUCH_APP` so t
|
||||
aren't compiled twice. The build is byte-identical to the original in-tree
|
||||
meshcomod firmware.
|
||||
|
||||
Lua apps use the on-device SDK described in [LUA_APPS.md](LUA_APPS.md). For
|
||||
WAV/MP3 playback, including a ready-to-sideload transport test app and exact SD
|
||||
and internal-storage paths, see [AUDIO_PLAYBACK_TESTING.md](AUDIO_PLAYBACK_TESTING.md).
|
||||
|
||||
## Build
|
||||
|
||||
[PlatformIO](https://platformio.org/) pulls the core fork and all libraries
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# Sideloading wadamesh on the Tanmatsu
|
||||
|
||||
For developers building wadamesh from source and putting it on a Tanmatsu.
|
||||
If you just want to run it, install **WadaMesh** from the launcher's app store
|
||||
instead; this document is about running your own build.
|
||||
|
||||
## Understand the model first, or you will brick the launcher
|
||||
|
||||
wadamesh is **not** standalone firmware on this board. The Tanmatsu boots
|
||||
badge.team's launcher, and wadamesh runs as an **AppFS app** underneath it,
|
||||
exactly like the launcher's own apps.
|
||||
|
||||
So you never flash the whole device. You write **one app image plus one metadata
|
||||
sector** into the AppFS partition and leave the bootloader, the partition table
|
||||
and the OTA slots alone.
|
||||
|
||||
> **Never run `idf.py flash` on a Tanmatsu.** It writes the bootloader, partition
|
||||
> table and OTA data, which replaces the launcher OS itself. Recovering means
|
||||
> reflashing the launcher from badge.team. Every command in this guide touches
|
||||
> only the AppFS partition.
|
||||
|
||||
The board has three processors and only one of them is yours: the **ESP32-P4**
|
||||
runs the launcher and your app, an **ESP32-C6** handles Wi-Fi, Bluetooth and LoRa
|
||||
over esp-hosted, and a **CH32** runs the keyboard and power. You flash the P4.
|
||||
|
||||
## One-time setup
|
||||
|
||||
You need ESP-IDF 5.5.1. The build script uses a project-local copy in
|
||||
`tanmatsu/esp-idf` (gitignored, around 3 GB); `make prepare` from `tanmatsu/`
|
||||
fetches it.
|
||||
|
||||
Then two one-off steps:
|
||||
|
||||
```bash
|
||||
tanmatsu/tools/fetch-appfs.sh
|
||||
```
|
||||
|
||||
This clones badge.team's `esp32-component-appfs`, whose `appfs.py` reads and
|
||||
rewrites the AppFS image. It is not vendored into this repo on purpose: it is
|
||||
their code, and the copy in circulation has no licence header.
|
||||
|
||||
```bash
|
||||
tanmatsu/tools/dump-pristine.sh /dev/cu.usbmodemXXXX
|
||||
```
|
||||
|
||||
This dumps **your** device's AppFS partition as the baseline that later deploys
|
||||
diff against. Do it while the launcher's own apps are installed and wadamesh is
|
||||
not, so the baseline is the device as it ships.
|
||||
|
||||
Use your own dump, not someone else's. The deploy writes only the sectors that
|
||||
differ from this file, so if it describes a different device you can overwrite
|
||||
apps you actually have.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd tanmatsu && ./build.sh build
|
||||
```
|
||||
|
||||
Use `build.sh`, not `idf.py` directly: it applies two build-time patches to
|
||||
`managed_components` (esp-hosted Wi-Fi-init tolerance, and an Arduino BLE stub)
|
||||
that the build needs.
|
||||
|
||||
**The build ends with an error and that is expected:**
|
||||
|
||||
```
|
||||
Generated .../application.bin
|
||||
Error: All app partitions are too small
|
||||
```
|
||||
|
||||
The ~2.9 MB app is being size-checked against the 2 MB `ota_0` slot, but it does
|
||||
not live there: it lives in the 8 MB `appfs` partition. Judge success by
|
||||
`Generated ... application.bin` appearing with no `error:` or `undefined
|
||||
reference` above it.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
tanmatsu/tools/tan_flash.sh
|
||||
```
|
||||
|
||||
With no port argument it probes each USB serial device and picks the one that
|
||||
answers as an ESP32-P4. The Tanmatsu exposes **two** ports, the P4 and the C6,
|
||||
and which name each gets changes between replugs, so detecting by chip beats
|
||||
guessing. Pass a port explicitly if you prefer.
|
||||
|
||||
What it does, and why in this order:
|
||||
|
||||
1. Builds the AppFS image from your pristine baseline plus the new
|
||||
`application.bin`, and works out the minimum set of 64 KB sectors that changed.
|
||||
2. Writes the **app data first**, with no reset afterwards.
|
||||
3. Writes the **metadata sector last**, then resets. That sector is what makes the
|
||||
app visible to the launcher, so writing it last means an interrupted flash
|
||||
leaves the previous app intact instead of a half-written one marked complete.
|
||||
4. Reads the metadata back and compares it. The commit write is the one that can
|
||||
silently drop, and without this check the launcher would quietly keep showing
|
||||
the old build.
|
||||
|
||||
Expect `metadata verified — installed`. Anything else means re-run it.
|
||||
|
||||
The version number increments automatically on each deploy. It has to: the
|
||||
launcher keys updates on name plus version, so republishing the same number
|
||||
leaves you on the old binary with no error anywhere.
|
||||
|
||||
## Verify
|
||||
|
||||
Launch **WadaMesh** from the launcher menu. An empty serial log immediately after
|
||||
flashing is normal, because the launcher is a silent GUI OS on USB-CDC; the boot
|
||||
log appears once the app itself starts.
|
||||
|
||||
```bash
|
||||
python3 tanmatsu/tools/sermon.py /dev/cu.usbmodemXXXX /tmp/wada.log
|
||||
```
|
||||
|
||||
`sermon.py` is a non-resetting monitor that survives the USB re-enumeration when
|
||||
an app launches. Prefer it to a monitor that asserts DTR/RTS: **resetting the P4
|
||||
also desynchronises the C6**, which has no reset line of its own, and the launcher
|
||||
then hangs on "Initializing radio" until you fully power-cycle the board.
|
||||
|
||||
## When it goes wrong
|
||||
|
||||
**No serial ports at all.** The C6 has wedged. Fully power-cycle the board, off
|
||||
then on. A reset is not enough.
|
||||
|
||||
**"No ESP32-P4 found"** with ports present. Something else is holding the port,
|
||||
often a still-running monitor. Close it and retry.
|
||||
|
||||
**"metadata DIFFERS".** The commit write did not land. Re-run; nothing is broken,
|
||||
the device still has the previous app.
|
||||
|
||||
**A crash to decode.**
|
||||
|
||||
```bash
|
||||
riscv32-esp-elf-addr2line -e tanmatsu/build/tanmatsu/application.elf -fpC <MEPC> <RA> <stack RAs>
|
||||
```
|
||||
|
||||
## Before you ship anything
|
||||
|
||||
The Tanmatsu shares one `src/` with the ESP32-S3 boards, so a change here can
|
||||
break them without you noticing. Build at least:
|
||||
|
||||
```bash
|
||||
pio run -e LilyGo_TDeck_companion_radio_touch
|
||||
pio run -e heltec_v4_tft_companion_radio_usb_tcp_touch
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- `TANMATSU_PORT.md` — the port's status and outstanding work
|
||||
- [wadamesh.com/sdk.html](https://wadamesh.com/sdk.html) — writing Lua apps, which
|
||||
needs no firmware build at all
|
||||
@@ -0,0 +1,97 @@
|
||||
# ThinkNode M9: Keyboard & D-pad Guide
|
||||
|
||||
The Elecrow ThinkNode M9 has **no touchscreen** — every screen, every setting,
|
||||
and every chat is driven by the d-pad, the dedicated function keys, and the
|
||||
QWERTY keyboard. If you're coming from the T-Deck or Heltec V4 (touch-first),
|
||||
this page is the five-minute tour that makes the board feel native.
|
||||
|
||||
## The inputs
|
||||
|
||||
- **D-pad** — four arrows around a centre button (**OK**). Arrows move the
|
||||
on-screen focus highlight; OK activates whatever is focused. **Holding OK**
|
||||
is the board's long-press: it fires the held item's long-press action (a
|
||||
chat message's context menu, the SD row's hold-to-format) and unlocks the
|
||||
lock screen.
|
||||
- **Function row** — dedicated keys for Chats, Home, Mentions, Advert, Map,
|
||||
Back, Mic and Ctrl (see the table below).
|
||||
- **QWERTY keyboard** — the keyboard controller resolves Shift/symbol layers
|
||||
itself, so keys always deliver their final character. It latches one key at
|
||||
a time, which is why there are no chords on this board — long-press carries
|
||||
the second layer instead. Start typing in a chat and the composer focuses
|
||||
itself; any key wakes the screen from idle.
|
||||
|
||||
## The one rule: Back closes what you see
|
||||
|
||||
**Back** always closes the *top* layer, one per press: map pan mode → an open
|
||||
dropdown → the power menu → the Control Center → a full-screen app → a dialog
|
||||
→ any popup → an open chat → back out of the page. Nothing ever closes
|
||||
invisibly beneath something else. The only exception is deliberate: progress
|
||||
overlays (SD format, bulk delete) block all keys until the operation finishes.
|
||||
|
||||
## Function keys
|
||||
|
||||
| Key | Press | Hold |
|
||||
|---|---|---|
|
||||
| **MSG** | Jump to the Chats tab (closes an open app first) | — |
|
||||
| **HOME** | Peel one layer off an open app; on the Home tab, toggle the app drawer; otherwise jump Home | — |
|
||||
| **@ (Mentions)** | Open the Mentions screen | — |
|
||||
| **ADV** | Open the Send Advert page | **Toggle GPS on/off** |
|
||||
| **MAP** | Jump to the Map tab — press again *on* the map to toggle pan mode | — |
|
||||
| **BACK** | Close the top layer (see above) | — |
|
||||
| **CTRL** | Open the Control Center (quick toggles, incl. the keyboard light: off / on / auto) | — |
|
||||
| **MIC** | Nothing yet — deliberately reserved | — |
|
||||
| **OK / Enter** | Activate the focused item; send a message; run a terminal command; newline in the editor | **Long-press the focused item / unlock the lock screen** |
|
||||
|
||||
## Map pan mode
|
||||
|
||||
On the Map tab, press **MAP** again: the arrows now pan the map instead of
|
||||
moving focus. **MAP** or **BACK** exits. Pan mode switches itself off whenever
|
||||
you leave the map or anything opens over it, so a stale pan can never eat a
|
||||
key press later. Auto-follow pauses while you pan and resumes when you exit.
|
||||
Entering the map fresh always starts in normal navigation.
|
||||
|
||||
## Typing & editing
|
||||
|
||||
Inside a text field the left/right arrows move the caret — and when the caret
|
||||
is already at the edge of the text, the same press steps focus *out* of the
|
||||
field, so you're never stuck. Enter sends (toggleable under Settings), Back
|
||||
leaves the field. In the Terminal, Enter runs the command; in the text editor
|
||||
it inserts a newline.
|
||||
|
||||
## Lock screen
|
||||
|
||||
**Hold OK** to unlock — the keyboard's hardware long-press stands in for the
|
||||
hold-to-unlock other boards do by touch. With "Flash on new message" enabled,
|
||||
an incoming message wakes (or lock-reveals) the screen and pulses the keyboard
|
||||
backlight.
|
||||
|
||||
## Inside apps
|
||||
|
||||
Store apps (Snake and friends) get the d-pad natively: arrows steer, OK taps
|
||||
the centre — start, steer, and tap-to-retry all work. Display-only apps
|
||||
(Airtime, RF Monitor) keep normal navigation: arrows move between the app's
|
||||
own buttons or scroll the feed. **Back and Home always escape an app** — no
|
||||
app can trap the keys, even while the screen is locked.
|
||||
|
||||
## Recent improvements (beta_66 branch)
|
||||
|
||||
If you last used the M9 on an earlier beta, the notable navigation changes:
|
||||
|
||||
- **Back restored and made consistent** — it had regressed to doing nothing
|
||||
outside text fields, and could close hidden popups beneath the Control
|
||||
Center. Both fixed; the close-what-you-see rule above now holds everywhere.
|
||||
- **Apps can't strand you** — opening Advert/Mentions over a running app used
|
||||
to orphan it with no key path back; app permission dialogs are now
|
||||
answerable with the d-pad.
|
||||
- **Map pan cleans up after itself** (see above — it used to leak across tab
|
||||
jumps).
|
||||
- **Keyboard light "On" applies at boot**, not after the first dim cycle.
|
||||
- **Terminal chat shows incoming replies**, not just your own sent lines.
|
||||
- **GPS works from a cold boot** — the toggle (and ADV-hold) no longer depends
|
||||
on a boot-time detection race.
|
||||
- **Spectrum analyzer** sweeps roughly twice as fast, survives read glitches
|
||||
without flattening the trace, and waits for an in-flight transmission
|
||||
instead of cutting it off when opened mid-send.
|
||||
|
||||
The engineering record behind these lives in
|
||||
[`variants/thinknode_m9/M9_PORT.md`](variants/thinknode_m9/M9_PORT.md).
|
||||
+131
-39
@@ -5,6 +5,60 @@ Goal: run the **full** wadamesh UI + functionality on the **LilyGo T-LoRa Pager*
|
||||
existing boards so a UI change ships everywhere at once. This file is the running
|
||||
plan/status — update it as we go.
|
||||
|
||||
## Current status (2026-08-20)
|
||||
|
||||
**The port has shipped.** Both radio variants debuted on the beta channel in
|
||||
**beta_45** (community PR #149, @codemonkeybr) and are installable from
|
||||
wadamesh.com; `README.md` and `DEVICES.md` list the pager as a supported beta
|
||||
board rather than a request. Milestones ⓪-⑦, ⑨ and ⑩ are done. Only ⑦b (the
|
||||
USB companion gate) and ⑧ (the on-device UI pass) are still open.
|
||||
|
||||
Done since the milestone history below was written, and re-verified against the
|
||||
code on 2026-08-20:
|
||||
|
||||
- **microSD is fully wired.** `CAP_SD`/`CAP_FILESYSTEM` are **1** — the staged
|
||||
per-call-site widening finished, so DataStore contacts/channels routing, the
|
||||
5000-message history ring, map-tile SD packs, settings/crash export and the
|
||||
About-page `/BINS` firmware export all work. See risk 1h for what that
|
||||
replaced.
|
||||
- **Release pipeline complete** — both envs in `release.sh`, both flasher
|
||||
manifests generated, `FIRMWARE_OTA_ENV` set on each (worklist ⑨).
|
||||
- **SD recovery without formatting**: a detected-but-unmountable card now shows
|
||||
a greyed tap-to-retry row. In-app format remains deliberately omitted on this
|
||||
board; the reasoning lives on the format-helper guard in `UITask.cpp`.
|
||||
- **"Save update bin to SD" reaches the pager** via `CAP_SD && CAP_OTA`
|
||||
(upstream e2d07d8) rather than a board-name list. Note that feature is spread
|
||||
over six `#if` regions: e2d07d8 converted two, which left the pager with the
|
||||
button but no worker — a hang plus a use-after-free on the status label, and
|
||||
it still compiled. All six are now identical; do not widen them piecemeal.
|
||||
|
||||
Still open, in priority order:
|
||||
|
||||
1. **⑧'s actual stated scope — the 222 px vertical-layout audit and the
|
||||
screen-by-screen focus-nav coverage pass — has still never been started**
|
||||
(risk 8). Everything that landed under M8 was reactive bug-fixing from
|
||||
user-reported issues. This remains the single biggest item in the port.
|
||||
2. **USB companion device-profile frame is still untested** (⑦b). Remove the
|
||||
temporary `[DISP]` register readback in `ST7796LCDDisplay::begin()` once it
|
||||
passes.
|
||||
3. Lock-screen wallpaper SD scanning is still T-Deck/M9-only (risk 1h).
|
||||
4. `deploy/flasher/` now carries pager cards + manifests for parity, but **no
|
||||
deploy script publishes that directory** (`deploy-site.sh` ships
|
||||
`deploy/site/` only) and `flasher.wadamesh.com` 301s to the apex. Decide
|
||||
whether to wire it into a deploy target or retire it.
|
||||
|
||||
Risk 11 (keyboard/encoder scroll not repainting) is **accepted as
|
||||
workaround-final**: the Alt+turn free-scroll gesture is the intended fix for
|
||||
this hardware.
|
||||
|
||||
---
|
||||
|
||||
## Milestone history
|
||||
|
||||
The narrative below is kept as the port's working record — the war stories are
|
||||
why several non-obvious things are the way they are. Where it conflicts with
|
||||
the section above, the section above is current.
|
||||
|
||||
Status: **M7's UI portion DONE — boot logo, full LVGL UI, QWERTY
|
||||
keyboard nav and rotary encoder all VERIFIED WORKING on real hardware
|
||||
(2026-07-07)**, after root-causing the intermittent black screen to the ST7796
|
||||
@@ -30,7 +84,8 @@ milestone: the radio-settings blur-autosave lag (risk 12), the encoder not
|
||||
scrolling open dropdowns (fixed 2026-07-08, commit `4bbfcb5`), the keyboard
|
||||
backlight never being wired up (risk 9, fixed 2026-07-10), and microSD file
|
||||
manager + WAV-picker support (risk 1h, fixed 2026-07-10, scoped — DataStore
|
||||
SD routing/chat-history/tiles/wallpaper/backup remain deferred). An
|
||||
SD routing/chat-history/tiles/backup were still deferred at the time and have
|
||||
all since landed; only wallpaper scanning is left, see risk 1h). An
|
||||
unread-message LED notification was investigated and explicitly decided
|
||||
against (risk 13) — this board has no onboard LED at all. Risk 11
|
||||
(keyboard/encoder-driven scroll not repainting) was **ACCEPTED as
|
||||
@@ -170,8 +225,8 @@ WASD-panning).
|
||||
`updatePagerAltShiftChord()` toggles Caps Lock ONLY while a text field is
|
||||
actually being edited (same "ta" derivation `handleHwKey()`'s TLORA_PAGER
|
||||
branch already uses to tell a bound-but-unfocused field apart from one
|
||||
actually being edited), and jumps straight to Home (`navGoToMainTab`)
|
||||
everywhere else.
|
||||
actually being edited), and is a no-op everywhere else. Alt+Backspace is
|
||||
the context-independent Home shortcut.
|
||||
3. **@-mention contact picker made encoder-navigable** (commit `57802eb`) —
|
||||
the same unreachable-without-touch problem the accent box had, fixed the
|
||||
same way (`mentionNavRestyle()`/`mentionNavConfirm()` mirror
|
||||
@@ -567,9 +622,9 @@ existing boards. Notes:
|
||||
`pagerKeyboardConsumeAltShiftChord()`; `UITask.cpp`'s
|
||||
`updatePagerAltShiftChord()` toggles persistent Caps Lock ONLY while a text
|
||||
field is actually being edited (same "ta" derivation as `handleHwKey()`'s
|
||||
TLORA_PAGER branch), and jumps straight Home (`navGoToMainTab`) everywhere
|
||||
else — toggling Caps Lock with no field to see it change in was reported as
|
||||
surprising/purposeless outside of typing.
|
||||
TLORA_PAGER branch), and is a no-op everywhere else — toggling Caps Lock
|
||||
with no field to see it change in was reported as surprising/purposeless
|
||||
outside of typing. Alt+Backspace owns the Home shortcut.
|
||||
See `PagerKeyboard.cpp`'s matrix-legend comment for the one documented
|
||||
edge case (Alt+Shift+O or Alt+Shift+L all three held phantom-ghosts a
|
||||
stray 'q'/'a' — a diode-less-matrix limitation, not a bug, and not the
|
||||
@@ -601,7 +656,7 @@ existing boards. Notes:
|
||||
**Gate:** all three envs build — verified.
|
||||
- [x] ⑥ **UITask wiring**: `device_caps.h` got its `TLORA_PAGER` cap block (no
|
||||
touch, no rotate, no large-screen, GPS 1, OTA 1, lock-screen 1; `CAP_SD`/
|
||||
`CAP_FILESYSTEM` corrected to 0 — see below) plus widened `CAP_KEYBOARD`/
|
||||
`CAP_FILESYSTEM` set to 0 at the time — both are 1 today, see risk 1h) plus widened `CAP_KEYBOARD`/
|
||||
`CAP_KEYPAD_NAV` derivations to recognize `HAS_PAGER_KEYBOARD`/
|
||||
`TLORA_PAGER`. `UITask.cpp` changes, all pager-gated: KEYPAD indev
|
||||
registered by widening the Tanmatsu branch's `#if` to
|
||||
@@ -624,9 +679,10 @@ existing boards. Notes:
|
||||
vs. the milestone doc's assumptions, all found by compiling rather than
|
||||
assumed — see risks 1h/1i/1j:
|
||||
- `CAP_SD`/`CAP_FILESYSTEM` looked right from the hardware (real microSD
|
||||
slot) but the actual mount code is hardcoded to `HAS_TDECK_GT911`, not
|
||||
truly `CAP_SD`-generic — set back to 0, real support is unscheduled
|
||||
follow-up.
|
||||
slot) but the mount code was hardcoded to `HAS_TDECK_GT911`, not truly
|
||||
`CAP_SD`-generic — set back to 0 for this milestone. *(Superseded: the
|
||||
call sites were widened over the following passes and both macros are 1
|
||||
today — risk 1h.)*
|
||||
- `navMaybeRebuild()` (populates the LVGL focus group every screen) wasn't
|
||||
reachable on the pager's own cap combination — added a
|
||||
`#elif defined(TLORA_PAGER)` arm; without it the KEYPAD indev would have
|
||||
@@ -653,11 +709,23 @@ existing boards. Notes:
|
||||
above): still untested. **SD storage: DONE** — real mount, browse,
|
||||
read/write/delete, and insert/remove detection all confirmed on hardware
|
||||
(risk 1h). (DataStore/NVS-level SD use — routing contacts/channels to the
|
||||
card — is a separate, still-deferred feature, not part of this bring-up
|
||||
gate.) Remove the temporary `[DISP]` register readback in
|
||||
card — was a separate feature outside this bring-up gate; it has since
|
||||
landed, see risk 1h.) Remove the temporary `[DISP]` register readback in
|
||||
`ST7796LCDDisplay::begin()` once the USB piece passes.
|
||||
- [~] ⑧ **On-device UI pass** — ACTIVE: nav-coverage audit screen by screen (every interactive control reachable via focus group — the Tanmatsu work paved this), chat layout at 222 px, map pan via encoder/keys, fonts legibility at 480-wide, plus fixing any weird UI/keyboard behaviors surfaced by manual testing (human provides photos/repro steps).
|
||||
- [ ] ⑨ **Release pipeline**: add the env:binname pair to `release.sh` `ENVS`, flasher manifest (`deploy/flasher/manifest-tlora-pager.json`), OTA env name via `FIRMWARE_OTA_ENV`. Separate PR.
|
||||
- [x] ⑨ **Release pipeline** — DONE (verified against the scripts 2026-08-20).
|
||||
Both envs are in `scripts/release.sh`'s `ENVS`
|
||||
(`tlora_pager_lr1121_...:wadamesh-tlora-pager-lr1121`,
|
||||
`tlora_pager_sx1262_...:wadamesh-tlora-pager-sx1262`); both manifests are
|
||||
generated by `scripts/build/gen-flasher-meta.py`
|
||||
(`manifest-tlora-pager-{lr1121,sx1262}.json`); both envs set
|
||||
`FIRMWARE_OTA_ENV`. The install page (`deploy/site/index.html`, which
|
||||
`flasher.wadamesh.com` 301s to) carries an install button + .bin download for
|
||||
each variant off the beta feed. Shipped to users in **beta_45**.
|
||||
One leftover, not user-facing: the legacy static `deploy/flasher/index.html`
|
||||
still lists only the T-Deck and Heltec V4 cards and has no pager manifests
|
||||
next to it. That page is no longer the install path — decide whether to add
|
||||
the two cards for parity or retire the directory.
|
||||
- [x] ⑩ (Optional, cheap) `tlora_pager_sx1262_...` env for SX1262-variant owners — **DONE, hardware-verified 2026-07-13** (`tlora_pager_sx1262_companion_radio_touch`, all 4 envs green; see `TLORA_PAGER_PORT_MILESTONES.md`'s Milestone 10 for the full writeup, including the `target.h` missing-include bug found + fixed). Radio gate (M7 step 3) confirmed on real hardware: user sent + received messages on public, a custom `#` channel, and DM.
|
||||
|
||||
## UI-changes inventory (what actually changes in `src/ui-touch/`)
|
||||
@@ -673,15 +741,17 @@ existing boards. Notes:
|
||||
| `navMaybeRebuild()` reachability | Added `#elif defined(TLORA_PAGER)` arm — was unreachable, would've left focus group permanently empty | small | done (bug fix) |
|
||||
| Vertical budget | Audit `STATUSBAR_H`, `TABBAR_H`, `CHAT_KB_H`, modal/chat height helpers for 222 px | **the real work** | deferred to M8 on-device (risk 8) |
|
||||
| Focus-nav coverage | Screen-by-screen pass that every control is in `s_nav_group` | medium, on-device | deferred to M8 |
|
||||
| device_caps.h | New `CAP_*` block: no touch, hw keyboard, encoder, 480×222, GPS, OTA, lock-screen; SD/filesystem left 0 by design — real SD support added via widened individual call sites instead (see risk 1h) | trivial | done |
|
||||
| device_caps.h | New `CAP_*` block: no touch, hw keyboard, encoder, 480×222, GPS, OTA, lock-screen. `CAP_SD`/`CAP_FILESYSTEM` were 0 while SD support was staged in per call site, and are now **1** (see risk 1h) | trivial | done |
|
||||
| Keyboard backlight (`pagerKeyboardSetBacklight()`) | `updatePagerKbBacklight()` tick + Settings → Keyboard cycle row (off/on/auto) | small | done (risk 9) |
|
||||
| microSD (file manager + WAV picker) | `fmSdTryMount()`/`fmShowRoots()`/`fmIsAudio()` etc. widened to `\|\| defined(TLORA_PAGER)`; DataStore SD routing, chat-history cap, tile/wallpaper/backup SD use still deferred | medium | done, scoped (risk 1h) |
|
||||
| microSD (file manager + WAV picker) | `fmSdTryMount()`/`fmShowRoots()`/`fmIsAudio()` etc. widened to `\|\| defined(TLORA_PAGER)`; DataStore routing, 5000-msg history, tiles and backup export all landed later. Only wallpaper SD scanning is still T-Deck/M9-only | medium | done (risk 1h) |
|
||||
| microSD recovery row | `fmSdPagerRetryMountCb` — a detected-but-unmountable card renders greyed with a tap-to-retry that clears the mount backoff. No in-app format on this board, by decision | small | done |
|
||||
| Save update bin to SD | About-page `/BINS` firmware export widened to the pager (`sdFwWorkerRun`); board-specific wording kept as separate TR() keys so the T-Deck's translated Launcher strings are not orphaned | small | done |
|
||||
| Chat bubble Enter/Backspace (`handleHwKey()` ~28808) | Enter on a focused bubble opens the Ack/Mention/Copy/Info/Block menu (`navEnterBubble()`); Backspace jumps to the newest message when the scroll-to-bottom button is showing | small | done |
|
||||
| Shift/Caps (`PagerKeyboard.cpp`) | Held Shift = momentary uppercase; held Alt+Shift = chord (see below for what it does) | small | done |
|
||||
| Accent-variant popup keyboard nav | Fn+Space arms it; encoder walks variants; encoder click/Enter confirms; Backspace cancels (`s_accentnav_active`/`accentNavRestyle()`/`accentNavConfirm()`) | medium | done |
|
||||
| @-mention picker keyboard nav | Auto-arms the instant the list appears (no Fn+Space needed); same encoder walk/confirm/cancel as the accent popup (`s_mentionnav_active`/`mentionNavRestyle()`/`mentionNavConfirm()`) | medium | done |
|
||||
| Encoder short click on a focused bubble | Mirrors keyboard Enter — opens the same action menu instead of a plain ENTER a bubble doesn't react to | small | done |
|
||||
| Fn+Shift chord effect | Toggles Caps Lock while editing a text field; jumps to Home (`navGoToMainTab`) everywhere else — `PagerKeyboard.cpp` only reports the chord, `UITask.cpp` decides the effect | small | done |
|
||||
| Fn+Shift chord effect | Toggles Caps Lock while editing a text field; no-op everywhere else. Fn+Backspace jumps Home — `PagerKeyboard.cpp` only reports the chords, `UITask.cpp` decides the effects | small | done |
|
||||
| Map pan (WASD) + slider nudge (Q/E) | WASD pans the Map tab (`mapNudge()`, shared with Tanmatsu's Ctrl+Arrow); Q/E adjust a focused slider (moved off D/F to avoid colliding with WASD) | small | done |
|
||||
| App-drawer tile focus highlight | `NAV_ACCENTFOCUS_FLAG` makes `navFocusCb` use the tile's own accent tint instead of the generic white reverse-fill, on every board | small | done |
|
||||
| SX1262-variant env | `tlora_pager_sx1262_companion_radio_touch` — same board/pins, `CustomSX1262`/`CustomSX1262Wrapper` instead of the LR1121 classes | medium | done (worklist ⑩) |
|
||||
@@ -776,29 +846,51 @@ MQTT, OTA) is resolution-agnostic or already keyed off caps.
|
||||
page-scroll backward/up instead. Fixed by negating once in
|
||||
`PagerEncoder.cpp`'s `pagerEncoderReadDelta()` (the driver, not UI code)
|
||||
so every consumer inherits the corrected sense from one place.
|
||||
1h. **`CAP_SD`/`CAP_FILESYSTEM` are 0 for the pager despite real microSD
|
||||
hardware — a latent gap in `device_caps.h`'s own abstraction, not a pager
|
||||
bug.** **File manager browsing + WAV notification-sound picker RESOLVED
|
||||
2026-07-10** (commits `8c7fb6a`/`1514e76`), scoped deliberately: `CAP_SD`
|
||||
turned out to gate ~37 call sites (file manager, WAV picker, DataStore
|
||||
contacts/channels routing, tile-cache SD fallback, wallpaper scanning,
|
||||
settings backup, screenshots), so flipping the macro itself would have
|
||||
silently turned on ~30 unvetted, T-Deck-only-tested features at once.
|
||||
Instead `CAP_SD`/`CAP_FILESYSTEM` stay 0 by design, and only the ~7
|
||||
specific call sites needed for browsing + WAV picking were widened with
|
||||
`|| defined(TLORA_PAGER)` — see those commits for the full categorized
|
||||
site list. Added `pagerSdCardPresent()` (XL9555 `PAGER_EXPAND_SD_DET`,
|
||||
confirmed-inverted polarity) and a pager `sdSharedSPI()` (reuses
|
||||
`TFT_eSPI::getSPIinstance()`, the same bus the radio already shares).
|
||||
Format support is explicitly out of scope (no `f_mkfs`/`sd_diskio.h` pulled
|
||||
in) — the pager's SD row is tap-to-open only. Hardware-verified: mount,
|
||||
browse, read/write/delete, insert/remove detection, and picking a WAV from
|
||||
SD as a notification sound all confirmed on real hardware. **Still not
|
||||
done** (unscheduled follow-up, deliberately deferred): DataStore
|
||||
contacts/channels routing to SD (`setSecondaryFS`, what Tanmatsu/T-Deck
|
||||
do to dodge SPIFFS GC stalls), the `MAX_UI_MESSAGES_SD` chat-history cap
|
||||
bump, map-tile SD fallback/offline tile packs, lock-screen wallpaper SD
|
||||
scanning, and settings/telemetry backup export-import to SD.
|
||||
1h. **microSD — RESOLVED; `CAP_SD`/`CAP_FILESYSTEM` are now 1.** They were 0
|
||||
from M6 onward, which looked wrong against the hardware (there is a real
|
||||
slot) but was deliberate: `CAP_SD` gates ~37 call sites (file manager, WAV
|
||||
picker, DataStore contacts/channels routing, tile-cache SD fallback,
|
||||
wallpaper scanning, settings backup, screenshots), so flipping the macro in
|
||||
one go would have switched on ~30 unvetted, T-Deck-only-tested features at
|
||||
once. The support was staged in instead, a group at a time:
|
||||
- **File manager browsing + WAV notification-sound picker** — 2026-07-10
|
||||
(commits `8c7fb6a`/`1514e76`), the first ~7 sites, widened with
|
||||
`|| defined(TLORA_PAGER)`. Added `pagerSdCardPresent()` (XL9555
|
||||
`PAGER_EXPAND_SD_DET`, confirmed-inverted polarity) and a pager
|
||||
`sdSharedSPI()` (reuses `TFT_eSPI::getSPIinstance()`, the same bus the
|
||||
radio already shares). Hardware-verified: mount, browse,
|
||||
read/write/delete, insert/remove detection, and picking a WAV off the card
|
||||
as a notification sound.
|
||||
- **The rest** — verified in code 2026-08-20: DataStore contacts/channels
|
||||
routing (`src/main.cpp`'s `setSecondaryFS`/`useSdStorage` blocks admit
|
||||
`TLORA_PAGER`), the `MAX_UI_MESSAGES_SD` 5000-message history ring
|
||||
(`uiDataFsIsSdCard()` admits the pager), map-tile SD fallback and offline
|
||||
packs (the Map "tiles from SD" toggle; the tile-cache fix shipped in
|
||||
beta_46), settings backup export/import plus crash-dump export
|
||||
(`doExportBackupFile`), and firmware export to `/BINS` via About → "Save
|
||||
update bin to SD".
|
||||
|
||||
With those vetted, both macros were flipped to 1 and `device_caps.h`
|
||||
describes the hardware again. Cleanup left over: the belt-and-braces
|
||||
`#if CAP_SD || defined(TLORA_PAGER)` at several sites is now redundant and
|
||||
can be collapsed to plain `CAP_SD`.
|
||||
|
||||
**In-app format stays deliberately out of scope** on this board (no
|
||||
`f_mkfs`/`sd_diskio.h` pulled in). The full reasoning now lives on the
|
||||
format-helper guard in `UITask.cpp` so it is found from the code, not only
|
||||
here: `f_mkfs` needs an `SD.end()` + `sdcard_init`/`uninit` lifecycle
|
||||
teardown on the live shared display/radio bus; there is no touchscreen, so
|
||||
the T-Deck's hold-to-format gesture does not map; and it has never been run
|
||||
on pager hardware. The *recoverable* case is covered non-destructively
|
||||
instead — a card the expander's SD_DET sees but that will not mount renders
|
||||
a greyed **"SD card (unreadable - tap to retry)"** row
|
||||
(`fmSdPagerRetryMountCb`) which clears the mount backoff and retries the
|
||||
single 4 MHz attempt, then points the user at formatting on a computer.
|
||||
|
||||
**Still not done**: lock-screen wallpaper SD scanning — that picker block is
|
||||
still `HAS_TDECK_GT911 || HAS_THINKNODE_M9`, so the pager keeps its baked
|
||||
480x222 wallpaper (`lockscreen_wallpaper_pager_rgb565.h`) with no way to
|
||||
choose one off the card.
|
||||
1i. **`navMaybeRebuild()` was unreachable for the pager in the first M6
|
||||
pass.** It's only called under `#if defined(HAS_TANMATSU) ... #elif
|
||||
CAP_TRACKBALL ...`, neither of which the pager's cap combination matches
|
||||
|
||||
+30
-22
@@ -40,7 +40,7 @@ while editing a text field, where the keys type normally.
|
||||
|
||||
| Gesture | Action | Keyboard equivalent |
|
||||
|---|---|---|
|
||||
| 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) |
|
||||
| Turn | Move focus to the next/previous item on screen | none — encoder only |
|
||||
| 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 | Move between the 5 main tabs (Mail / Contacts / Home / Map / Settings) | **M / C / H / A / S** jumps directly |
|
||||
@@ -93,29 +93,19 @@ text box → Send**, in that order, in both directions — nothing is skipped.
|
||||
Turning past the △ chip (toward the messages) always jumps straight to the
|
||||
newest message, regardless of channel or DM.
|
||||
|
||||
### Backspace and Fn (Alt) — leaving vs. catching up
|
||||
### Backspace — catching up
|
||||
|
||||
These two keys are your shortcuts in and out of "reading mode" on this
|
||||
screen, and they do different things depending on whether you've got unread
|
||||
messages:
|
||||
**Backspace (tap)** jumps to whichever message needs your attention:
|
||||
|
||||
- **Fn (Alt) tapped alone** — jumps straight to the **newest** message and
|
||||
moves focus into the text box, ready to type a reply. This is the
|
||||
deliberate "I'm done reading, let me respond" gesture — it works regardless
|
||||
of where your focus currently is in the chat.
|
||||
- **Backspace (tap)** — jumps to whichever message needs your attention:
|
||||
- If there are unread messages, it jumps to the first one — right below the
|
||||
**"NEW ----"** divider — and selects it, so you can then turn forward
|
||||
through your unread messages in order, oldest-of-the-unread first.
|
||||
- If nothing is unread, it jumps to the newest message instead (same
|
||||
destination as Fn+Alt, but leaves focus on the message itself rather than
|
||||
the text box).
|
||||
- If there are unread messages, it jumps to the first one — right below the
|
||||
**"NEW ----"** divider — and selects it, so you can then turn forward
|
||||
through your unread messages in order, oldest-of-the-unread first.
|
||||
- If nothing is unread, it jumps to the newest message instead (same
|
||||
destination, with focus left on the message).
|
||||
|
||||
Backspace's override above only applies while you're inside an open chat and
|
||||
*not* actively editing the text box — the usual rule from the top of this
|
||||
guide; with a field focused, Backspace deletes a character as normal.
|
||||
Fn (Alt) tap's override fires the same way whether or not the text box
|
||||
already has focus, since landing there is the whole point of the gesture.
|
||||
|
||||
## Keyboard layout
|
||||
|
||||
@@ -128,7 +118,9 @@ a s d f g h j k l [Enter]
|
||||
[Space]
|
||||
```
|
||||
|
||||
Hold **Fn (Alt)** for numbers/symbols instead:
|
||||
Hold **Fn (Alt)** for numbers/symbols, or tap it once to apply this layer to
|
||||
the next key only. Double-tap Fn to lock the symbol layer; tap it again to
|
||||
return to letters.
|
||||
|
||||
```
|
||||
1 2 3 4 5 6 7 8 9 0
|
||||
@@ -162,7 +154,7 @@ Hold **Fn (Alt)** for numbers/symbols instead:
|
||||
| **Space** (tap) | Types a space | — |
|
||||
| **Space** (double-tap, within 250 ms) | Switches between English and your configured secondary keyboard layout | — |
|
||||
| **Space** (hold ~1 s) | — | Locks the screen (shows a "Locking…" progress bar; tapping any key cancels) |
|
||||
| **Fn (Alt)** tapped alone (press+release, nothing else) | — | Moves focus to the next field/item — a keyboard-only substitute for a turn of the encoder. In an open chat: jumps to the newest message and focuses the text box instead — see [Chat screen](#chat-screen) |
|
||||
| **Fn (Alt)** (tap / double-tap) | Next key uses symbols / lock symbols until tapped again | Same |
|
||||
|
||||
The **BOOT** button (top of the device) instantly wakes the screen from
|
||||
idle-dim. It does *not* unlock a screen you've manually locked with the
|
||||
@@ -231,18 +223,32 @@ needed.
|
||||
Unlike the T-Deck, this is a plain on/off strip under the keys, not a
|
||||
dimmable brightness curve.
|
||||
|
||||
## Leaving Remote Mode
|
||||
|
||||
Remote Mode replaces the normal device UI with a physical-screen placeholder
|
||||
showing the browser address and an **EXIT REMOTE** button. While that screen is
|
||||
active, normal navigation is paused. Press the **encoder knob** to activate the
|
||||
button, turn off Remote Mode, and reboot back to the normal screen. You can also
|
||||
press **Space twice within 3 seconds**; after the first press, the button changes
|
||||
to **press SPACE again**. The browser's **Exit remote** button performs the same
|
||||
action.
|
||||
|
||||
This Remote Mode gesture is separate from the normal 250 ms Space double-tap
|
||||
that changes keyboard language.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| 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 encoder | 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 |
|
||||
| Hold encoder ~1s (or hold Backspace ~1s) | Back |
|
||||
| Fn + turn (main tab) | Switch tabs |
|
||||
| Fn + turn (page/chat) | Scroll |
|
||||
| Fn tap alone | Next field (in a chat: jump to newest message + focus the text box) |
|
||||
| Fn tap alone | Use the symbol layer for the next key |
|
||||
| Fn double-tap | Lock the symbol layer; tap Fn again to unlock |
|
||||
| Hold Shift + letter | Momentary uppercase |
|
||||
| Fn + Shift (editing a field) | Toggle Caps Lock |
|
||||
| Fn + Shift (not editing a field) | Nothing |
|
||||
@@ -255,5 +261,7 @@ dimmable brightness curve.
|
||||
| Space (tap) | Space |
|
||||
| Space (double-tap) | Switch keyboard language |
|
||||
| Space (hold 1s) | Lock screen |
|
||||
| Encoder click (Remote Mode only) | Activate EXIT REMOTE and reboot normally |
|
||||
| Space twice within 3s (Remote Mode only) | Exit Remote Mode and reboot normally |
|
||||
| Q / E (slider focused) | Decrease / increase |
|
||||
| W / A / X / D (Map tab) | Pan map north/west/south/east |
|
||||
+14
-7
@@ -5,8 +5,8 @@ Distribution stack for wadamesh: a **VPS nginx origin behind Cloudflare**.
|
||||
```
|
||||
tiles.wadamesh.com →CF (HTTP, edge-cached) → nginx → OpenStreetMap / OpenTopoMap
|
||||
firmware.wadamesh.com →CF (cache bins) → nginx → /srv/wadamesh/firmware
|
||||
flasher.wadamesh.com →CF (HTTPS) → web flasher (TODO — see below)
|
||||
wadamesh.com →CF (HTTPS) → landing page (later)
|
||||
flasher.wadamesh.com →CF (HTTPS) → 301 → wadamesh.com
|
||||
wadamesh.com →CF (HTTPS) → nginx → /srv/wadamesh/site
|
||||
```
|
||||
|
||||
**Map tile styles.** The default `/{z}/{x}/{y}.jpg` route serves **OpenStreetMap**
|
||||
@@ -67,11 +67,18 @@ highest `beta_<N>`, and (once OTA-over-Wi-Fi is re-enabled) pulls
|
||||
|
||||
## Done
|
||||
|
||||
- **Web flasher** ✅ at `flasher.wadamesh.com` — `deploy/flasher/` (esp-web-tools /
|
||||
Web Serial, board picker, manifests pointing at the rolling `/latest/` merged
|
||||
bins; `release.sh` refreshes `/latest/` each publish).
|
||||
- **Apex** `wadamesh.com` 301-redirects to the flasher — **activate by pointing
|
||||
the `wadamesh.com` A record at the VPS** in Cloudflare (it's still on the parking IP).
|
||||
- **Web flasher** ✅ — the guided install page at `wadamesh.com`, served from
|
||||
`deploy/site/` and published by `scripts/deploy-site.sh` (esp-web-tools / Web
|
||||
Serial, per-board install buttons + .bin downloads, manifests generated per
|
||||
release by `scripts/build/gen-flasher-meta.py` into the `/latest/` and
|
||||
`/latest-beta/` feeds that `release.sh` refreshes each publish).
|
||||
- **`flasher.wadamesh.com` 301-redirects to the apex** (see
|
||||
`deploy/nginx/flasher.wadamesh.com.conf`) — it is an alias, not its own page.
|
||||
- **`deploy/flasher/`** is the original standalone flasher page. **No deploy
|
||||
script publishes it** — `deploy-site.sh` ships `deploy/site/` only — so it is
|
||||
effectively an offline/local copy kept in parity by hand. Retire it or wire it
|
||||
into a deploy target; until then, treat `deploy/site/index.html` as the only
|
||||
install page users can reach.
|
||||
|
||||
## TODO before public launch
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"2048","name":"2048","ver":"1.2","desc":"The classic sliding-tile puzzle. Swipe to move, tap to restart. Keeps your best score. Contributed by pisti87."}
|
||||
@@ -0,0 +1,837 @@
|
||||
-- 2048 — wada.* reference app
|
||||
-- Swipe (or trackball mapped swipe) to move tiles.
|
||||
-- Tap after win or game over to restart.
|
||||
|
||||
local ui, sys, store, timer = wada.ui, wada.sys, wada.store, wada.timer
|
||||
local C = ui.colors
|
||||
|
||||
local app = {}
|
||||
|
||||
local cv, score_lbl
|
||||
|
||||
local CELL = 38
|
||||
|
||||
local grid = {}
|
||||
|
||||
local history = {
|
||||
grids = {},
|
||||
maxsize = 5
|
||||
}
|
||||
|
||||
local px_w, px_h
|
||||
local score = 0
|
||||
local hiscore = 0
|
||||
|
||||
local over = false
|
||||
local won = false
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- History
|
||||
--------------------------------------------------
|
||||
|
||||
function history:save()
|
||||
|
||||
local old = {
|
||||
grid = {},
|
||||
score = score,
|
||||
over = over,
|
||||
won = won
|
||||
}
|
||||
|
||||
for x = 1, 4 do
|
||||
|
||||
old.grid[x] = {}
|
||||
|
||||
for y = 1, 4 do
|
||||
old.grid[x][y] = grid[x][y]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
table.insert(self.grids, old)
|
||||
|
||||
if #self.grids > self.maxsize then
|
||||
table.remove(self.grids, 1)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
function history:revert()
|
||||
|
||||
if #self.grids > 0 then
|
||||
|
||||
local old = table.remove(self.grids)
|
||||
|
||||
grid = old.grid
|
||||
score = old.score
|
||||
over = old.over
|
||||
won = old.won
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Game logic
|
||||
--------------------------------------------------
|
||||
|
||||
local function clear_grid()
|
||||
|
||||
grid = {}
|
||||
|
||||
for x = 1, 4 do
|
||||
|
||||
grid[x] = {}
|
||||
|
||||
for y = 1, 4 do
|
||||
grid[x][y] = 0
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Add random tile
|
||||
--------------------------------------------------
|
||||
|
||||
local function add_random_tile()
|
||||
|
||||
local free = {}
|
||||
|
||||
for x = 1, 4 do
|
||||
|
||||
for y = 1, 4 do
|
||||
|
||||
if grid[x][y] == 0 then
|
||||
|
||||
table.insert(
|
||||
free,
|
||||
{
|
||||
x = x,
|
||||
y = y
|
||||
}
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
-- Board is full.
|
||||
if #free == 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
local p
|
||||
|
||||
|
||||
-- If there is only one empty cell,
|
||||
-- use it directly.
|
||||
if #free == 1 then
|
||||
|
||||
p = free[1]
|
||||
|
||||
else
|
||||
|
||||
local index = sys.random(1, #free)
|
||||
|
||||
if index == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
p = free[index]
|
||||
|
||||
if p == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
-- 10% chance for 4,
|
||||
-- 90% chance for 2.
|
||||
if sys.random(0, 9) == 0 then
|
||||
grid[p.x][p.y] = 4
|
||||
else
|
||||
grid[p.x][p.y] = 2
|
||||
end
|
||||
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Reset
|
||||
--------------------------------------------------
|
||||
|
||||
local function reset()
|
||||
|
||||
clear_grid()
|
||||
|
||||
score = 0
|
||||
over = false
|
||||
won = false
|
||||
|
||||
history.grids = {}
|
||||
|
||||
add_random_tile()
|
||||
add_random_tile()
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Compress line
|
||||
--------------------------------------------------
|
||||
|
||||
local function compress(line)
|
||||
|
||||
local r = {}
|
||||
|
||||
for i = 1, 4 do
|
||||
|
||||
if line[i] ~= 0 then
|
||||
table.insert(r, line[i])
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
while #r < 4 do
|
||||
table.insert(r, 0)
|
||||
end
|
||||
|
||||
return r
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Merge line
|
||||
--------------------------------------------------
|
||||
|
||||
local function merge(line)
|
||||
|
||||
local r = {}
|
||||
local i = 1
|
||||
|
||||
while i <= 4 do
|
||||
|
||||
if i < 4
|
||||
and line[i] ~= 0
|
||||
and line[i] == line[i + 1]
|
||||
then
|
||||
|
||||
local v = line[i] * 2
|
||||
|
||||
table.insert(r, v)
|
||||
|
||||
score = score + v
|
||||
|
||||
|
||||
-- Permanent high score.
|
||||
if score > hiscore then
|
||||
|
||||
hiscore = score
|
||||
|
||||
store.set(
|
||||
"2048_best",
|
||||
hiscore
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
|
||||
-- Reaching 2048 means victory.
|
||||
if v >= 2048 then
|
||||
won = true
|
||||
end
|
||||
|
||||
|
||||
i = i + 2
|
||||
|
||||
else
|
||||
|
||||
table.insert(r, line[i])
|
||||
|
||||
i = i + 1
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
while #r < 4 do
|
||||
table.insert(r, 0)
|
||||
end
|
||||
|
||||
return r
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Move left
|
||||
--------------------------------------------------
|
||||
|
||||
local function move_left()
|
||||
|
||||
local changed = false
|
||||
|
||||
for y = 1, 4 do
|
||||
|
||||
local old = {}
|
||||
local line = {}
|
||||
|
||||
for x = 1, 4 do
|
||||
|
||||
old[x] = grid[x][y]
|
||||
line[x] = grid[x][y]
|
||||
|
||||
end
|
||||
|
||||
|
||||
line = compress(line)
|
||||
line = merge(line)
|
||||
|
||||
|
||||
for x = 1, 4 do
|
||||
|
||||
grid[x][y] = line[x]
|
||||
|
||||
if old[x] ~= line[x] then
|
||||
changed = true
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return changed
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Rotate
|
||||
--------------------------------------------------
|
||||
|
||||
local function rotate()
|
||||
|
||||
local n = {}
|
||||
|
||||
for x = 1, 4 do
|
||||
n[x] = {}
|
||||
end
|
||||
|
||||
|
||||
for x = 1, 4 do
|
||||
|
||||
for y = 1, 4 do
|
||||
|
||||
n[y][5 - x] = grid[x][y]
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
grid = n
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Game over
|
||||
--------------------------------------------------
|
||||
|
||||
local function game_over()
|
||||
|
||||
-- Any empty cell means the game can continue.
|
||||
for x = 1, 4 do
|
||||
|
||||
for y = 1, 4 do
|
||||
|
||||
if grid[x][y] == 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
-- Check horizontal and vertical merges.
|
||||
for x = 1, 4 do
|
||||
|
||||
for y = 1, 4 do
|
||||
|
||||
if x < 4
|
||||
and grid[x][y] == grid[x + 1][y]
|
||||
then
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
if y < 4
|
||||
and grid[x][y] == grid[x][y + 1]
|
||||
then
|
||||
return false
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
-- Board is full and no merge is possible.
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Move
|
||||
--------------------------------------------------
|
||||
|
||||
local function move(dir)
|
||||
|
||||
-- Do nothing after victory or defeat.
|
||||
if over or won then
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
history:save()
|
||||
|
||||
local changed = false
|
||||
|
||||
|
||||
if dir == "left" then
|
||||
|
||||
changed = move_left()
|
||||
|
||||
|
||||
elseif dir == "right" then
|
||||
|
||||
rotate()
|
||||
rotate()
|
||||
|
||||
changed = move_left()
|
||||
|
||||
rotate()
|
||||
rotate()
|
||||
|
||||
|
||||
elseif dir == "up" then
|
||||
|
||||
rotate()
|
||||
|
||||
changed = move_left()
|
||||
|
||||
rotate()
|
||||
rotate()
|
||||
rotate()
|
||||
|
||||
|
||||
elseif dir == "down" then
|
||||
|
||||
rotate()
|
||||
rotate()
|
||||
rotate()
|
||||
|
||||
changed = move_left()
|
||||
|
||||
rotate()
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Nothing moved.
|
||||
--------------------------------------------------
|
||||
|
||||
if not changed then
|
||||
|
||||
history:revert()
|
||||
|
||||
return
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Successful move.
|
||||
-- Exactly one new tile must be created.
|
||||
--------------------------------------------------
|
||||
|
||||
local added = add_random_tile()
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- This should only happen if the board was already
|
||||
-- completely full. Normally a successful move on a
|
||||
-- full board creates space, so added should be true.
|
||||
--------------------------------------------------
|
||||
|
||||
if not added then
|
||||
|
||||
over = true
|
||||
|
||||
return
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Victory.
|
||||
--
|
||||
-- merge() sets won when a 2048 tile is created.
|
||||
--------------------------------------------------
|
||||
|
||||
if won then
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- IMPORTANT:
|
||||
--
|
||||
-- Game-over is checked AFTER the new random tile
|
||||
-- has been inserted.
|
||||
--------------------------------------------------
|
||||
|
||||
if game_over() then
|
||||
over = true
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Tile colors
|
||||
--------------------------------------------------
|
||||
|
||||
local function tile_color(v)
|
||||
|
||||
if v == 0 then
|
||||
return 0xCDC1B4
|
||||
|
||||
elseif v == 2 then
|
||||
return 0xEEE4DA
|
||||
|
||||
elseif v == 4 then
|
||||
return 0xEDE0C8
|
||||
|
||||
elseif v == 8 then
|
||||
return 0xF2B179
|
||||
|
||||
elseif v == 16 then
|
||||
return 0xF59563
|
||||
|
||||
elseif v == 32 then
|
||||
return 0xF67C5F
|
||||
|
||||
elseif v == 64 then
|
||||
return 0xF65E3B
|
||||
|
||||
elseif v == 128 then
|
||||
return 0xEDCF72
|
||||
|
||||
elseif v == 256 then
|
||||
return 0xEDCC61
|
||||
|
||||
elseif v == 512 then
|
||||
return 0xEDC850
|
||||
|
||||
elseif v == 1024 then
|
||||
return 0xEDC53F
|
||||
|
||||
elseif v == 2048 then
|
||||
return 0xEDC22E
|
||||
|
||||
end
|
||||
|
||||
-- Values above 2048.
|
||||
return 0x3C3A32
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Text colors
|
||||
--------------------------------------------------
|
||||
|
||||
local function text_color(v)
|
||||
|
||||
if v <= 4 then
|
||||
return 0x776E65
|
||||
else
|
||||
return 0xFFFFFF
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Text size
|
||||
--------------------------------------------------
|
||||
|
||||
local function text_size(v)
|
||||
|
||||
if v < 100 then
|
||||
return 16
|
||||
|
||||
elseif v < 1000 then
|
||||
return 14
|
||||
|
||||
elseif v < 10000 then
|
||||
return 11
|
||||
|
||||
else
|
||||
return 9
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Text positioning
|
||||
--------------------------------------------------
|
||||
|
||||
local function text_x(px, v, size)
|
||||
|
||||
local width
|
||||
|
||||
if v < 100 then
|
||||
width = size * 1.1
|
||||
|
||||
elseif v < 1000 then
|
||||
width = size * 1.7
|
||||
|
||||
elseif v < 10000 then
|
||||
width = size * 2.3
|
||||
|
||||
else
|
||||
width = size * 2.9
|
||||
end
|
||||
|
||||
|
||||
return px + math.floor(
|
||||
(CELL - width) / 2
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
|
||||
local function text_y(py, size)
|
||||
|
||||
return py + math.floor(
|
||||
(CELL - size) / 2
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Score line
|
||||
--------------------------------------------------
|
||||
|
||||
local function scoreline()
|
||||
|
||||
local status = ""
|
||||
|
||||
if won then
|
||||
|
||||
status = " - tap retry"
|
||||
|
||||
elseif over then
|
||||
|
||||
status = " - tap retry"
|
||||
|
||||
end
|
||||
|
||||
|
||||
score_lbl:set(
|
||||
string.format(
|
||||
"Score %d Best %d%s",
|
||||
score,
|
||||
hiscore,
|
||||
status
|
||||
)
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- Draw
|
||||
--------------------------------------------------
|
||||
|
||||
local function draw()
|
||||
|
||||
cv:fill(0x101418)
|
||||
|
||||
|
||||
for x = 1, 4 do
|
||||
|
||||
for y = 1, 4 do
|
||||
|
||||
local px = (x - 1) * CELL
|
||||
local py = (y - 1) * CELL
|
||||
|
||||
local v = grid[x][y]
|
||||
|
||||
|
||||
cv:rect(
|
||||
px + 2,
|
||||
py + 2,
|
||||
CELL - 4,
|
||||
CELL - 4,
|
||||
tile_color(v),
|
||||
true,
|
||||
4
|
||||
)
|
||||
|
||||
|
||||
if v > 0 then
|
||||
|
||||
local size = text_size(v)
|
||||
|
||||
cv:text(
|
||||
text_x(px, v, size),
|
||||
text_y(py, size),
|
||||
tostring(v),
|
||||
text_color(v),
|
||||
size
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- End messages
|
||||
--------------------------------------------------
|
||||
|
||||
if won then
|
||||
|
||||
cv:text(
|
||||
30,
|
||||
px_h / 2,
|
||||
"YOU WIN!",
|
||||
C.good,
|
||||
18
|
||||
)
|
||||
|
||||
elseif over then
|
||||
|
||||
cv:text(
|
||||
30,
|
||||
px_h / 2,
|
||||
"GAME OVER",
|
||||
C.bad,
|
||||
18
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------
|
||||
-- WADAMESH API
|
||||
--------------------------------------------------
|
||||
|
||||
function app.on_open(w, h)
|
||||
|
||||
hiscore = store.get(
|
||||
"2048_best",
|
||||
0
|
||||
)
|
||||
|
||||
|
||||
px_w = CELL * 4
|
||||
px_h = CELL * 4
|
||||
|
||||
|
||||
score_lbl = ui.label(
|
||||
"",
|
||||
4,
|
||||
4,
|
||||
14,
|
||||
C.text
|
||||
)
|
||||
|
||||
|
||||
cv = ui.canvas(
|
||||
px_w,
|
||||
px_h
|
||||
)
|
||||
|
||||
|
||||
cv:pos(
|
||||
math.floor(
|
||||
(w - px_w) / 2
|
||||
),
|
||||
28
|
||||
)
|
||||
|
||||
|
||||
reset()
|
||||
|
||||
scoreline()
|
||||
draw()
|
||||
|
||||
|
||||
timer.every(100)
|
||||
|
||||
end
|
||||
|
||||
|
||||
function app.on_input(ev)
|
||||
|
||||
if ev.type == "swipe" then
|
||||
|
||||
move(ev.dir)
|
||||
|
||||
scoreline()
|
||||
draw()
|
||||
|
||||
|
||||
elseif ev.type == "down"
|
||||
and (over or won)
|
||||
then
|
||||
|
||||
reset()
|
||||
|
||||
scoreline()
|
||||
draw()
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
function app.on_tick(dt)
|
||||
|
||||
end
|
||||
|
||||
|
||||
function app.on_close()
|
||||
|
||||
end
|
||||
|
||||
|
||||
return app
|
||||
+31
-4
@@ -13,9 +13,20 @@ so somebody has to read it first.
|
||||
## What an app is
|
||||
|
||||
One Lua file plus a small manifest. Apps talk to the firmware through the `wada.*`
|
||||
API — `wada.ui` (widgets, colours), `wada.sys`, `wada.store` (persistence),
|
||||
`wada.timer`. There is no arbitrary filesystem or network access; the API is the
|
||||
whole surface, which is what makes reviewing tractable.
|
||||
API — `wada.ui` (widgets, colours, a text prompt), `wada.sys`, `wada.store`
|
||||
(persistence), `wada.timer`, and on the larger boards `wada.fs`, `wada.net`,
|
||||
`wada.crypto` and the writable half of `wada.mesh`. Supported boards also expose
|
||||
asynchronous WAV/MP3 playback through `wada.audio`; plain filenames come from the
|
||||
app sandbox on internal flash, SD, or SD_MMC. Read-only physical-SD directory
|
||||
metadata is available through `wada.sd`, and direct card playback uses an explicit
|
||||
`sd:/...` path. There is no general-purpose filesystem or network access; the API
|
||||
is the whole surface, which is what makes reviewing tractable. It is documented at
|
||||
[wadamesh.com/sdk.html](https://wadamesh.com/sdk.html).
|
||||
|
||||
Two numbers worth knowing before you start: every callback runs under a
|
||||
**100,000 Lua instruction** budget, and Lua here is built with **32-bit numbers**,
|
||||
so its floats are single precision. If you are logging coordinates, use the
|
||||
`lat_e6` / `lon_e6` integers rather than `lat` / `lon`.
|
||||
|
||||
Apps run on **every** board that has the Apps drawer, from the 2 MB-PSRAM Heltec
|
||||
V4 to the 800×480 Tanmatsu. Do not hard-code pixel sizes — read the screen from
|
||||
@@ -34,12 +45,18 @@ deploy/apps/<id>/<ver>/<id>.lua the app
|
||||
The manifest is one line:
|
||||
|
||||
```json
|
||||
{"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.","icon":"game"}
|
||||
```
|
||||
|
||||
`desc` is what people read in the store listing before installing, so make it say
|
||||
what the app *does*. One sentence.
|
||||
|
||||
`icon` is optional and picks the drawer tile's glyph by NAME — an app cannot
|
||||
ship its own artwork, so anything unrecognised falls back to the generic app
|
||||
symbol. Choose from: `gps` / `compass` / `map`, `radio`, `signal`, `chart`,
|
||||
`list`, `message`, `person`, `group`, `bell`, `star`, `search`, `settings`,
|
||||
`battery`, `game`.
|
||||
|
||||
**Version directories are immutable.** Once `1.0/` is published it is never edited
|
||||
— a change ships as `1.1/`. Devices cache by version, so editing in place means
|
||||
some users silently run different code from others under the same version number.
|
||||
@@ -49,6 +66,11 @@ some users silently run different code from others under the same version number
|
||||
1. Fork `ALLFATHER-BV/wadamesh` and branch off `main`.
|
||||
2. Add your `deploy/apps/<id>/<ver>/` directory with the two files.
|
||||
3. Add or update your app's row in `deploy/apps/apps.json` (the store catalog).
|
||||
Add `"seed": false` to that row if your app should be downloadable but not
|
||||
compiled into the firmware. Apps in the catalog are baked into the image as
|
||||
built-ins for boards that cannot reach the Store, and that is flash spent on
|
||||
the boards with the least of it. A large app, or one that needs hardware
|
||||
those boards do not have, is better left to the Store.
|
||||
4. Open a pull request. In the description, say what the app does, which boards
|
||||
you tested it on, and anything it persists via `wada.store`.
|
||||
|
||||
@@ -84,6 +106,11 @@ people's devices. We read every submission for:
|
||||
has 2 MB and is the floor.
|
||||
- **Radio behaviour** — an app may read counters and statistics. Transmitting, or
|
||||
changing radio parameters, needs a clear reason and an explicit user action.
|
||||
- **Probing** — `wada.mesh.discover()` costs a transmission from the user *and a
|
||||
reply from every node that hears it*, so it spends other people's airtime as
|
||||
well as theirs. The firmware enforces 15 seconds between probes, but a survey
|
||||
app is still expected to probe on a cadence a person chose and to stop when it
|
||||
is not on screen. Probing in a loop for no stated reason gets sent back.
|
||||
- **Where data goes** — anything leaving the device has to be something the user
|
||||
asked for and can see.
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "airtime",
|
||||
"name": "Airtime",
|
||||
"ver": "1.4",
|
||||
"desc": "Utilization bars with a % axis, rx/tx split, duty ceiling, budget. Reset button top-right."
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
-- Airtime (Lua) — channel utilization over time: live + average bars with a %
|
||||
-- axis, rx/tx air split, duty ceiling, tx budget and packet counters.
|
||||
local ui, sys, mesh, timer = wada.ui, wada.sys, wada.mesh, wada.timer
|
||||
-- Translate through the device's language table (wada.sys.tr, added for #257).
|
||||
-- Older firmware has no sys.tr, so fall back to the English string rather than error.
|
||||
local tr = sys.tr or function(x) return x end
|
||||
local C = ui.colors
|
||||
local app = {}
|
||||
|
||||
local W, narrow
|
||||
local head, sub, legend, detail, chart
|
||||
local base, t0, last
|
||||
local peak_pct = 0
|
||||
|
||||
local function restart_window()
|
||||
local st = mesh.stats()
|
||||
base = { rx = st.rx_air_s, tx = st.tx_air_s }
|
||||
t0 = sys.millis()
|
||||
last = { ms = t0, rx = st.rx_air_s, tx = st.tx_air_s }
|
||||
peak_pct = 0
|
||||
chart:fill(1, 0)
|
||||
chart:fill(2, 0)
|
||||
head:set(tr("sampling..."))
|
||||
head:color(C.accent)
|
||||
sys.toast(tr("window restarted"), 900)
|
||||
end
|
||||
|
||||
local function refresh()
|
||||
local st = mesh.stats()
|
||||
local now = sys.millis()
|
||||
local wall = (now - t0) / 1000
|
||||
if wall < 0.5 then return end
|
||||
|
||||
local rx_air, tx_air = st.rx_air_s - base.rx, st.tx_air_s - base.tx
|
||||
local avg = math.min(100, (rx_air + tx_air) * 100 / wall)
|
||||
|
||||
local dw = (now - last.ms) / 1000
|
||||
local live = 0
|
||||
if dw > 0 then
|
||||
live = math.min(100, ((st.rx_air_s - last.rx) + (st.tx_air_s - last.tx)) * 100 / dw)
|
||||
end
|
||||
last = { ms = now, rx = st.rx_air_s, tx = st.tx_air_s }
|
||||
if live > peak_pct then peak_pct = live end
|
||||
|
||||
chart:push(1, math.floor(live))
|
||||
chart:push(2, math.floor(avg))
|
||||
|
||||
head:set(string.format(tr("channel busy %.1f%%"), live))
|
||||
head:color(live > 50 and C.bad or live > 20 and 0xE8A33D or C.good)
|
||||
sub:set(string.format(tr("avg %.1f%% peak %.1f%% over %ds"), avg, peak_pct, math.floor(wall)))
|
||||
if narrow then
|
||||
detail:set(string.format("air rx %ds / tx %ds\nduty %d%% budget %dms\ntx %d rx %d err %d",
|
||||
rx_air, tx_air, st.duty_pct, st.tx_budget_ms, st.tx_pkts, st.rx_pkts, st.rx_err))
|
||||
else
|
||||
detail:set(string.format("air rx %ds / tx %ds duty %d%% budget %dms\ntx %d rx %d err %d drop %d SF%d",
|
||||
rx_air, tx_air, st.duty_pct, st.tx_budget_ms, st.tx_pkts, st.rx_pkts, st.rx_err, st.rx_dropped, st.sf))
|
||||
end
|
||||
end
|
||||
|
||||
function app.on_open(w, h)
|
||||
W, narrow = w, w < 280
|
||||
local LH12, LH16 = ui.text_h(12), ui.text_h(16)
|
||||
|
||||
local y = 2
|
||||
head = ui.label(tr("sampling..."), 4, y, 16, C.accent); head:width(w - 86)
|
||||
ui.button(tr("Reset"), w - 76, y, 70, 30, function() restart_window() end) -- top-right
|
||||
y = y + LH16 + 4
|
||||
sub = ui.label("", 4, y, 12, C.sub); sub:width(w - 10)
|
||||
y = y + LH12 + 5
|
||||
|
||||
local AX = 34
|
||||
local det_lines = narrow and 3 or 2
|
||||
local ch_h = h - y - (det_lines * LH12) - LH12 - 14
|
||||
if ch_h < 48 then ch_h = 48 end
|
||||
if ch_h > 110 then ch_h = 110 end
|
||||
chart = ui.chart(w - AX - 8, ch_h, 60, C.accent, 0x4F9DF7, true)
|
||||
chart:pos(AX, y)
|
||||
chart:range(0, 100)
|
||||
chart:axis(3, AX)
|
||||
y = y + ch_h + 4
|
||||
|
||||
legend = ui.label(tr("teal = now blue = average"), 4, y, 12, C.sub); legend:width(w - 10)
|
||||
y = y + LH12 + 2
|
||||
detail = ui.label("", 4, y, 12, C.text); detail:width(w - 10)
|
||||
|
||||
local st = mesh.stats()
|
||||
base = { rx = st.rx_air_s, tx = st.tx_air_s }
|
||||
t0 = sys.millis()
|
||||
last = { ms = t0, rx = st.rx_air_s, tx = st.tx_air_s }
|
||||
timer.every(1000)
|
||||
end
|
||||
|
||||
function app.on_tick(dt) refresh() end
|
||||
|
||||
return app
|
||||
+55
-6
@@ -1,6 +1,55 @@
|
||||
{"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":"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)."}
|
||||
]}
|
||||
{
|
||||
"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.4",
|
||||
"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": "sdktest",
|
||||
"name": "SDK Test",
|
||||
"ver": "1.7",
|
||||
"desc": "Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, private file access, audio API, read-only SD listing, crypto against published RFC vectors, channel discovery, and buttons that TRANSMIT test messages."
|
||||
},
|
||||
{
|
||||
"id": "2048",
|
||||
"name": "2048",
|
||||
"ver": "1.2",
|
||||
"desc": "The classic sliding-tile puzzle. Swipe to move, tap to restart. Keeps your best score. Contributed by pisti87."
|
||||
},
|
||||
{
|
||||
"id": "wardrive",
|
||||
"name": "Wardrive",
|
||||
"ver": "1.1",
|
||||
"desc": "LoRa coverage survey. Probes every 20s and logs each reply with GPS position, altitude and BOTH link directions (how well you heard them, how well they heard you) to a CSV. Reference app for wada.mesh.discover.",
|
||||
"requires": "sdk_ext"
|
||||
},
|
||||
{
|
||||
"id": "nearby",
|
||||
"name": "Nearby",
|
||||
"ver": "1.0",
|
||||
"desc": "Your contacts on a real map, sorted by distance, with bearing and a live frame counter. Reference app for wada.map, wada.ui.list, wada.geo and app.on_packet.",
|
||||
"requires": "sdk_ext"
|
||||
},
|
||||
{
|
||||
"id": "gpscompass",
|
||||
"name": "GPS Compass",
|
||||
"ver": "1.0",
|
||||
"desc": "Compass rose: magnetometer or GPS course, live fix, bearing to contacts",
|
||||
"seed": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"gpscompass","name":"GPS Compass","ver":"1.0","desc":"Compass rose: magnetometer or GPS course, live fix, bearing to contacts","icon":"gps"}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+128
-4
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: bg
|
||||
# ver: 8
|
||||
# ver: 20
|
||||
# name: Български
|
||||
# base: bg
|
||||
(device behind) (устройството изостава)
|
||||
@@ -184,6 +184,7 @@ Contact added Контактът е добавен
|
||||
Contact deleted Контактът е изтрит
|
||||
Contact gone Контактът изчезна
|
||||
Contact missing Контактът липсва
|
||||
Contact shared Контактът е споделен
|
||||
Contacts Контакти
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Контактите са във вътрешната памет - SD картата не се монтира при старт. Постави картата отново и рестартирай.
|
||||
Contacts are on internal flash. Контактите са във вътрешната памет.
|
||||
@@ -282,6 +283,7 @@ End time Краен час
|
||||
Enter SSID first Първо въведете SSID
|
||||
Enter a name Въведете име
|
||||
Enter a network name Въведете име на мрежа
|
||||
Enter a region name Enter a region name
|
||||
Enter adds a new line Enter добавя нов ред
|
||||
Enter exactly 32 hex characters. Въведете точно 32 hex знака.
|
||||
Enter key sends message Клавишът Enter изпраща съобщението
|
||||
@@ -407,6 +409,7 @@ Keyboard nav: WASDZ on Клавиатурна навиг.: WASDZ вкл.
|
||||
Keyboard nav: off Клавиатурна навиг.: изкл.
|
||||
Keyboard navigation Навигация с клавиатура
|
||||
Keys
|
||||
Known regions Known regions
|
||||
LOS → %s Пряка видимост → %s
|
||||
Landscape (desktop) Пейзаж (десктоп)
|
||||
Landscape set - applies when remote mode starts. Пейзажът е зададен - прилага се при стартиране на отдалечения режим.
|
||||
@@ -533,6 +536,7 @@ No networks found (2.4 GHz only) Няма намерени мрежи (само
|
||||
No onboard GPS Няма вграден GPS
|
||||
No other networks found Не са открити други мрежи
|
||||
No position yet. A cold start needs a clear view of the sky and can take several minutes. Още няма позиция. Студен старт изисква чисто небе и може да отнеме няколко минути.
|
||||
No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details.
|
||||
No reply from %s Няма отговор от %s
|
||||
No reply. Check password, server,\nor device clock (device: %s) Няма отговор. Проверете паролата, сървъра\nили часовника на устройството (устройство: %s)
|
||||
No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. Тук няма запазени плочки.\n\nСвържете се с Wi-Fi (Настройки → Wi-Fi),\nза да изтеглите този район.\nЗапазените плочки остават достъпни офлайн.
|
||||
@@ -641,6 +645,7 @@ Receive unreleased test firmware — newer features, but less tested. The update
|
||||
Recently heard Наскоро чути
|
||||
Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. Форматиране на SD като MESHCOMOD (FAT32)?\nВСИЧКИ данни на картата ще бъдат изтрити.
|
||||
Region & scope Регион и обхват
|
||||
Region list is full Region list is full
|
||||
Region scope (#tag) for this channel.\nBlank = use the default scope. Регионален обхват (#tag) за този канал.\nПразно = обхват по подразбиране.
|
||||
Region scope (#tag), blank = default. Обхват на регион (#tag), празно = по подразбиране.
|
||||
Region scope (#tag, blank = none) Регионален обхват (#tag, празно = няма)
|
||||
@@ -683,6 +688,7 @@ Run setup again Стартирай настройката отново
|
||||
SD card %s SD карта %s
|
||||
SD card %s (hold: format) SD карта %s (задръж: формат)
|
||||
SD card (tap to mount) SD карта (докоснете за монтиране)
|
||||
SD card (unreadable - tap to retry) SD card (unreadable - tap to retry)
|
||||
SD card inserted SD картата е поставена
|
||||
SD card lost - reinsert to recover SD картата е изгубена - поставете я отново
|
||||
SD card remounted SD картата е монтирана отново
|
||||
@@ -695,6 +701,7 @@ SD formatted - %s (MESHCOMOD) SD форматирана - %s (MESHCOMOD)
|
||||
SD is busy - close tools and retry
|
||||
SD removed - card-backed data cannot be saved
|
||||
SD stayed busy - format cancelled, retry
|
||||
SD unreadable - format it as FAT32 on a computer SD unreadable - format it as FAT32 on a computer
|
||||
SF SF
|
||||
SF / CR / TX / AF SF / CR / TX / AF
|
||||
SPIFFS %u/%u KB used SPIFFS %u/%u KB използвани
|
||||
@@ -722,6 +729,8 @@ Saved — applying…
|
||||
Saved — rebooting Запазено — рестарт
|
||||
Saved — reconnecting… Запазено — повторно свързване…
|
||||
Saved: %s\nFlash it from the Launcher. Запазено: %s\nФлашнете го от Launcher.
|
||||
Saved: %s\nReady for offline flashing. Saved: %s\nReady for offline flashing.
|
||||
Saves the latest firmware of the selected channel to the SD card (BINS folder). Saves the latest firmware of the selected channel to the SD card (BINS folder).
|
||||
Saving to SD… %d%% Запис на SD… %d%%
|
||||
Saving to SD… 0% Запис на SD… 0%
|
||||
Scan to open on phone Сканирай, за да отвориш на телефона
|
||||
@@ -770,6 +779,8 @@ Settings Настройки
|
||||
Setup complete Настройката е готова
|
||||
Share QR Сподели QR
|
||||
Share channel Сподели канала
|
||||
Share contact Сподели контакт
|
||||
Share failed Споделянето неуспешно
|
||||
Share location in advert Споделяй местоположението в advert
|
||||
Share secret Сподели ключа
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Споделете 32-символната тайна, за да се присъединят други. Празно = случайна.
|
||||
@@ -818,7 +829,7 @@ TCP on TCP вкл
|
||||
TOUCH BETA TOUCH BETA
|
||||
TX TX
|
||||
Tab hotkeys — tap a row, then press a key Клавиши за раздела — докосни ред, после натисни клавиш
|
||||
Tap Use to switch - the device reboots to apply. Натиснете „Избери“, за да превключите — устройството ще се рестартира.
|
||||
Tap Use to switch - the device reboots to apply. Натиснете "Избери", за да превключите — устройството ще се рестартира.
|
||||
Tap Use to switch - the device reboots to apply. Language files live in /lang on the storage; edit them or add your own. Докосни «Избери» за смяна - устройството се рестартира. Езиковите файлове са в /lang в паметта; редактирай ги или добави свои.
|
||||
Tap a language; the device reboots to apply. Изберете език; устройството ще се рестартира.
|
||||
Taste the rainbow! Вкуси дъгата!
|
||||
@@ -926,6 +937,7 @@ Your own apps Твоите приложения
|
||||
Zero-hop Без препредаване
|
||||
Zero-hop advert sent Zero-hop advert изпратен
|
||||
Zoom: +/- buttons Мащаб: бутони +/-
|
||||
ambiguous (several regions matched) ambiguous (several regions matched)
|
||||
client connected клиент свързан
|
||||
command команда
|
||||
contacts (%d): контакти (%d):
|
||||
@@ -968,8 +980,6 @@ tap to exit full screen докоснете за изход от цял екра
|
||||
zoom мащаб
|
||||
zoom %d мащаб %d
|
||||
~17 dB amp for quiet/remote areas; turn off in noisy spots. усилвател ~17 dB за тихи/отдалечени места; изключи в шумни.
|
||||
© OpenStreetMap © OpenStreetMap
|
||||
© OpenTopoMap © OpenTopoMap
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
@@ -985,3 +995,117 @@ Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
"%s" wants to send messages on the mesh.\n\nThey will be sent as %s and cannot be told apart from messages you typed.\n\nOnly allow this for an app you trust. "%s" wants to send messages on the mesh.\n\nThey will be sent as %s and cannot be told apart from messages you typed.\n\nOnly allow this for an app you trust.
|
||||
%d of %d on map %d of %d on map
|
||||
1-character messages will be ignored 1-character messages will be ignored
|
||||
1-character messages will be shown 1-character messages will be shown
|
||||
All All
|
||||
Allow Allow
|
||||
Ignore 1-character messages Ignore 1-character messages
|
||||
Join w/ password Join w/ password
|
||||
Location no longer shared Location no longer shared
|
||||
Location shared with anyone who asks Location shared with anyone who asks
|
||||
Location shared with chosen contacts only Location shared with chosen contacts only
|
||||
Location shared with this contact Location shared with this contact
|
||||
Location sharing off Location sharing off
|
||||
Max dots Max dots
|
||||
Most mesh spam is a single character, because it is the cheapest message to send. Dropped silently: no chat entry, no notification, no sound. Most mesh spam is a single character, because it is the cheapest message to send. Dropped silently: no chat entry, no notification, no sound.
|
||||
Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone who asks
|
||||
No apps installed. No apps installed.
|
||||
No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh)
|
||||
Not supported on this board Not supported on this board
|
||||
Reconnected to %.40s Reconnected to %.40s
|
||||
Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu.
|
||||
Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password.
|
||||
Share my loc Share my loc
|
||||
Share my location when asked Share my location when asked
|
||||
Stop sharing loc Stop sharing loc
|
||||
not asked yet not asked yet
|
||||
Battery life: gathering data… Battery life: gathering data…
|
||||
Small\nMedium\nLarge\nJumbo Small\nMedium\nLarge\nJumbo
|
||||
"%s" wants to send PRIVATE messages as you.\n\nIt could write to any of your contacts, or post to a room, and the message will look exactly like one you typed.\n\nOnly allow this for an app you trust. "%s" wants to send PRIVATE messages as you.\n\nIt could write to any of your contacts, or post to a room, and the message will look exactly like one you typed.\n\nOnly allow this for an app you trust.
|
||||
Map pan off Map pan off
|
||||
Map pan: arrows pan, Back exits Map pan: arrows pan, Back exits
|
||||
Perf: Perf:
|
||||
Post to channels as me Post to channels as me
|
||||
Powering off… press BOOT to wake Powering off… press BOOT to wake
|
||||
Read channel messages Read channel messages
|
||||
Read private messages Read private messages
|
||||
Send private messages as me Send private messages as me
|
||||
What each installed app is allowed to do. Anything an app sends goes out under your node name and cannot be told apart from a message you typed. Private covers direct messages and room posts, and is kept separate from channels on purpose. Turn one off to take it back. What each installed app is allowed to do. Anything an app sends goes out under your node name and cannot be told apart from a message you typed. Private covers direct messages and room posts, and is kept separate from channels on purpose. Turn one off to take it back.
|
||||
Wi-Fi off, new map areas can't download Wi-Fi off, new map areas can't download
|
||||
App permissions App permissions
|
||||
"%s" wants to send discovery probes.\n\nEach probe asks every node in range to reply, so it uses airtime on the whole local mesh, not just yours.\n\nNothing is sent under your name and no message is transmitted. "%s" wants to send discovery probes.\n\nEach probe asks every node in range to reply, so it uses airtime on the whole local mesh, not just yours.\n\nNothing is sent under your name and no message is transmitted.
|
||||
? ?
|
||||
About / credits About / credits
|
||||
Auth failed Auth failed
|
||||
Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically.
|
||||
Console mode Console mode
|
||||
Console mode (experimental) Console mode (experimental)
|
||||
Direct (0-hop) only Direct (0-hop) only
|
||||
EXPERIMENTAL. Boots into a text console with no graphical interface: type commands, read replies. Frees the memory and processor time the interface uses. Type 'ui' in the console to come back, and if it ever fails to start the device returns here on its own. Toggling reboots. EXPERIMENTAL. Boots into a text console with no graphical interface: type commands, read replies. Frees the memory and processor time the interface uses. Type 'ui' in the console to come back, and if it ever fails to start the device returns here on its own. Toggling reboots.
|
||||
EXPERIMENTAL. Boots into a text console with no graphical interface: type commands, read replies. Frees the memory and processor time the interface uses. Type 'ui' in the console to come back. Toggling reboots. EXPERIMENTAL. Boots into a text console with no graphical interface: type commands, read replies. Frees the memory and processor time the interface uses. Type 'ui' in the console to come back. Toggling reboots.
|
||||
Enter text Enter text
|
||||
Entering console mode - rebooting... Entering console mode - rebooting...
|
||||
Favorite Favorite
|
||||
How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted.
|
||||
Init… Init…
|
||||
Leaving console mode - rebooting... Leaving console mode - rebooting...
|
||||
Link lost Link lost
|
||||
MESH MESH
|
||||
Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed under the Open Database License (ODbL).\n\n Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed under the Open Database License (ODbL).\n\n
|
||||
Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n
|
||||
Name (A-Z) Name (A-Z)
|
||||
Nearest first Nearest first
|
||||
Recent message Recent message
|
||||
Reload tiles in view Reload tiles in view
|
||||
Repeaters Repeaters
|
||||
SIGNAL SIGNAL
|
||||
SSID not found SSID not found
|
||||
Saved networks Saved networks
|
||||
Select to delete Select to delete
|
||||
Send discovery probes Send discovery probes
|
||||
Show contacts Show contacts
|
||||
Show coordinates Show coordinates
|
||||
Show tile z/x/y Show tile z/x/y
|
||||
Starting… Starting…
|
||||
Text console (experimental) Text console (experimental)
|
||||
Tile debug overlay Tile debug overlay
|
||||
chats chats
|
||||
repeaters repeaters
|
||||
rooms rooms
|
||||
sensors sensors
|
||||
tap to cycle off / on / auto tap to cycle off / on / auto
|
||||
#7A7F87 Scanning… nothing has answered yet# #7A7F87 Scanning… nothing has answered yet#
|
||||
Export crash report (%uK) Export crash report (%uK)
|
||||
On-device text browser · no proxy, no tracking On-device text browser · no proxy, no tracking
|
||||
Paste (copy) Paste (copy)
|
||||
Paste (move) Paste (move)
|
||||
avg %.1f%% peak %.1f%% over %ds avg %.1f%% peak %.1f%% over %ds
|
||||
channel busy %.1f%% channel busy %.1f%%
|
||||
sampling... sampling...
|
||||
teal = now blue = average teal = now blue = average
|
||||
window restarted window restarted
|
||||
Filter Filter
|
||||
Hide system files Hide system files
|
||||
Networks Networks
|
||||
Other networks Other networks
|
||||
Show system files Show system files
|
||||
Unfav Unfav
|
||||
Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi…
|
||||
Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots.
|
||||
Other (hidden) network… Other (hidden) network…
|
||||
Paused Paused
|
||||
Scan again Scan again
|
||||
At a glance At a glance
|
||||
At a glance disabled At a glance disabled
|
||||
At a glance enabled At a glance enabled
|
||||
At a glance while locked At a glance while locked
|
||||
Glance only shows when unlocked Glance only shows when unlocked
|
||||
Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
+128
-8
@@ -1,7 +1,7 @@
|
||||
# wadamesh language file
|
||||
# canonical source - translators: edit this file and PR it
|
||||
# code: de
|
||||
# ver: 9
|
||||
# ver: 20
|
||||
# name: Deutsch
|
||||
# base: de
|
||||
(device behind) (Gerät geht nach)
|
||||
@@ -184,6 +184,7 @@ Contact added Kontakt hinzugefügt
|
||||
Contact deleted Kontakt gelöscht
|
||||
Contact gone Kontakt weg
|
||||
Contact missing Kontakt fehlt
|
||||
Contact shared Kontakt geteilt
|
||||
Contacts Kontakte
|
||||
Contacts are on internal flash - the SD card did not mount at boot. Re-seat the card and reboot. Kontakte liegen im internen Flash - die SD-Karte wurde beim Start nicht eingebunden. Karte neu einsetzen und neu starten.
|
||||
Contacts are on internal flash. Kontakte liegen im internen Flash.
|
||||
@@ -282,6 +283,7 @@ End time Endzeit
|
||||
Enter SSID first Zuerst SSID eingeben
|
||||
Enter a name Namen eingeben
|
||||
Enter a network name Netzwerknamen eingeben
|
||||
Enter a region name Enter a region name
|
||||
Enter adds a new line Enter fügt neue Zeile ein
|
||||
Enter exactly 32 hex characters. Genau 32 Hex-Zeichen eingeben.
|
||||
Enter key sends message Enter-Taste sendet Nachricht
|
||||
@@ -407,6 +409,7 @@ Keyboard nav: WASDZ on Tastaturnav.: WASDZ an
|
||||
Keyboard nav: off Tastaturnav.: aus
|
||||
Keyboard navigation Tastaturnavigation
|
||||
Keys Tasten
|
||||
Known regions Known regions
|
||||
LOS → %s Sichtlinie → %s
|
||||
Landscape (desktop) Querformat (Desktop)
|
||||
Landscape set - applies when remote mode starts. Querformat gesetzt - gilt beim Start des Fernmodus.
|
||||
@@ -450,7 +453,6 @@ Login OK (perms %u) Anmeldung OK (Rechte %u)
|
||||
Login failed Anmeldung fehlgeschlagen
|
||||
Logs Protokolle
|
||||
Longitude Längengrad
|
||||
Lua Store Lua Store
|
||||
MHz MHz
|
||||
MQTT bridge MQTT-Brücke
|
||||
Map Karte
|
||||
@@ -533,6 +535,7 @@ No networks found (2.4 GHz only) Keine Netzwerke gefunden (nur 2,4 GHz)
|
||||
No onboard GPS Kein integriertes GPS
|
||||
No other networks found Keine anderen Netzwerke gefunden
|
||||
No position yet. A cold start needs a clear view of the sky and can take several minutes. Noch keine Position. Ein Kaltstart braucht freie Sicht zum Himmel und kann mehrere Minuten dauern.
|
||||
No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details. No regions yet.\n\nAdd a #region to have messages\nscoped to it named in message details.
|
||||
No reply from %s Keine Antwort von %s
|
||||
No reply. Check password, server,\nor device clock (device: %s) Keine Antwort. Prüfe Passwort, Server\noder Geräteuhr (Gerät: %s)
|
||||
No saved map tiles here.\n\nConnect to Wi-Fi (Settings → Wi-Fi)\nto download this area.\nSaved tiles stay available offline. Hier keine gespeicherten Kacheln.\n\nMit Wi-Fi verbinden (Einstellungen → Wi-Fi),\num dieses Gebiet zu laden.\nGespeicherte Kacheln bleiben offline verfügbar.
|
||||
@@ -641,6 +644,7 @@ Receive unreleased test firmware — newer features, but less tested. The update
|
||||
Recently heard Kürzlich gehört
|
||||
Reformat SD as MESHCOMOD (FAT32)?\nALL data on the card will be erased. SD als MESHCOMOD (FAT32) neu formatieren?\nALLE Daten auf der Karte werden gelöscht.
|
||||
Region & scope Region & Reichweite
|
||||
Region list is full Region list is full
|
||||
Region scope (#tag) for this channel.\nBlank = use the default scope. Regionsbereich (#tag) für diesen Kanal.\nLeer = Standardbereich verwenden.
|
||||
Region scope (#tag), blank = default. Regionsbereich (#tag), leer = Standard.
|
||||
Region scope (#tag, blank = none) Regionsbereich (#tag, leer = keiner)
|
||||
@@ -683,18 +687,20 @@ Run setup again Setup erneut ausführen
|
||||
SD card %s SD-Karte %s
|
||||
SD card %s (hold: format) SD-Karte %s (halten: formatieren)
|
||||
SD card (tap to mount) SD-Karte (zum Einbinden tippen)
|
||||
SD card (unreadable - tap to retry) SD card (unreadable - tap to retry)
|
||||
SD card inserted SD-Karte eingelegt
|
||||
SD card lost - reinsert to recover SD-Karte verloren - neu einstecken
|
||||
SD card remounted SD-Karte neu eingebunden
|
||||
SD card removed SD-Karte entfernt
|
||||
SD data is unavailable - contacts and channels cannot be saved until the card is reinserted. SD-Daten nicht verfügbar – Kontakte und Kanäle können erst nach Einsetzen der Karte gespeichert werden.
|
||||
SD data is unavailable - identity, settings, contacts and channels cannot be saved until the card is reinserted. SD-Daten nicht verfügbar – Identität, Einstellungen, Kontakte und Kanäle können erst nach Einsetzen der Karte gespeichert werden.
|
||||
SD data migration is incomplete. Identity and settings remain internal; use Copy internal data to SD to retry. SD-Datenübertragung unvollständig. Identität und Einstellungen bleiben intern; „Interne Daten auf SD kopieren“ zum erneuten Versuch verwenden.
|
||||
SD data migration is incomplete. Identity and settings remain internal; use Copy internal data to SD to retry. SD-Datenübertragung unvollständig. Identität und Einstellungen bleiben intern; "Interne Daten auf SD kopieren" zum erneuten Versuch verwenden.
|
||||
SD download failed: %s SD-Download fehlgeschlagen: %s
|
||||
SD formatted - %s (MESHCOMOD) SD formatiert - %s (MESHCOMOD)
|
||||
SD is busy - close tools and retry SD ist belegt – Tools schließen und erneut versuchen
|
||||
SD removed - card-backed data cannot be saved SD entfernt – Kartendaten können nicht gespeichert werden
|
||||
SD stayed busy - format cancelled, retry SD blieb belegt – Formatierung abgebrochen, erneut versuchen
|
||||
SD unreadable - format it as FAT32 on a computer SD unreadable - format it as FAT32 on a computer
|
||||
SF SF
|
||||
SF / CR / TX / AF SF / CR / TX / AF
|
||||
SPIFFS %u/%u KB used SPIFFS %u/%u KB belegt
|
||||
@@ -722,6 +728,8 @@ Saved — applying… Gespeichert – wird angewendet …
|
||||
Saved — rebooting Gespeichert — Neustart
|
||||
Saved — reconnecting… Gespeichert — verbinde neu…
|
||||
Saved: %s\nFlash it from the Launcher. Gespeichert: %s\nFlashe es über den Launcher.
|
||||
Saved: %s\nReady for offline flashing. Saved: %s\nReady for offline flashing.
|
||||
Saves the latest firmware of the selected channel to the SD card (BINS folder). Saves the latest firmware of the selected channel to the SD card (BINS folder).
|
||||
Saving to SD… %d%% Speichern auf SD… %d%%
|
||||
Saving to SD… 0% Speichern auf SD… 0%
|
||||
Scan to open on phone Scannen, um am Handy zu öffnen
|
||||
@@ -770,6 +778,8 @@ Settings Einstellungen
|
||||
Setup complete Einrichtung abgeschlossen
|
||||
Share QR QR teilen
|
||||
Share channel Kanal teilen
|
||||
Share contact Kontakt teilen
|
||||
Share failed Teilen fehlgeschlagen
|
||||
Share location in advert Standort im Advert teilen
|
||||
Share secret Geheimnis teilen
|
||||
Share the 32-char secret so others can join. Leave the secret empty to generate a random one. Teilen Sie das 32-Zeichen-Geheimnis, damit andere beitreten. Leer lassen für ein zufälliges.
|
||||
@@ -789,7 +799,6 @@ Sleep ready Bereit zum Schlafen
|
||||
Slot %d cleared Slot %d gelöscht
|
||||
Slot empty Slot leer
|
||||
Small\nMedium\nLarge Klein\nMittel\nGroß
|
||||
Snake Snake
|
||||
Snake — swipe or roll the trackball Snake — wischen oder Trackball rollen
|
||||
Sort Sortieren
|
||||
Sound Ton
|
||||
@@ -797,7 +806,6 @@ Sound off Ton aus
|
||||
Sound on Ton ein
|
||||
Sound: %s Ton: %s
|
||||
Start time Startzeit
|
||||
Status Status
|
||||
Step 1 of 3 Schritt 1 von 3
|
||||
Step 2 of 3 Schritt 2 von 3
|
||||
Step 3 of 3 Schritt 3 von 3
|
||||
@@ -810,7 +818,6 @@ Streams the device screen as a live picture you tap to control. Heavier; the pan
|
||||
Switch Wechseln
|
||||
Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot. Auf die externe Antenne wechseln?\n\nErst sicherstellen, dass wirklich eine Antenne an der Buchse steckt. Senden ohne Antenne kann das Funkmodul beschädigen.\n\nFällt bei jedem Neustart auf die interne Antenne zurück.
|
||||
Sync clock from system Uhr synchronisieren
|
||||
System System
|
||||
System info Systeminfo
|
||||
TCP %s BLE %s\nWS clients %d GPS %s Buzzer %s TCP %s BLE %s\nWS-Clients %d GPS %s Summer %s
|
||||
TCP off TCP aus
|
||||
@@ -926,6 +933,7 @@ Your own apps Eigene Apps
|
||||
Zero-hop Ohne Hop
|
||||
Zero-hop advert sent Zero-Hop-Advert gesendet
|
||||
Zoom: +/- buttons Zoom: +/- Tasten
|
||||
ambiguous (several regions matched) ambiguous (several regions matched)
|
||||
client connected Client verbunden
|
||||
command Befehl
|
||||
contacts (%d): Kontakte (%d):
|
||||
@@ -968,8 +976,6 @@ tap to exit full screen tippen zum Verlassen des Vollbilds
|
||||
zoom Zoom
|
||||
zoom %d Zoom %d
|
||||
~17 dB amp for quiet/remote areas; turn off in noisy spots. ~17 dB Verstärker für ruhige/abgelegene Gebiete; in lauten Zonen aus.
|
||||
© OpenStreetMap © OpenStreetMap
|
||||
© OpenTopoMap © OpenTopoMap
|
||||
Scope
|
||||
another region
|
||||
my region
|
||||
@@ -985,3 +991,117 @@ Direct (0-hop)
|
||||
Heard %s
|
||||
Traffic (since boot)\n Sent %u flood, %u direct\n Recv %u flood, %u direct
|
||||
Overwrite oldest non-favorite
|
||||
"%s" wants to send messages on the mesh.\n\nThey will be sent as %s and cannot be told apart from messages you typed.\n\nOnly allow this for an app you trust. "%s" wants to send messages on the mesh.\n\nThey will be sent as %s and cannot be told apart from messages you typed.\n\nOnly allow this for an app you trust.
|
||||
%d of %d on map %d of %d on map
|
||||
1-character messages will be ignored 1-character messages will be ignored
|
||||
1-character messages will be shown 1-character messages will be shown
|
||||
All All
|
||||
Allow Allow
|
||||
Ignore 1-character messages Ignore 1-character messages
|
||||
Join w/ password Join w/ password
|
||||
Location no longer shared Location no longer shared
|
||||
Location shared with anyone who asks Location shared with anyone who asks
|
||||
Location shared with chosen contacts only Location shared with chosen contacts only
|
||||
Location shared with this contact Location shared with this contact
|
||||
Location sharing off Location sharing off
|
||||
Max dots Max dots
|
||||
Most mesh spam is a single character, because it is the cheapest message to send. Dropped silently: no chat entry, no notification, no sound. Most mesh spam is a single character, because it is the cheapest message to send. Dropped silently: no chat entry, no notification, no sound.
|
||||
Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone who asks
|
||||
No apps installed. No apps installed.
|
||||
No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh)
|
||||
Not supported on this board Not supported on this board
|
||||
Reconnected to %.40s Reconnected to %.40s
|
||||
Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu.
|
||||
Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password.
|
||||
Share my loc Share my loc
|
||||
Share my location when asked Share my location when asked
|
||||
Stop sharing loc Stop sharing loc
|
||||
not asked yet not asked yet
|
||||
Battery life: gathering data… Battery life: gathering data…
|
||||
Small\nMedium\nLarge\nJumbo Small\nMedium\nLarge\nJumbo
|
||||
"%s" wants to send PRIVATE messages as you.\n\nIt could write to any of your contacts, or post to a room, and the message will look exactly like one you typed.\n\nOnly allow this for an app you trust. "%s" wants to send PRIVATE messages as you.\n\nIt could write to any of your contacts, or post to a room, and the message will look exactly like one you typed.\n\nOnly allow this for an app you trust.
|
||||
Map pan off Map pan off
|
||||
Map pan: arrows pan, Back exits Map pan: arrows pan, Back exits
|
||||
Perf: Perf:
|
||||
Post to channels as me Post to channels as me
|
||||
Powering off… press BOOT to wake Powering off… press BOOT to wake
|
||||
Read channel messages Read channel messages
|
||||
Read private messages Read private messages
|
||||
Send private messages as me Send private messages as me
|
||||
What each installed app is allowed to do. Anything an app sends goes out under your node name and cannot be told apart from a message you typed. Private covers direct messages and room posts, and is kept separate from channels on purpose. Turn one off to take it back. What each installed app is allowed to do. Anything an app sends goes out under your node name and cannot be told apart from a message you typed. Private covers direct messages and room posts, and is kept separate from channels on purpose. Turn one off to take it back.
|
||||
Wi-Fi off, new map areas can't download Wi-Fi off, new map areas can't download
|
||||
App permissions App permissions
|
||||
"%s" wants to send discovery probes.\n\nEach probe asks every node in range to reply, so it uses airtime on the whole local mesh, not just yours.\n\nNothing is sent under your name and no message is transmitted. "%s" wants to send discovery probes.\n\nEach probe asks every node in range to reply, so it uses airtime on the whole local mesh, not just yours.\n\nNothing is sent under your name and no message is transmitted.
|
||||
? ?
|
||||
About / credits About / credits
|
||||
Auth failed Auth failed
|
||||
Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically.
|
||||
Console mode Console mode
|
||||
Console mode (experimental) Console mode (experimental)
|
||||
Direct (0-hop) only Direct (0-hop) only
|
||||
EXPERIMENTAL. Boots into a text console with no graphical interface: type commands, read replies. Frees the memory and processor time the interface uses. Type 'ui' in the console to come back, and if it ever fails to start the device returns here on its own. Toggling reboots. EXPERIMENTAL. Boots into a text console with no graphical interface: type commands, read replies. Frees the memory and processor time the interface uses. Type 'ui' in the console to come back, and if it ever fails to start the device returns here on its own. Toggling reboots.
|
||||
EXPERIMENTAL. Boots into a text console with no graphical interface: type commands, read replies. Frees the memory and processor time the interface uses. Type 'ui' in the console to come back. Toggling reboots. EXPERIMENTAL. Boots into a text console with no graphical interface: type commands, read replies. Frees the memory and processor time the interface uses. Type 'ui' in the console to come back. Toggling reboots.
|
||||
Enter text Enter text
|
||||
Entering console mode - rebooting... Entering console mode - rebooting...
|
||||
Favorite Favorite
|
||||
How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted.
|
||||
Init… Init…
|
||||
Leaving console mode - rebooting... Leaving console mode - rebooting...
|
||||
Link lost Link lost
|
||||
MESH MESH
|
||||
Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed under the Open Database License (ODbL).\n\n Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed under the Open Database License (ODbL).\n\n
|
||||
Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n
|
||||
Name (A-Z) Name (A-Z)
|
||||
Nearest first Nearest first
|
||||
Recent message Recent message
|
||||
Reload tiles in view Reload tiles in view
|
||||
Repeaters Repeaters
|
||||
SIGNAL SIGNAL
|
||||
SSID not found SSID not found
|
||||
Saved networks Saved networks
|
||||
Select to delete Select to delete
|
||||
Send discovery probes Send discovery probes
|
||||
Show contacts Show contacts
|
||||
Show coordinates Show coordinates
|
||||
Show tile z/x/y Show tile z/x/y
|
||||
Starting… Starting…
|
||||
Text console (experimental) Text console (experimental)
|
||||
Tile debug overlay Tile debug overlay
|
||||
chats chats
|
||||
repeaters repeaters
|
||||
rooms rooms
|
||||
sensors sensors
|
||||
tap to cycle off / on / auto tap to cycle off / on / auto
|
||||
#7A7F87 Scanning… nothing has answered yet# #7A7F87 Scanning… nothing has answered yet#
|
||||
Export crash report (%uK) Export crash report (%uK)
|
||||
On-device text browser · no proxy, no tracking On-device text browser · no proxy, no tracking
|
||||
Paste (copy) Paste (copy)
|
||||
Paste (move) Paste (move)
|
||||
avg %.1f%% peak %.1f%% over %ds avg %.1f%% peak %.1f%% over %ds
|
||||
channel busy %.1f%% channel busy %.1f%%
|
||||
sampling... sampling...
|
||||
teal = now blue = average teal = now blue = average
|
||||
window restarted window restarted
|
||||
Filter Filter
|
||||
Hide system files Hide system files
|
||||
Networks Networks
|
||||
Other networks Other networks
|
||||
Show system files Show system files
|
||||
Unfav Unfav
|
||||
Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi…
|
||||
Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots.
|
||||
Other (hidden) network… Other (hidden) network…
|
||||
Paused Paused
|
||||
Scan again Scan again
|
||||
At a glance At a glance
|
||||
At a glance disabled At a glance disabled
|
||||
At a glance enabled At a glance enabled
|
||||
At a glance while locked At a glance while locked
|
||||
Glance only shows when unlocked Glance only shows when unlocked
|
||||
Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked
|
||||
Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction
|
||||
N/A N/A
|
||||
This board cannot run this app. This board cannot run this app.
|
||||
Signal Signal
|
||||
Signal (stale) Signal (stale)
|
||||
nothing heard yet nothing heard yet
|
||||
Loaded 100 of 254 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user