Compare commits
No files matched your search
@@ -0,0 +1,75 @@
|
||||
name: Bug report
|
||||
description: Something in the firmware does not work. Best opened from the device, which fills most of this in for you.
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**From your device:** Settings, About, then "Report a bug".
|
||||
The device shows a QR code that opens this form with your board, firmware
|
||||
version and diagnostics already filled in, so you only type what went wrong.
|
||||
|
||||
- type: input
|
||||
id: board
|
||||
attributes:
|
||||
label: Board
|
||||
description: Which device, and which revision if you know it.
|
||||
placeholder: LilyGo T-Deck Plus
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Firmware version
|
||||
description: Settings, About. A beta number such as beta_84, or the stable version.
|
||||
placeholder: beta_84
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: What happened
|
||||
description: What you did, what you expected, and what the device did instead.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: How to reproduce it
|
||||
description: >
|
||||
Every step from a known state, starting at a reboot if you can. A bug we
|
||||
can reproduce is usually fixed the same day; one we cannot often waits
|
||||
for a crash dump.
|
||||
placeholder: |
|
||||
1. Open Contacts
|
||||
2. Press down past the last row
|
||||
3. The device reboots
|
||||
|
||||
- type: input
|
||||
id: diag
|
||||
attributes:
|
||||
label: Diagnostics
|
||||
description: >
|
||||
Filled in by the device. Free heap, storage backend and similar. Leave it
|
||||
alone if you opened this form from the QR code.
|
||||
|
||||
- type: textarea
|
||||
id: extra
|
||||
attributes:
|
||||
label: Crash report or screenshot
|
||||
description: >
|
||||
If the device saved a crash report, attach it: Files, or USB Files at
|
||||
files.wadamesh.com. A crash report turns a guess into a line number.
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Before you send
|
||||
options:
|
||||
- label: I am on the newest build, or I have said which build I am on above
|
||||
required: true
|
||||
- label: I have looked for an existing issue about this
|
||||
required: false
|
||||
@@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Ask on Discord
|
||||
url: https://discord.gg/T8X2UaQ7Sm
|
||||
about: Questions, help with flashing, and anything you are not sure is a bug.
|
||||
- name: User guide
|
||||
url: https://wadamesh.com/docs.html
|
||||
about: How the firmware works, board by board.
|
||||
- name: Beta test matrix
|
||||
url: https://wadamesh.com/beta.html
|
||||
about: Which boards have been vouched for on the current test build, and how to report yours.
|
||||
@@ -46,3 +46,6 @@ CLAUDE.md
|
||||
# device's AppFS partition and is both large and device-specific.
|
||||
tanmatsu/tools/appfs-src/
|
||||
tanmatsu/tools/dev_appfs.bin
|
||||
# Generated by scripts/build/gen-changelog.py at every site deploy
|
||||
deploy/site/changelog.html
|
||||
src/ui-touch/doc_radio_apps.h
|
||||
+105
-4
@@ -9,13 +9,17 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
|
||||
| Device | MCU / radio | Display and input | Install | Channel | Status |
|
||||
|---|---|---|---|---|---|
|
||||
| LilyGo T-Deck / T-Deck Plus | ESP32-S3, SX1262 | 2.8" 320x240 touch, QWERTY, trackball | Web flasher (standalone) or Launcher app image | Stable | Fully supported, reference device |
|
||||
| LilyGo T-Deck Pro | ESP32-S3, SX1262 | 3.1" 240x320 e-paper touch, TCA8418 QWERTY | Development build only | Experimental | Initial target; display, touch, keyboard, radio, GPS and microSD validation pending (#62) |
|
||||
| LilyGo T-Deck Max | ESP32-S3, SX1262 | 3.1" 240x320 e-paper touch, TCA8418 QWERTY | Web flasher | Experimental (new in beta_84) | The Pro's hardware with the peripheral power rails and resets on an XL9555 expander and a BQ27220 fuel gauge; contributed and tested by the author of PR #545 |
|
||||
| Heltec V4 + TFT | ESP32-S3, SX1262 | 2.4" 240x320 touch (CHSC6x) | Web flasher | Stable | Fully supported; Expansion Kit sensors, V4.3 high-gain RX toggle |
|
||||
| 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 |
|
||||
| Elecrow CrowPanel Advance 3.5 | ESP32-S3, external SX1262 | 3.5" ILI9488 480x320 touch (GT911) | Web flasher (beta) | Beta, hardware validation pending | Requires an SX1262 module in the LoRa expansion slot; not for other CrowPanel models |
|
||||
| Elecrow CrowPanel Advance 5.0 | ESP32-S3, external SX1262 | 5" ST7262 RGB 800x480 touch (GT911) | Web flasher (beta) | Beta, hardware-tested (display, touch, radio) | V1.1+ boards; SX1262 in the wireless slot with the function DIP switch on the wireless module; no SD, GPS or battery gauge |
|
||||
| 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 |
|
||||
| LilyGo T-Display P4 | ESP32-P4 + ESP32-C6, SX1262 or LR2021 | AMOLED or TFT-LCD, touch | Web flasher | Beta (new in beta_45) | Four panel/radio builds; AMOLED + SX1262 hardware-tested, the other combinations need validation |
|
||||
| Attaky Mesh Series | ESP32-S3, SX1262 | Touch display + front D-pad | Web flasher | Beta (new in beta_47) | Community port by attakygit (#158/#169); detachable keyboard supported; the front D-pad + SELECT navigate the UI |
|
||||
|
||||
## Feature notes per board
|
||||
@@ -31,9 +35,31 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
|
||||
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.
|
||||
- **T-Deck Pro**: separate `LilyGo_TDeck_Pro_v1_0_companion_radio_touch` and
|
||||
`LilyGo_TDeck_Pro_v1_1_companion_radio_touch` targets for the incompatible PCB
|
||||
revisions. V1.0 has no frontlight and uses GPIO45 for touch reset; V1.1 uses
|
||||
GPIO45 for its frontlight and GPIO38 for touch reset. Flashing the V1.1 image
|
||||
onto V1.0 holds touch in reset. Both use the GDEQ031T10 e-paper panel,
|
||||
CST328/CST3530 touch detection, and the Pro-specific TCA8418 matrix. See
|
||||
[variants/lilygo_tdeck_pro/PORT.md](variants/lilygo_tdeck_pro/PORT.md) before
|
||||
choosing a target.
|
||||
- **T-Deck Max**: `LilyGo_TDeck_Max_companion_radio_touch`. The same e-paper,
|
||||
keyboard and radio as the Pro, whose display and touch drivers it reuses; the
|
||||
differences are an XL9555 expander in front of the peripheral power enables and
|
||||
resets, a BQ27220 fuel gauge, and the three capacitive pads under the glass.
|
||||
See variants/lilygo_tdeck_max/PORT.md.
|
||||
- **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.
|
||||
buzzer. On the original Expansion Kit, tap **IO** for Back or hold it for one
|
||||
second to return Home; this reuses GPIO35, so that target no longer drives the
|
||||
same line as a TX LED. **PWR** remains the expansion board's hardware power
|
||||
control and is not readable by firmware. V4.3 boards get the switchable
|
||||
high-gain receive LNA toggle. Both V4 targets auto-detect an optional M5Stack
|
||||
CardKB at address `0x5F` on the board I2C bus (GPIO4/3 on V4, GPIO17/18 on
|
||||
V4-R8). It provides text and focus navigation; the on-screen keyboard remains
|
||||
available when CardKB is absent or when the active field is tapped again.
|
||||
V4-R8 also offers font-only Normal, Large and Huge text presets under Display;
|
||||
controls retain their normal geometry so the compact screen stays navigable.
|
||||
- **Tanmatsu**: keyboard-driven UI (no touchscreen) with the coloured function
|
||||
keys mapped to tabs, ALT accent picker, UI scaling (Normal/Large/Huge),
|
||||
microSD for all persistent data. Ships through the Tanmatsu launcher store,
|
||||
@@ -57,12 +83,87 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
|
||||
validated at boot; if it reports lost integrity, the same optional saved-Wi-Fi
|
||||
cold-boot sync offered on T-Deck is available in Clock settings. New in beta_38;
|
||||
report anything that feels off.
|
||||
- **T-Display P4**: touch UI with the on-screen keyboard. The BOOT button wakes
|
||||
the screen (or lights the lock screen) and holding it for two seconds locks or
|
||||
unlocks, as the Wio Tracker L2's wake button does; "Lock when screen off" is
|
||||
under Settings, Lock screen. The optional clip-on
|
||||
keyboard expansion can be attached or removed while running and is picked up
|
||||
within a couple of seconds; while it is on, it types into the focused field
|
||||
and the on-screen keyboard stays down (tap the field again to bring it up).
|
||||
Arrows move the caret or the focus highlight, Esc goes back, F1-F5 open the
|
||||
tabs left to right, F11 steps the keyboard light through off, low, medium and
|
||||
high (the level is kept across reboots), Caps Lock lights its LEDs,
|
||||
and Shift, Sym and Alt apply to the next key. Alt+Backspace goes back.
|
||||
- **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.
|
||||
- **Seeed Wio Tracker L2**: pre-release touch target under active bring-up. Builds are for
|
||||
development and hardware validation only; no public release artifact is
|
||||
promised until the port is verified.
|
||||
promised until the port is verified. It has no battery-backed clock, so the
|
||||
same optional saved-Wi-Fi cold-boot time sync offered on T-Deck is available
|
||||
in Clock settings.
|
||||
- **CrowPanel Advance 3.5**: `crowpanel_35_companion_radio_touch` is a
|
||||
16 MB flash / 8 MB OPI PSRAM build for the ILI9488/GT911 panel and an SX1262
|
||||
in the expansion slot. It starts upright in 480x320 landscape; Display
|
||||
settings can switch to 320x480 portrait, rotated 180 degrees. The selected
|
||||
orientation persists across reboots; landscape is only the default when no
|
||||
saved choice exists.
|
||||
It uses the same compact font and control sizing as Wio Tracker L2.
|
||||
Fit the radio before booting. GPIO0 is shared by BOOT and the radio's chip
|
||||
select, so BOOT is not configured as a wake/unlock input on stock wiring.
|
||||
Screen-off remains touch-wakeable; hard lock is disabled without a separate
|
||||
unlock button. See the
|
||||
[official schematic](https://github.com/Elecrow-RD/CrowPanel-Advance-HMI-ESP32-AI-Display/blob/master/3.5/schematic/ESP32%20Display%203.5%20inch%20V1.0.sch)
|
||||
and [reference radio pin map](https://github.com/meshtastic/firmware/blob/master/variants/esp32s3/elecrow_panel/variant.h).
|
||||
An optional external GPS can be connected to UART1 (CrowPanel RX GPIO18 /
|
||||
TX GPIO17, 9600 baud). Data
|
||||
can use a FAT32 microSD card for data, offline maps, File Manager and browser
|
||||
Transfer. The card uses software SPI (SCLK 5, MISO 4, MOSI 6, CS 7), leaving
|
||||
the display and radio SPI hosts untouched; missing or unreadable cards fall
|
||||
back to SPIFFS. The dedicated LittleFS partition remains the internal tile
|
||||
cache. Cards are never auto-formatted; format them on a computer. Serial
|
||||
uses the board's USB-to-UART bridge. Battery voltage monitoring is not
|
||||
supported. Display, touch, radio and SD still need validation on a physical
|
||||
CrowPanel. Like Wio Tracker L2, it is included in the all-target builds and local beta release
|
||||
scripts, not the two-board stable CI workflow. Select it in the upload
|
||||
helper with `scripts/build-upload-monitor.sh --crowpanel-35`.
|
||||
- **CrowPanel Advance 5.0**: `crowpanel_50_companion_radio_touch` is a
|
||||
16 MB flash / 8 MB OPI PSRAM build for the 800x480 ST7262 RGB panel, the
|
||||
GT911 touch controller and an SX1262 in the wireless-module slot (J8/J9). The
|
||||
board quirks follow Camillia-MT's port:
|
||||
- The panel is driven over the 16-bit RGB bus from a PSRAM framebuffer at a
|
||||
14 MHz pixel clock (Elecrow's demo uses 21 MHz); the slower clock leaves
|
||||
PSRAM bandwidth for WiFi and LVGL so the picture does not drift or tear.
|
||||
LVGL's draw band stays in internal RAM for the same reason.
|
||||
- Flash writes (saving chats and settings) take the bus that the panel's DMA
|
||||
shares with PSRAM, which made the picture jump sideways. The firmware stops
|
||||
the LCD clock for each flash write or erase, so the screen briefly freezes
|
||||
or flickers instead.
|
||||
- Backlight and touch reset sit behind an I2C controller on GPIO15/16 that
|
||||
changed between revisions: a PCA9557 at 0x18 on V1.0, a board MCU at 0x30
|
||||
from V1.1. The driver detects which one answers, resets the GT911 through
|
||||
it and accepts the touch controller at either 0x5D or 0x14. Below full
|
||||
brightness the MCU dims by PWM, which can whine; 100% is steady.
|
||||
- The function DIP switch on the back routes GPIO 2/4/5/6/19/20 to the
|
||||
wireless slot, the microSD slot or the speaker/microphone. Set it to the
|
||||
wireless module (S1=0, S0=1), or the radio is not found (the serial log
|
||||
says so). The microSD slot is therefore unusable; browser Transfer uses the
|
||||
internal tiles partition instead, as on the Attaky.
|
||||
- The radio's chip select is GPIO8 on V1.2/V1.3 boards (current production)
|
||||
and GPIO0 on V1.0. The build targets GPIO8; a V1.0 board is detected at boot
|
||||
and logged, and needs `P_LORA_NSS=0`.
|
||||
- No GPS (no free GPIO pair for a UART; 17/18 are panel data), no battery
|
||||
voltage sensing and no free user button, so hard lock is disabled and the
|
||||
screen wakes by touch.
|
||||
It starts in native 800x480 landscape; Display settings can switch to
|
||||
480x800 portrait. It uses the large-screen layouts and UI-size presets of the
|
||||
Tanmatsu. Serial uses the board's CH340 USB-to-UART bridge. Tested on a physical
|
||||
unit: a steady picture with smooth gradients, responsive typing with key
|
||||
highlighting, the chat header and composer, and sending and receiving messages
|
||||
over the radio (flash writes show as a brief freeze or flicker), and a fresh
|
||||
full install and an on-device update, whose long flash erases the panel
|
||||
recovers from. Select it in
|
||||
the upload helper with `scripts/build-upload-monitor.sh --crowpanel-50`.
|
||||
|
||||
## Requested boards
|
||||
|
||||
@@ -70,7 +171,7 @@ Open hardware requests, roughly in demand order. Ports are welcome, see
|
||||
[CONTRIBUTING.md](CONTRIBUTING.md); the M9 and Tap V2 both started as community
|
||||
PRs.
|
||||
|
||||
- LilyGo T-Deck Pro Max: [#62](https://github.com/ALLFATHER-BV/wadamesh/issues/62)
|
||||
- LilyGo T-Deck Pro hardware validation / T-Deck Max variants: [#62](https://github.com/ALLFATHER-BV/wadamesh/issues/62)
|
||||
- SenseCAP Indicator D1L: [#14](https://github.com/ALLFATHER-BV/wadamesh/issues/14)
|
||||
|
||||
## Channels
|
||||
|
||||
@@ -53,6 +53,9 @@ We do NOT bind LVGL wholesale. We bind a stable, versioned surface we own
|
||||
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).
|
||||
- `wada.widget` (extended SDK) — `set{title, value, line, bar, spark}` publishes the app's
|
||||
Home widget (at most once a second), `clear()` empties it. The last one is kept after the
|
||||
app closes and written to `<root>/apps/<id>.wgt` at teardown; nothing runs headless.
|
||||
|
||||
Sandbox env (no `io`, `os`, `require`, `dofile`, `load` of new chunks;
|
||||
whitelisted stdlib: `math`, `string`, `table`, `pairs/ipairs/select/pcall/
|
||||
|
||||
@@ -91,5 +91,18 @@ driver, variants/tdisplay_p4/P4Audio.cpp)
|
||||
LovyanGFX (RAK WisMesh Tap V2 display backend)
|
||||
Copyright (c) lovyan03 — FreeBSD License — https://github.com/lovyan03/LovyanGFX
|
||||
|
||||
Camillia-MT (CrowPanel Advance 3.5 software-SPI microSD backend)
|
||||
Copyright (c) 2025-2026 Michael A. Cojocari and contributors
|
||||
GNU General Public License v3.0 or later
|
||||
Adapted from src/sd_soft_spi.cpp; integrated with WadaMesh's filesystem
|
||||
API and concurrent storage consumers.
|
||||
|
||||
Camillia-MT (CrowPanel Advance 5.0 display, backlight and touch bring-up)
|
||||
Copyright (c) 2025-2026 Michael A. Cojocari and contributors
|
||||
GNU General Public License v3.0 or later
|
||||
The RGB panel configuration, revision detection, backlight controller and
|
||||
GT911 reset in variants/crowpanel_50/CrowPanel50Display.cpp are adapted
|
||||
from src/hal/crowpanel_50_display.h and src/hal/hw_crowpanel_50.h.
|
||||
|
||||
This list is not exhaustive and is expanded as dependencies are vendored or
|
||||
added.
|
||||
@@ -8,10 +8,10 @@
|
||||
<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**, **Heltec V4 + TFT** and eight other boards
|
||||
firmware for the **LilyGo T-Deck / T-Deck Plus**, **Heltec V4 + TFT** and other boards
|
||||
(ESP32-S3).
|
||||
|
||||
An LVGL touch UI — map, chat, contacts, channels, settings — split out of
|
||||
An LVGL touch UI (map, chat, contacts, channels, settings) split out of
|
||||
[meshcomod](https://github.com/ALLFATHER-BV/meshcomod). The app depends on a
|
||||
MeshCore fork via PlatformIO `lib_deps`.
|
||||
|
||||
@@ -20,21 +20,23 @@ MeshCore fork via PlatformIO `lib_deps`.
|
||||
See **[DEVICES.md](DEVICES.md)** for the full support matrix, install paths and
|
||||
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 — [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)
|
||||
- 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; [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 + SX1262 by default, with `WADA_P4_LCD=1` for TFT-LCD and `WADA_P4_LR2021=1` for LR2021 (beta)
|
||||
- Attaky Mesh Series: env `attaky_mesh_series_companion_radio_touch` (beta)
|
||||
- Elecrow CrowPanel Advance 3.5: env `crowpanel_35_companion_radio_touch` (beta, hardware validation pending; requires an SX1262 in the expansion slot)
|
||||
- Elecrow CrowPanel Advance 5.0: env `crowpanel_50_companion_radio_touch` (beta, hardware validation pending; requires an SX1262 in the wireless slot and the DIP switch set to it)
|
||||
|
||||
## Architecture
|
||||
|
||||
This repo holds only the **app**: the `companion_radio` glue, the `ui-touch`
|
||||
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
|
||||
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-*`
|
||||
git tag. The touch-app files this repo owns (TouchPrefsStore, WifiRuntimeStore,
|
||||
@@ -45,6 +47,50 @@ 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).
|
||||
Region discovery, known-region management, and per-channel flood scopes are
|
||||
described in [REGIONS.md](REGIONS.md).
|
||||
|
||||
## Offline map tiles
|
||||
|
||||
Builds with a microSD card expose a **Transfer** app for authenticated browser
|
||||
uploads over the local Wi-Fi network. WadaMesh accepts unpacked 256x256 PNG XYZ
|
||||
tiles in a `z/x/y.png` directory tree.
|
||||
|
||||
To create a compatible pack with [QGIS](https://qgis.org/):
|
||||
|
||||
1. Add a map source whose licence and service terms explicitly permit offline
|
||||
or bulk use. A self-hosted source is also suitable. Do **not** bulk-download
|
||||
from `tile.openstreetmap.org`: the official
|
||||
[OSM tile policy](https://operations.osmfoundation.org/policies/tiles/)
|
||||
prohibits offline downloads from that community service.
|
||||
2. Frame only the area needed in the QGIS map canvas. Tile counts quadruple at
|
||||
every added zoom level, so a small region and a practical range such as
|
||||
zoom 8 through 16 are preferable to exporting an entire country at zoom 19.
|
||||
3. Open **Processing Toolbox → Raster tools → Generate XYZ tiles (Directory)**.
|
||||
4. Use the map-canvas extent, choose minimum and maximum zooms within 3–19,
|
||||
select **PNG**, set tile width and height to **256**, leave **Use inverted
|
||||
tile Y axis (TMS conventions)** disabled, and select an output directory.
|
||||
5. Confirm the output contains paths such as `12/2048/1362.png`. If obtaining
|
||||
an existing pack instead, extract it first and confirm it has the same XYZ
|
||||
directory layout and that its provider permits offline use and redistribution.
|
||||
|
||||
To upload the pack:
|
||||
|
||||
1. Connect the device and the browser to the same Wi-Fi network.
|
||||
2. Open **Transfer** from the device's app drawer, visit the displayed URL, and
|
||||
enter the six-digit session code.
|
||||
3. Choose **Offline OSM map (/tiles)**, select the generated output folder, and
|
||||
start the upload. The browser ignores unrelated files and uploads valid tiles
|
||||
sequentially while preserving their coordinates.
|
||||
4. Existing tiles are skipped by default, allowing an interrupted upload to be
|
||||
resumed. Enable **Replace existing map tiles** when intentionally refreshing
|
||||
a pack.
|
||||
5. Close Transfer after completion. In **Map options**, disable **Topographic
|
||||
map** and enable **Tiles from SD card**. Disable Wi-Fi to verify that the
|
||||
uploaded area renders offline.
|
||||
|
||||
Individual PNG tiles must be no larger than 256 KB. Keep the map provider's
|
||||
required attribution and licence information with any pack you distribute.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -54,28 +100,30 @@ automatically:
|
||||
```bash
|
||||
pio run -e heltec_v4_tft_companion_radio_usb_tcp_touch # Heltec V4 TFT
|
||||
pio run -e LilyGo_TDeck_companion_radio_touch # LilyGo T-Deck
|
||||
# or just `pio run` to build both
|
||||
pio run -e crowpanel_35_companion_radio_touch # CrowPanel Advance 3.5
|
||||
pio run -e crowpanel_50_companion_radio_touch # CrowPanel Advance 5.0
|
||||
# or just `pio run` to build the configured default environments
|
||||
```
|
||||
|
||||
Flash with the NVS-preserving 4-component chain (bootloader / partitions /
|
||||
boot_app0 / firmware at `0x0 / 0x8000 / 0xe000 / 0x10000`) so saved Wi-Fi
|
||||
credentials survive — not a merged image, which 0xFF-pads and wipes NVS.
|
||||
credentials survive, not a merged image, which 0xFF-pads and wipes NVS.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). One topic per
|
||||
Contributions are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md). One topic per
|
||||
PR; inbound contributions are accepted under the project's GPL-3.0 license.
|
||||
|
||||
## License
|
||||
|
||||
**GPL-3.0-or-later** — see [LICENSE](LICENSE). wadamesh is copyleft: anyone who
|
||||
**GPL-3.0-or-later**: see [LICENSE](LICENSE). wadamesh is copyleft: anyone who
|
||||
distributes a build or a fork must also make their source available under the GPL.
|
||||
This keeps the UI open and concentrates community effort instead of fragmenting it
|
||||
into closed forks.
|
||||
|
||||
wadamesh incorporates and depends on
|
||||
[MeshCore](https://github.com/meshcore-dev/MeshCore) (MIT, © Scott Powell /
|
||||
rippleradios.com) and other third-party components — see [NOTICE](NOTICE) for the
|
||||
rippleradios.com) and other third-party components; see [NOTICE](NOTICE) for the
|
||||
full list and their licenses. MeshCore-derived files keep their MIT notices; the
|
||||
combined work is distributed under the GPL (MIT is GPL-compatible). The MeshCore
|
||||
fork that wadamesh builds against stays **MIT** on purpose, so its Wi-Fi/BLE hooks
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Region discovery and channel scopes
|
||||
|
||||
WadaMesh can query nearby MeshCore repeaters for the public regions they allow
|
||||
and use those names as per-channel flood scopes.
|
||||
|
||||
## Discover nearby regions
|
||||
|
||||
1. Open **Settings > Radio & Mesh > Known regions**.
|
||||
2. Select **Scan**.
|
||||
3. WadaMesh broadcasts one zero-hop node-discovery request, waits for nearby
|
||||
repeater replies, then queries each repeater directly for its allowed public
|
||||
regions.
|
||||
4. New names appear under **Discovered**. Select **Add** beside each region you
|
||||
want to keep.
|
||||
|
||||
The scan queries directly reachable repeaters running a MeshCore version that
|
||||
supports anonymous region requests. It is bounded by the radio duty-cycle budget
|
||||
and stops after two minutes if requests cannot be completed.
|
||||
|
||||
Region traffic does not expose a reusable region identifier: its transport code
|
||||
is a payload-dependent HMAC. WadaMesh therefore cannot infer a name by passively
|
||||
listening. The active scan asks repeaters to provide their configured names.
|
||||
Private `$region` names are not imported because their keys are not included in
|
||||
the response and cannot be derived from the name.
|
||||
|
||||
## Select a channel scope
|
||||
|
||||
1. Open a channel's settings and select **Region & scope**.
|
||||
2. Choose a saved name from the **Known regions** dropdown. The selection is
|
||||
copied into the editable field.
|
||||
3. Select **Save**.
|
||||
|
||||
The field remains editable for manual entry. Leave it blank to use the device's
|
||||
default region scope. A channel override affects messages sent on that channel;
|
||||
it does not restrict which messages the device receives.
|
||||
+6
-14
@@ -57,21 +57,13 @@ apps you actually have.
|
||||
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.
|
||||
Use `build.sh`, not `idf.py` directly: it configures and applies the required
|
||||
`managed_components` compatibility patches before compiling.
|
||||
|
||||
**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.
|
||||
The wrapper builds the `gen_project_binary` target because this app lives in the
|
||||
8 MB `appfs` partition, not either 2 MB OTA slot in the device partition table.
|
||||
That runs the full compile and link while skipping IDF's irrelevant OTA size
|
||||
check, so a zero exit status means `build/tanmatsu/application.bin` is ready.
|
||||
|
||||
## Install
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# T-Deck Pro 4G issue #532 testing
|
||||
|
||||
These post-flash checks verify the setup-wizard keyboard fallback and CST3530
|
||||
touch fix on a LilyGo T-Deck Pro 4G.
|
||||
|
||||
## Choose the hardware revision
|
||||
|
||||
Flash `LilyGo_TDeck_Pro_v1_0_companion_radio_touch` on V1.0 hardware and
|
||||
`LilyGo_TDeck_Pro_v1_1_companion_radio_touch` on V1.1 hardware. Do not use the
|
||||
V1.1 image on V1.0: GPIO45 is touch reset on V1.0 but frontlight PWM on V1.1,
|
||||
so the wrong image can hold the touch controller in reset. V1.0 has no
|
||||
frontlight; V1.1 identifies a DRV2605 at I2C address `0x5A`.
|
||||
|
||||
Run the checks below separately on each available PCB revision and include the
|
||||
revision and firmware target in the report.
|
||||
|
||||
## Boot check
|
||||
|
||||
With a 115200-baud serial monitor attached, reboot the device and record this
|
||||
line:
|
||||
|
||||
```text
|
||||
[BOOT] T-Deck Pro e-paper ready touch=CST3530
|
||||
```
|
||||
|
||||
`CST3530` is expected on the affected hardware. `CST328` is also a valid Pro
|
||||
revision. `missing` is a failure and should be reported with the complete boot
|
||||
log.
|
||||
|
||||
## Open setup
|
||||
|
||||
If setup is already displayed, test from that screen. On an already configured
|
||||
device, open:
|
||||
|
||||
```text
|
||||
Settings > Device > Run setup again
|
||||
```
|
||||
|
||||
This preserves the existing flash contents while reproducing the screen from
|
||||
issue #532.
|
||||
|
||||
## Keyboard-only test
|
||||
|
||||
Do not touch the screen during this section.
|
||||
|
||||
1. On Welcome, press a WASDZ navigation key. A focus outline must appear.
|
||||
2. Use `A`/`D` to focus **Get Started**, then press Return. Step 1 must open.
|
||||
3. Focus the name field and press Return to enter edit mode.
|
||||
4. Type several letters. They must appear in the name field.
|
||||
5. Press Return to leave edit mode, navigate to **Next**, and press Return.
|
||||
6. On the region list, use `W`/`Z` to move and Return to select a region.
|
||||
7. Navigate to **Next** and press Return. Step 3 must open.
|
||||
8. Navigate to **Finish** and press Return. The wizard must close normally.
|
||||
|
||||
Pass criteria: setup can be completed without touch, ordinary keys respond,
|
||||
Return activates controls, and typing works in the name field.
|
||||
|
||||
## Touch-only test
|
||||
|
||||
Open **Run setup again** a second time and do not use the keyboard.
|
||||
|
||||
1. Tap **Get Started**. The name step must open on the first deliberate tap.
|
||||
2. Tap the prefilled name field and confirm it receives focus; leave its text unchanged.
|
||||
3. Tap **Next** using touch.
|
||||
4. Scroll the region list in both directions, select a region, and tap **Next**.
|
||||
5. Tap **Back**, confirm the region page returns, then advance again.
|
||||
6. Tap **Finish**.
|
||||
7. Repeat ten taps on controls, including taps immediately after an e-paper
|
||||
refresh. Each deliberate tap must register once, with no phantom taps.
|
||||
|
||||
Pass criteria: touch works throughout setup, scrolling remains usable, and no
|
||||
tap is lost merely because the CST3530 interrupt pulse ended before the UI poll.
|
||||
|
||||
## Regression checks
|
||||
|
||||
1. Outside setup, hold Space until the device locks, then unlock with GPIO0.
|
||||
2. Open a normal text field, type, erase with Backspace, and leave edit mode.
|
||||
3. Reboot and confirm normal startup, keyboard input, and touch still work.
|
||||
4. Confirm the serial log contains no I2C, touch, watchdog, or crash errors.
|
||||
|
||||
When reporting results, include the detected controller (`CST3530`, `CST328`,
|
||||
or `missing`), which section failed, the exact step, and the boot log.
|
||||
@@ -0,0 +1,412 @@
|
||||
# T-Display P4 C6 firmware and Bluetooth plan
|
||||
|
||||
## Decision
|
||||
|
||||
Use Espressif esp-hosted as the normal WadaMesh backend for current T-Display P4
|
||||
V1 units, which ship C6 hosted firmware 2.12.3. Retain the existing ESP-AT
|
||||
backend only for older units carrying LilyGo's now-deprecated AT images.
|
||||
|
||||
Older AT units need a guided two-stage conversion because the P4 can reset the
|
||||
C6 but cannot press its documented physical BOOT control. Current hosted units
|
||||
need no C6 reflash. After any legacy conversion, C6 firmware updates should be
|
||||
delivered and verified by the P4 over SDIO so normal WadaMesh updates remain a
|
||||
one-device operation.
|
||||
|
||||
## Goals
|
||||
|
||||
- Expose the C6 Bluetooth LE radio to WadaMesh.
|
||||
- Run the existing MeshCore Bluetooth companion transport without an AT-to-BLE
|
||||
protocol translation layer.
|
||||
- Preserve P4 Wi-Fi scanning, station mode, TCP clients, the companion TCP
|
||||
server, HTTP, SNTP, and simultaneous Wi-Fi/BLE operation.
|
||||
- Make the one-time C6 conversion understandable from the WadaMesh installer.
|
||||
- Make every later P4 installation or update automatically enforce a compatible
|
||||
C6 firmware version.
|
||||
- Provide A/B update, verification, rollback, and a wired recovery route for the
|
||||
C6.
|
||||
- Keep the P4 usable for LoRa and local UI if the C6 is absent or broken.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Classic Bluetooth. The ESP32-C6 supports Bluetooth LE only.
|
||||
- Silently rewriting the factory C6 through undocumented raw flash commands.
|
||||
- Depending on deprecated factory ESP-AT images for long-term update support.
|
||||
- Removing the existing AT implementation before the hosted replacement has
|
||||
equivalent Wi-Fi and socket coverage.
|
||||
|
||||
## Current architecture
|
||||
|
||||
### Processors
|
||||
|
||||
- ESP32-P4: runs WadaMesh, LVGL, LoRa, GPS, storage, and the companion logic.
|
||||
- ESP32-C6: owns the physical 2.4 GHz Wi-Fi and Bluetooth LE radio.
|
||||
|
||||
The P4 itself has no Wi-Fi or Bluetooth radio.
|
||||
|
||||
### Current C6 connection
|
||||
|
||||
The C6 is connected to the P4 over SDIO slot 1:
|
||||
|
||||
| Signal | P4 GPIO |
|
||||
|---|---:|
|
||||
| CLK | 18 |
|
||||
| CMD | 19 |
|
||||
| D0 | 14 |
|
||||
| D1 | 15 |
|
||||
| D2 | 16 |
|
||||
| D3 | 17 |
|
||||
|
||||
C6 enable/reset is `XL9535 IO14`. The P4 can pulse this line but does not have a
|
||||
connection to the C6 IO9 boot strap.
|
||||
|
||||
### Current software generations
|
||||
|
||||
- Current V1 factory/release firmware uses esp-hosted MCU 2.12.3. WadaMesh uses
|
||||
the matching local host fork (`2.12.3~1`) and standard remote Wi-Fi/NimBLE
|
||||
paths.
|
||||
- Older V1 units may carry LilyGo's deprecated ESP-AT SDIO firmware.
|
||||
- `tdisplay_p4/components/c6_at/` and `C6WifiShim.h` remain as an explicit
|
||||
`WADA_P4_LEGACY_AT=1` compatibility backend for those units.
|
||||
- Legacy AT Wi-Fi scans, joins, sockets, server mode, and SNTP are implemented
|
||||
in that worker. Its BLE commands cannot reliably expose the MeshCore service.
|
||||
- `AT+CIUPDATE` and `AT+USEROTA` are unavailable on the tested legacy image.
|
||||
- `C6_FLASH_HELPER=1 ./tdisplay_p4/build.sh build` creates a P4 image that does
|
||||
not reset or start the C6. It is useful for development while the C6 is placed
|
||||
in ROM download mode and flashed through its own USB connection.
|
||||
|
||||
### P4 flash layout
|
||||
|
||||
The current 16 MB P4 layout contains:
|
||||
|
||||
- 4 MB `ota_0`
|
||||
- 4 MB `ota_1`
|
||||
- approximately 8 MB FAT `storage`
|
||||
- NVS, OTA metadata, PHY data, and coredump partitions
|
||||
|
||||
The P4 application currently has less than 1 MB free in each app slot. A full C6
|
||||
image should therefore not be linked into the P4 application. If offline C6
|
||||
updates are required, reserve a dedicated data partition by reducing the FAT
|
||||
partition after measuring the final C6 image size.
|
||||
|
||||
## Target architecture
|
||||
|
||||
### C6 firmware: `wadamesh-c6`
|
||||
|
||||
Create a separately versioned ESP-IDF project for the ESP32-C6. Start from the
|
||||
same esp-hosted slave generation already proven by the Tanmatsu target.
|
||||
|
||||
Required C6 features:
|
||||
|
||||
- esp-hosted SDIO slave transport
|
||||
- Wi-Fi station and scan support
|
||||
- Bluetooth LE HCI transport for NimBLE on the P4
|
||||
- a small management endpoint for firmware identity and updates
|
||||
- A/B application partitions with rollback
|
||||
- signed-image verification
|
||||
- watchdog and boot-health reporting
|
||||
- a stable protocol version independent of the human firmware version
|
||||
|
||||
The C6 firmware should not implement the MeshCore GATT service itself. It should
|
||||
transport Bluetooth HCI to the P4, allowing the existing P4-side NimBLE and
|
||||
MeshCore `SerialBLEInterface` code to own advertising, pairing, GATT, and packet
|
||||
routing. This keeps one MeshCore BLE implementation across boards.
|
||||
|
||||
### Management protocol
|
||||
|
||||
Add a narrow WadaMesh management channel alongside the hosted data path. It must
|
||||
support at least:
|
||||
|
||||
- `GET_INFO`: protocol version, firmware version, build hash, active OTA slot,
|
||||
rollback state, and capabilities
|
||||
- `BEGIN_UPDATE`: image size, version, SHA-256, and signature metadata
|
||||
- `WRITE_CHUNK`: offset, bytes, and per-chunk integrity check
|
||||
- `END_UPDATE`: full hash/signature verification and boot-slot selection
|
||||
- `REBOOT`: restart into the pending slot
|
||||
- `CONFIRM_BOOT`: mark the new C6 image valid after host-side health checks
|
||||
- `ABORT_UPDATE`: discard a partial update
|
||||
|
||||
Commands need sequence numbers and idempotent replies so a P4 reset or SDIO
|
||||
retry cannot corrupt the inactive slot.
|
||||
|
||||
### P4 host changes
|
||||
|
||||
Introduce one explicit C6 backend boundary instead of allowing both transports
|
||||
to initialize:
|
||||
|
||||
- `C6_BACKEND_AT`: existing factory ESP-AT support, retained for migration and
|
||||
recovery builds
|
||||
- `C6_BACKEND_HOSTED`: production backend after conversion
|
||||
|
||||
For the hosted backend:
|
||||
|
||||
- stop compiling or hard-stubbing the duplicate hosted implementations
|
||||
- use exactly one esp-hosted component, following the Tanmatsu collision fix
|
||||
- route Wi-Fi through `esp_wifi_remote` and the standard Arduino Wi-Fi facade
|
||||
- route Bluetooth HCI into NimBLE
|
||||
- enable the existing MeshCore BLE companion interface
|
||||
- expose real Bluetooth capability to the UI
|
||||
- keep C6 reset under the XL9535 callback rather than a fake host GPIO
|
||||
- leave LoRa/UI startup independent from hosted initialization success
|
||||
|
||||
The host must query `GET_INFO` before enabling Wi-Fi or Bluetooth. An unsupported
|
||||
protocol should produce a clear Settings/About diagnostic and keep the rest of
|
||||
WadaMesh operational.
|
||||
|
||||
## First-time conversion
|
||||
|
||||
### Why it cannot currently be one P4 flash
|
||||
|
||||
The factory C6 has no working self-update command, and the P4 cannot force C6
|
||||
IO9 low to enter the ROM downloader. A P4 image cannot safely replace the first
|
||||
C6 firmware through the current wiring.
|
||||
|
||||
`AT+SYSFLASH` or other undocumented raw-partition writes must not be used for the
|
||||
public migration unless a sacrificial-device test proves all of the following:
|
||||
|
||||
- the required C6 partitions can be written while ESP-AT is running
|
||||
- bootloader, partition table, OTA metadata, and app offsets are known
|
||||
- power loss cannot leave the C6 unrecoverable without opening the device
|
||||
- image verification and rollback are available
|
||||
|
||||
Until then, use the C6 USB downloader.
|
||||
|
||||
### Guided web installer
|
||||
|
||||
Extend the WadaMesh install page with a P4-specific two-stage flow:
|
||||
|
||||
1. **Install C6 radio firmware**
|
||||
- Explain which USB connector reaches the C6.
|
||||
- Explain the required BOOT/IO9 and reset sequence with board photographs.
|
||||
- Launch an ESP Web Tools manifest with `chipFamily: ESP32-C6`.
|
||||
- Flash a full merged `wadamesh-c6` image containing bootloader, partition
|
||||
table, OTA metadata, and app.
|
||||
- Verify the chip and image before showing success.
|
||||
2. **Install WadaMesh P4 firmware**
|
||||
- Move the cable to the P4 USB connector.
|
||||
- Launch the existing `ESP32-P4` WadaMesh manifest.
|
||||
- On first boot, require the expected C6 protocol handshake and show the
|
||||
detected C6 version in About/Diagnostics.
|
||||
|
||||
The page should remember only browser-local completion state. The device
|
||||
handshake remains authoritative.
|
||||
|
||||
### Development flow
|
||||
|
||||
Use the existing helper for early testing:
|
||||
|
||||
```bash
|
||||
C6_FLASH_HELPER=1 ./tdisplay_p4/build.sh build
|
||||
./tdisplay_p4/build.sh flash -p <P4-port>
|
||||
```
|
||||
|
||||
Then place the C6 in ROM download mode and flash `wadamesh-c6` through the C6
|
||||
USB port. After the C6 image is installed, flash a normal hosted-backend P4
|
||||
build.
|
||||
|
||||
Document a full factory-image backup and restore command before the first C6
|
||||
write.
|
||||
|
||||
## Future seamless C6 updates
|
||||
|
||||
Once `wadamesh-c6` is installed, subsequent updates should not require the C6
|
||||
USB connector.
|
||||
|
||||
### Image delivery
|
||||
|
||||
Preferred production path:
|
||||
|
||||
- publish immutable, versioned C6 OTA images beside each P4 release
|
||||
- include required C6 protocol/version metadata in the P4 release manifest
|
||||
- let the P4 download the C6 image over its working C6 Wi-Fi connection
|
||||
- stream directly into the C6 inactive OTA slot over the management channel
|
||||
|
||||
Offline/full-install option:
|
||||
|
||||
- add a dedicated P4 `c6_fw` data partition
|
||||
- place the matching C6 OTA image there in the full merged P4 artifact
|
||||
- reduce the P4 FAT storage partition by the measured partition size
|
||||
- do not embed the image in either 4 MB P4 application slot
|
||||
|
||||
The online path should remain available for P4 OTA updates where the full flash
|
||||
image and data partition are not rewritten.
|
||||
|
||||
### Update state machine
|
||||
|
||||
1. P4 reads C6 identity and compatibility.
|
||||
2. If compatible, continue boot without rewriting anything.
|
||||
3. If an update is required, keep Wi-Fi/BLE user transports disabled.
|
||||
4. Select the C6 inactive OTA slot.
|
||||
5. Stream chunks with retries and a visible progress state.
|
||||
6. Verify SHA-256 and signature on the C6.
|
||||
7. Mark the inactive slot pending and reboot only the C6.
|
||||
8. Re-establish SDIO and run health checks:
|
||||
- management handshake
|
||||
- Wi-Fi scan
|
||||
- Wi-Fi join where credentials exist
|
||||
- BLE controller initialization
|
||||
- BLE advertising start
|
||||
9. Confirm the C6 slot only after health checks pass.
|
||||
10. If the handshake does not recover, reset the C6 and allow its bootloader
|
||||
rollback. Keep the P4 UI and LoRa available with a recovery message.
|
||||
|
||||
Never update both P4 and C6 active slots without an independently recoverable
|
||||
checkpoint between them.
|
||||
|
||||
## Release pipeline
|
||||
|
||||
Add a dedicated C6 build/release surface rather than hiding it inside the P4
|
||||
build script:
|
||||
|
||||
```text
|
||||
tdisplay_p4/c6_firmware/
|
||||
CMakeLists.txt
|
||||
sdkconfig.defaults
|
||||
partitions.csv
|
||||
main/
|
||||
build.sh
|
||||
```
|
||||
|
||||
Release artifacts:
|
||||
|
||||
- `wadamesh-c6-full.bin`: first-time USB conversion
|
||||
- `wadamesh-c6-ota.bin`: later in-band update
|
||||
- `manifest-tdisplay-p4-c6.json`: ESP Web Tools C6 manifest
|
||||
- `c6-version.json`: protocol, firmware version, hash, signature, minimum P4
|
||||
version, and release channel
|
||||
|
||||
Update these existing surfaces:
|
||||
|
||||
- `scripts/build-all-targets.sh`: build the C6 project in IDF sweeps
|
||||
- `scripts/release.sh`: stage C6 artifacts with the P4 release
|
||||
- `scripts/build/gen-flasher-meta.py`: create the ESP32-C6 manifest
|
||||
- `deploy/site/index.html`: guided two-stage P4 install
|
||||
- P4 About/Diagnostics: show C6 backend, protocol, firmware, active slot, and
|
||||
last update result
|
||||
|
||||
A release gate should reject a P4 build whose required C6 protocol is not
|
||||
present in the staged C6 metadata.
|
||||
|
||||
## Security and integrity
|
||||
|
||||
- Sign C6 images with a WadaMesh release key.
|
||||
- Verify the signature on the C6 before selecting the pending slot.
|
||||
- Pin the expected SHA-256 in release metadata and report it in diagnostics.
|
||||
- Reject downgrade unless a physical recovery flow explicitly requests it.
|
||||
- Do not carry Wi-Fi credentials inside C6 update images.
|
||||
- Clear partial-update state after abort or rollback.
|
||||
- Rate-limit and authenticate update commands so a network client cannot invoke
|
||||
the management endpoint.
|
||||
|
||||
Secure Boot and flash encryption should be evaluated for both processors as a
|
||||
separate rollout because enabling eFuses changes recovery procedures.
|
||||
|
||||
## Compatibility and fallback
|
||||
|
||||
During migration, a P4 build should detect three states:
|
||||
|
||||
| State | Behavior |
|
||||
|---|---|
|
||||
| Factory ESP-AT | Keep AT Wi-Fi available, report Bluetooth unavailable, offer C6 conversion instructions |
|
||||
| Supported hosted protocol | Enable Wi-Fi and Bluetooth normally |
|
||||
| Unknown/unresponsive C6 | Keep LoRa/UI running, disable wireless transports, expose diagnostics and wired recovery |
|
||||
|
||||
Do not infer the backend from one failed packet. Use a bounded handshake, reset
|
||||
once through XL9535 IO14, then classify the C6.
|
||||
|
||||
## Test plan
|
||||
|
||||
### C6 unit/integration
|
||||
|
||||
- version/capability handshake
|
||||
- chunk replay, duplication, and out-of-order rejection
|
||||
- full hash and signature failure
|
||||
- power loss at each update stage
|
||||
- pending-slot rollback
|
||||
- management request timeout and retry
|
||||
|
||||
### Wi-Fi
|
||||
|
||||
- scan in sparse and dense RF environments
|
||||
- hidden SSIDs
|
||||
- open, WPA2, WPA3, and wrong-password behavior
|
||||
- reconnect after C6 reset
|
||||
- TCP client and inbound companion server
|
||||
- HTTP downloads, tiles, OTA, and SNTP
|
||||
|
||||
### Bluetooth LE
|
||||
|
||||
- advertising name and MeshCore service UUID
|
||||
- Android and iOS discovery
|
||||
- connect/disconnect/reconnect
|
||||
- bidirectional MeshCore packet transport
|
||||
- MTU negotiation and fragmented writes
|
||||
- simultaneous Wi-Fi traffic
|
||||
- BLE availability after P4 and C6 independent resets
|
||||
|
||||
### System
|
||||
|
||||
- LoRa receive/transmit while Wi-Fi and BLE are active
|
||||
- SD card and C6 sharing separate SDMMC hosts
|
||||
- display underrun/flicker during C6 update and heavy traffic
|
||||
- screen-off and wake behavior
|
||||
- low-memory operation
|
||||
- C6 absent, factory, supported, outdated, and corrupt states
|
||||
- P4 OTA with matching and mismatched C6 versions
|
||||
- first-time web installation on macOS, Windows, Linux, Android Chrome, and
|
||||
ChromeOS where supported
|
||||
|
||||
## Rollout phases
|
||||
|
||||
### Phase 0: preserve and measure
|
||||
|
||||
- Back up the complete factory C6 flash.
|
||||
- Record chip revision, flash size, partition table, MAC addresses, and USB IDs.
|
||||
- Confirm the C6 USB/BOOT procedure on at least two boards.
|
||||
- Capture current AT Wi-Fi performance as a regression baseline.
|
||||
|
||||
### Phase 1: hosted proof of life
|
||||
|
||||
- Build and manually flash the smallest esp-hosted C6 slave.
|
||||
- Establish the SDIO handshake from P4.
|
||||
- Validate Wi-Fi scan/join and Bluetooth controller discovery.
|
||||
- Keep the existing AT build selectable for recovery.
|
||||
|
||||
### Phase 2: transport parity
|
||||
|
||||
- Port every P4 Wi-Fi/socket use away from `c6_at`.
|
||||
- Enable the existing MeshCore BLE companion transport.
|
||||
- Pass Wi-Fi, BLE, TCP server, OTA, tile, and time-sync tests.
|
||||
|
||||
### Phase 3: public first-time installer
|
||||
|
||||
- Publish signed full C6 artifacts and an ESP32-C6 web manifest.
|
||||
- Add the two-stage installer UI and connector/BOOT instructions.
|
||||
- Add backend/version diagnostics to P4.
|
||||
|
||||
### Phase 4: in-band updates
|
||||
|
||||
- Implement C6 A/B management protocol and host updater.
|
||||
- Add online C6 artifact delivery.
|
||||
- Add an optional offline P4 `c6_fw` partition after measuring image size.
|
||||
- Exercise interruption and rollback tests before enabling automatic updates.
|
||||
|
||||
### Phase 5: default hosted backend
|
||||
|
||||
- Make hosted the normal P4 release backend.
|
||||
- Retain an explicit AT recovery build for at least one stable release cycle.
|
||||
- Remove temporary ESP-AT GATT provisioning only after migration coverage is
|
||||
acceptable.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
The migration is complete when:
|
||||
|
||||
- a fresh user can follow one WadaMesh page to flash both chips without command
|
||||
line tools
|
||||
- normal later WadaMesh updates need only the P4 connection or on-device OTA
|
||||
- About reports compatible P4 and C6 versions
|
||||
- Wi-Fi feature parity is maintained
|
||||
- the MeshCore phone app can discover, connect, exchange packets, and reconnect
|
||||
over Bluetooth LE
|
||||
- failed or interrupted C6 updates roll back without losing P4 LoRa/UI access
|
||||
- a documented wired restore path returns the C6 to a known image
|
||||
+71
-28
@@ -1,22 +1,22 @@
|
||||
# ThinkNode M9: Keyboard & D-pad Guide
|
||||
|
||||
The Elecrow ThinkNode M9 has **no touchscreen** — every screen, every setting,
|
||||
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
|
||||
- **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
|
||||
- **Function row**: dedicated keys for Chats, Home, Mentions, Advert, Map,
|
||||
Back, Ctrl and Triangle (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
|
||||
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.
|
||||
|
||||
@@ -28,7 +28,7 @@ dropdown → the power menu → the Control Center → a full-screen app → a d
|
||||
else.
|
||||
|
||||
Once there is nothing left covering the screen, **Back returns you to the
|
||||
previous screen** — the tab you came from, however you got there: a function
|
||||
previous screen**, the tab you came from, however you got there: a function
|
||||
key, an app-drawer tile, "Show on map", a contact's Send-msg button. Keep
|
||||
pressing and you keep walking back through where you have been. When the trail
|
||||
runs out Back lands on Home, and on Home with nothing open it does nothing,
|
||||
@@ -36,25 +36,33 @@ because that is the root.
|
||||
|
||||
**HOME** is the shortcut past all of that: it goes straight to the root and
|
||||
clears the trail, so Back from there won't jump you back out again.
|
||||
When **App drawer as home** is enabled, the drawer cog's **Lock home to drawer**
|
||||
option makes HOME stay in or return to the drawer, and makes Back stop there as
|
||||
the root after closing anything above it. The **Cmdr** tile still opens the
|
||||
Commander screen when you need it.
|
||||
|
||||
The only exception is deliberate: progress overlays (SD format, bulk delete)
|
||||
block all keys until the operation finishes — Back will not navigate out from
|
||||
block all keys until the operation finishes; Back will not navigate out from
|
||||
under a running operation.
|
||||
|
||||
## 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 (and clear the Back trail) | — |
|
||||
| **@ (Mentions)** | Open the Mentions screen | — |
|
||||
| **MSG** | Jump to the Chats tab; inside a conversation, return to the chat list (closes an open app first) | None |
|
||||
| **HOME** | Peel one layer off an open app; on the Home tab, toggle the app drawer; otherwise jump Home (and clear the Back trail). **Double-press** to open the launcher settings (what Home opens, icon size) | None |
|
||||
| **@ (Mentions)** | Open the Mentions screen | None |
|
||||
| **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 | — |
|
||||
| **MAP** | Jump to the Map tab; press again *on* the map to toggle pan mode | None |
|
||||
| **BACK** | Close the top layer (see above) | None |
|
||||
| **CTRL** | Open the Control Center (quick toggles, incl. the keyboard light: off / on / auto) | None |
|
||||
| **Triangle** | The focused item's hold action, e.g. a chat row's menu | None |
|
||||
| **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** |
|
||||
|
||||
The **MSG**, **HOME**, **@**, **ADV**, **MAP**, and **CTRL** shortcuts remain
|
||||
active while typing or using a picker. Back, the d-pad, and OK remain contextual
|
||||
so they can leave edit mode, move the caret or selection, and activate controls.
|
||||
|
||||
## Map pan mode
|
||||
|
||||
On the Map tab, press **MAP** again: the arrows now pan the map instead of
|
||||
@@ -63,17 +71,44 @@ 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.
|
||||
|
||||
## Files and offline maps
|
||||
|
||||
From the Home tab, press **HOME** to open the app drawer. **Files** browses and
|
||||
organizes the M9's SD card. **Transfer** starts an authenticated browser session
|
||||
for uploads and downloads over the same Wi-Fi network.
|
||||
|
||||
To install an offline map, open Transfer, visit the displayed URL, enter the
|
||||
six-digit code, choose **Offline OSM map (/tiles)**, and select an unpacked
|
||||
folder containing `z/x/y.png` tiles. After it completes, open Map options,
|
||||
select OSM, and enable **Tiles from SD card**. See the
|
||||
[offline map tile guide](README.md#offline-map-tiles) for the QGIS export steps
|
||||
and map-provider policy requirements.
|
||||
|
||||
## Web pages
|
||||
|
||||
In the Web browser, **Up** and **Down** read the page: each press moves the
|
||||
highlight to the next link already on screen, or, when there is none, scrolls
|
||||
the text by about two-thirds of a screen. If the highlighted link scrolls out
|
||||
of view, the highlight moves to a link coming into view. Only at the top or
|
||||
bottom of the page does focus leave for the address bar or the toolbar.
|
||||
**OK** opens the highlighted link.
|
||||
|
||||
## Typing & editing
|
||||
|
||||
Inside a text field the left/right arrows move the caret — and when the caret
|
||||
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.
|
||||
|
||||
Channels, direct messages, and room conversations place **#** and emoji to the
|
||||
right of the message input. Press Right to move from the input to **#**, and
|
||||
Right again to move to emoji. Press **OK** on either button to open its picker.
|
||||
While a chat message is focused and newer messages are below it, a down arrow
|
||||
appears at the right edge. Press **Right** to select it, then **OK** to jump to
|
||||
the newest message.
|
||||
|
||||
Channels, direct messages, and room conversations place emoji and **#** to the
|
||||
right of the message input, next to Send. With the caret at the end of the
|
||||
input, press Right to move to emoji, and Right again to move to **#**; Left
|
||||
walks back to the input. Press **OK** on either button to open its picker.
|
||||
|
||||
In the symbol grid, use all four arrows to move the highlight and **OK** to
|
||||
insert the selected character.
|
||||
@@ -85,7 +120,7 @@ After inserting a symbol, focus returns to the originating text field.
|
||||
|
||||
## Lock screen
|
||||
|
||||
**Hold OK** to unlock — the keyboard's hardware long-press stands in for the
|
||||
**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.
|
||||
@@ -93,26 +128,26 @@ 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
|
||||
the centre, so 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
|
||||
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 now actually goes back** — once everything covering the screen was
|
||||
- **Back now actually goes back**: once everything covering the screen was
|
||||
closed, Back had nothing left to do and the press simply died on any bare
|
||||
tab. It now walks back through the screens you visited, then Home. Jumps that
|
||||
used to strand you ("Show on map" from Discover or Contacts, a route replay
|
||||
from a chat, the Home unread line, the ✉ badge, app-drawer tiles) all return
|
||||
properly. **Back also works in the first-boot wizard** now, stepping back
|
||||
through its screens like the on-screen Back button.
|
||||
- **Back restored and made consistent** — it had regressed to doing nothing
|
||||
- **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.
|
||||
- **The map opens faster the first time** — the cold open was doing a
|
||||
- **The map opens faster the first time**: the cold open was doing a
|
||||
whole-card zoom scan whose result was thrown away, and (with no GPS fix) was
|
||||
repeating that scan on *every* open. The "Loading map…" hint now appears
|
||||
immediately instead of after the scan, so the board no longer looks frozen on
|
||||
@@ -120,14 +155,14 @@ If you last used the M9 on an earlier beta, the notable navigation changes:
|
||||
- **The d-pad no longer goes dead for a second** when a message arrives, and
|
||||
the Spectrum page no longer steers an invisible cursor around the app drawer
|
||||
underneath it.
|
||||
- **Apps can't strand you** — opening Advert/Mentions over a running app used
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
@@ -135,3 +170,11 @@ If you last used the M9 on an earlier beta, the notable navigation changes:
|
||||
|
||||
The engineering record behind these lives in
|
||||
[`variants/thinknode_m9/M9_PORT.md`](variants/thinknode_m9/M9_PORT.md).
|
||||
|
||||
## Tooltips and the cursor
|
||||
|
||||
The focus highlight is amber, so the selected control stands out on any
|
||||
background. Icon buttons say what they are: rest on one for a moment and a
|
||||
small card names it. On a control panel tile the card adds "Hold Enter:
|
||||
settings": holding OK (the d-pad's Enter) there opens that tile's settings
|
||||
page.
|
||||
+88
-60
@@ -1,19 +1,19 @@
|
||||
# T-LoRa Pager: Keyboard & Rotary Encoder Guide
|
||||
|
||||
The LilyGo T-LoRa Pager has **no touchscreen and no trackball** — every
|
||||
The LilyGo T-LoRa Pager has **no touchscreen and no trackball**: every
|
||||
screen, every setting, and every chat is driven entirely by the physical
|
||||
QWERTY keyboard and the rotary encoder knob. If you're coming from the
|
||||
T-Deck or Heltec V4 (touch-first), this is the one board where learning a
|
||||
handful of gestures up front pays off immediately. This guide covers both
|
||||
radio variants (LR1121 and SX1262) — the UI and input handling are identical
|
||||
radio variants (LR1121 and SX1262); the UI and input handling are identical
|
||||
on both.
|
||||
|
||||
## The two inputs
|
||||
|
||||
- **Keyboard** — a physical QWERTY matrix with a Space bar, Backspace, Enter,
|
||||
- **Keyboard**: a physical QWERTY matrix with a Space bar, Backspace, Enter,
|
||||
a Shift key, and an orange **Fn/Alt** key (bottom-left). No dedicated Esc,
|
||||
arrow keys, or number row — those are covered by combos below.
|
||||
- **Rotary encoder** — turn to move the on-screen focus highlight; click
|
||||
arrow keys, or number row; those are covered by combos below.
|
||||
- **Rotary encoder**: turn to move the on-screen focus highlight; click
|
||||
(press the knob) to select. It's your only pointing device, so almost
|
||||
every screen is navigable with turn + click alone.
|
||||
|
||||
@@ -36,20 +36,40 @@ They do nothing inside an open chat, settings detail, app page, or popup, so a
|
||||
letter cannot unexpectedly close the inner screen. They also remain inactive
|
||||
while editing a text field, where the keys type normally.
|
||||
|
||||
**H** and **S** also work like holding those buttons on a touchscreen:
|
||||
|
||||
- **Tap H** on Home switches between the app drawer and your widgets.
|
||||
- **Hold H** (about half a second) opens the launcher options.
|
||||
- **Hold S** opens the control panel.
|
||||
|
||||
A hold opens them over the screen you are on.
|
||||
|
||||
Each tab remembers where you were: coming back puts the cursor on the item
|
||||
you left. Contacts opens on the first contact, and a chat with unread
|
||||
messages opens on the first of them (see [N](#n-catching-up)).
|
||||
|
||||
Icon buttons say what they are: rest the cursor on one for a moment and a
|
||||
small card names it, sometimes with a line on what it does or, on a
|
||||
control-panel tile, "Hold Enter: settings".
|
||||
|
||||
**Power off** (control panel, power button) asks first, with Cancel
|
||||
selected, so one stray click never switches the Pager off. Press **BOOT** to
|
||||
wake it again.
|
||||
|
||||
## Rotary encoder
|
||||
|
||||
| Gesture | Action | Keyboard equivalent |
|
||||
|---|---|---|
|
||||
| Turn | Move focus to the next/previous item on screen | none — encoder only |
|
||||
| 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** |
|
||||
| Hold ~1 s, then release | **Back**: closes a popup → closes an open chat → goes Home → Esc (whichever applies first). A ring fills while you hold | **Backspace** (tap) |
|
||||
| **Fn (Alt) + turn**, on a main tab | Move between the 5 main tabs (Mail / Contacts / Home / Map / Settings) | **M / C / H / A / S** jumps directly |
|
||||
| **Fn (Alt) + turn**, inside a settings page or chat | Scroll the page up/down | none — encoder only |
|
||||
| **Fn (Alt) + short click**, with a text field focused | Open the special-character picker | none — encoder only |
|
||||
| Turn, with a dropdown open | Scroll through the dropdown's options | none — encoder only |
|
||||
| Turn, with the accent picker open | Cycle through the accent variants (see below) | none — encoder only (Fn+Space to enter is shared) |
|
||||
| Turn, with the @-mention list open | Cycle through matching contacts (see below) | none — encoder only |
|
||||
| Turn, at the oldest/newest **loaded** message in an open chat | Loads more history in that direction and keeps going — see [Chat screen](#chat-screen) | none — encoder only |
|
||||
| **Fn (Alt) + turn**, inside a settings page or chat | Scroll the page up/down | none (encoder only) |
|
||||
| **Fn (Alt) + short click**, with a text field focused | Open the special-character picker | none (encoder only) |
|
||||
| Turn, with a dropdown open | Scroll through the dropdown's options | none (encoder only) |
|
||||
| Turn, with the accent picker open | Cycle through the accent variants (see below) | none (encoder only; Fn+Space to enter is shared) |
|
||||
| Turn, with the @-mention list open | Cycle through matching contacts (see below) | none (encoder only) |
|
||||
| Turn, at the oldest/newest **loaded** message in an open chat | Loads more history in that direction and keeps going (see [Chat screen](#chat-screen)) | none (encoder only) |
|
||||
|
||||
Since there's no touch to tap the bottom bar, use its visible **M / C / H / A /
|
||||
S** shortcuts to jump directly or **Fn+turn** to move through the tabs in
|
||||
@@ -58,7 +78,7 @@ order.
|
||||
## Chat screen
|
||||
|
||||
An open chat has no touch-only jump-to-oldest/jump-to-latest buttons on this
|
||||
board — everything is reachable by turning and clicking, and the encoder
|
||||
board: everything is reachable by turning and clicking, and the encoder
|
||||
behaves a bit differently here than anywhere else in the app.
|
||||
|
||||
### Turning through messages
|
||||
@@ -67,16 +87,16 @@ Message history is loaded lazily: only the messages currently near the
|
||||
viewport actually exist as on-screen items, and older/newer ones are pulled in
|
||||
as you approach them. Turning the encoder normally moves the focus highlight
|
||||
one message at a time, and **at the edge of what's currently loaded, it keeps
|
||||
going** — the turn instead scrolls the list to pull in the next batch of
|
||||
going**: the turn instead scrolls the list to pull in the next batch of
|
||||
history, then continues moving the highlight onto it. You'll only ever land
|
||||
outside the message list (see below) once you've genuinely reached the very
|
||||
oldest or very newest message in the whole conversation — never mid-history.
|
||||
oldest or very newest message in the whole conversation, never mid-history.
|
||||
|
||||
### Leaving the message list
|
||||
|
||||
Turning **past the oldest loaded message** (top edge), once there's truly
|
||||
nothing more to load, wraps around to whatever the last focusable item on the
|
||||
screen is — the channel-settings gear in an open **channel**, or the Send
|
||||
screen is: the channel-settings gear in an open **channel**, or the Send
|
||||
button in a **DM** (a DM has no gear to reach). This is plain focus wrap, the
|
||||
same as reaching the end of any other list in the app; there's no chat-specific
|
||||
handling for this edge.
|
||||
@@ -90,23 +110,23 @@ newest message instead.
|
||||
### The reply row
|
||||
|
||||
Below the messages, turning steps through: **quick-reply (△) → emoji →
|
||||
text box → Send**, in that order, in both directions — nothing is skipped.
|
||||
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 — catching up
|
||||
### N: catching up
|
||||
|
||||
**Backspace (tap)** jumps to whichever message needs your attention:
|
||||
A chat with unread messages opens on the first of them. **N** 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
|
||||
- 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.
|
||||
N only does this inside an open chat and while you are not typing in the
|
||||
reply box. While you type, it is just the letter n.
|
||||
|
||||
## Keyboard layout
|
||||
|
||||
@@ -142,14 +162,14 @@ While any text field is focused, including a Wi-Fi or repeater password:
|
||||
### Shift and Caps Lock
|
||||
|
||||
- **Hold Shift + a letter**: that letter (or letters, for as long as you
|
||||
hold Shift) types uppercase — released, typing goes back to lowercase.
|
||||
hold Shift) types uppercase; released, typing goes back to lowercase.
|
||||
Real momentary Shift, just like a normal keyboard.
|
||||
- **Hold Fn (Alt), then press Shift**: while you're editing a text field,
|
||||
toggles **Caps Lock** on/off — stays uppercase until you repeat the chord.
|
||||
toggles **Caps Lock** on/off; it stays uppercase until you repeat the chord.
|
||||
Anywhere else (not editing a field), the chord does nothing.
|
||||
- Shift alone, tapped with nothing else, does nothing (as expected).
|
||||
- **Hold Fn (Alt), then press Backspace**: jumps straight to the **Home**
|
||||
screen — works everywhere, including while you're actively editing a field
|
||||
screen; it works everywhere, including while you're actively editing a field
|
||||
(unlike Alt+Shift above, this one isn't context-dependent). Doesn't delete
|
||||
a character or trigger the plain-Backspace gestures below.
|
||||
|
||||
@@ -157,18 +177,21 @@ While any text field is focused, including a Wi-Fi or repeater password:
|
||||
|
||||
| Key | While editing a text field | Otherwise |
|
||||
|---|---|---|
|
||||
| **Enter** | Send / submit / newline | Select the focused item — or, if a chat message bubble is focused, opens its action menu (Ack / Mention / Copy / Info / Block) |
|
||||
| **Backspace** (tap) | Delete a character | In an open chat: jump to your first unread message, or the newest message if nothing's unread — see [Chat screen](#chat-screen) |
|
||||
| **Backspace** (hold ~1 s) | — | Same as the encoder's long-press: **Back** |
|
||||
| **Backspace** (hold ~1 s) *while the screen is locked* | — | Unlock the screen |
|
||||
| **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) |
|
||||
| **Enter** | Send / submit / newline | Select the focused item. On a chat message bubble, opens its action menu (Ack / Mention / Copy / Info / Block) |
|
||||
| **Enter** (hold about half a second) | Nothing | What a long press does on a touchscreen: a chat's actions, hiding an app in the drawer, a control-panel switch's settings page |
|
||||
| **Backspace** (tap) | Delete a character | **Back**: closes a popup, a page or the chat, otherwise goes Home |
|
||||
| **Backspace** (hold ~1 s) | **Back**, leaving the screen the field is on | **Back** |
|
||||
| **N** | Types n | In an open chat: jump to your first unread message, or the newest if nothing is unread. See [Chat screen](#chat-screen) |
|
||||
| **Backspace** (hold ~1 s) *while the screen is locked* | Nothing | Unlock the screen |
|
||||
| **Space** (tap) | Types a space | Nothing |
|
||||
| **Space** (double-tap, within 250 ms) | Switches between English and your configured secondary keyboard layout | Nothing |
|
||||
| **Space** (hold ~1 s) | Nothing | Locks the screen (shows a "Locking…" progress bar; tapping any key cancels) |
|
||||
| **Fn (Alt)** (tap / double-tap) | Next key uses symbols / lock symbols until tapped again | Same |
|
||||
| **Fn (Alt) held + Enter** | **Paste** the clipboard at the cursor (replaces a highlighted range). The clipboard is filled by **Copy** in a message bubble's action menu. Fn must be held, not tapped | Nothing |
|
||||
|
||||
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
|
||||
Space-hold gesture above — for that, hold Backspace.
|
||||
Space-hold gesture above; for that, hold Backspace.
|
||||
|
||||
## Accented characters
|
||||
|
||||
@@ -176,30 +199,30 @@ Typing a letter that has accent variants (all vowels, plus `n c s y t z l r`,
|
||||
both cases) pops up a small row of alternatives above the field. To pick one
|
||||
without touch:
|
||||
|
||||
1. **Fn (Alt) + Space** — jumps focus into the popup.
|
||||
2. **Turn the encoder** — cycles through the variants.
|
||||
3. **Enter** — confirms your choice and replaces the letter you just typed.
|
||||
4. **Backspace** — dismisses the popup and keeps the plain letter.
|
||||
1. **Fn (Alt) + Space**: jumps focus into the popup.
|
||||
2. **Turn the encoder**: cycles through the variants.
|
||||
3. **Enter**: confirms your choice and replaces the letter you just typed.
|
||||
4. **Backspace**: dismisses the popup and keeps the plain letter.
|
||||
|
||||
## @-mentions
|
||||
|
||||
Typing `@` followed by a few letters of a contact's name pops up a matching
|
||||
list in a channel/group chat. Unlike the accent picker above, this one grabs
|
||||
the encoder **immediately** — no Fn+Space needed, since it only appears once
|
||||
the encoder **immediately**, no Fn+Space needed, since it only appears once
|
||||
you've deliberately started typing a mention:
|
||||
|
||||
1. **Turn the encoder** — cycles through the matching contacts.
|
||||
2. **Enter / encoder click** — confirms the highlighted one, replacing
|
||||
1. **Turn the encoder**: cycles through the matching contacts.
|
||||
2. **Enter / encoder click**: confirms the highlighted one, replacing
|
||||
`@partial` with `@FullName ` in your message.
|
||||
3. **Backspace** — dismisses the list without touching what you typed.
|
||||
3. **Backspace**: dismisses the list without touching what you typed.
|
||||
|
||||
## Sliders
|
||||
|
||||
Any focused slider (Control Center brightness, a Settings slider, the Map
|
||||
zoom bar) can't be dragged without touch — instead:
|
||||
zoom bar) can't be dragged without touch. Instead:
|
||||
|
||||
- **Q** — decrease
|
||||
- **E** — increase
|
||||
- **Q**: decrease
|
||||
- **E**: increase
|
||||
|
||||
Each press moves it proportionally (about 20 presses end-to-end) and
|
||||
persists the new value immediately, the same as releasing a drag.
|
||||
@@ -208,10 +231,10 @@ persists the new value immediately, the same as releasing a drag.
|
||||
|
||||
While the **Map** tab is active:
|
||||
|
||||
- **W** — pan north
|
||||
- **A** — pan west
|
||||
- **X** — pan south
|
||||
- **D** — pan east
|
||||
- **W**: pan north
|
||||
- **A**: pan west
|
||||
- **X**: pan south
|
||||
- **D**: pan east
|
||||
|
||||
`X` replaces the usual `S` position because **S** is the visible Settings
|
||||
shortcut. Pressing **A** on another main screen opens Map; once Map is already
|
||||
@@ -225,10 +248,10 @@ needed.
|
||||
**Settings → Keyboard → Keyboard backlight** cycles through three modes
|
||||
(tap to advance):
|
||||
|
||||
- **Off** — always dark
|
||||
- **On** — always lit
|
||||
- **Auto** (default) — lights up on any keypress, turns off ~3 seconds after
|
||||
the last one
|
||||
- **Off**: always dark
|
||||
- **On**: always lit
|
||||
- **Auto** (default): lights up on any keypress and follows the configured
|
||||
**Screen timeout**; `0 = never` keeps it lit until the screen is turned off
|
||||
|
||||
Unlike the T-Deck, this is a plain on/off strip under the keys, not a
|
||||
dimmable brightness curve.
|
||||
@@ -252,9 +275,9 @@ that changes keyboard language.
|
||||
|---|---|
|
||||
| M / C / H / A / S (top-level main screens only) | Open Mail / Contacts / Home / Map / Settings |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| Hold encoder ~1 s, or tap Backspace | Back |
|
||||
| Fn + turn (main tab) | Switch tabs |
|
||||
| Fn + turn (page/chat) | Scroll |
|
||||
| Fn + encoder click (text field focused) | Open special-character picker |
|
||||
@@ -264,11 +287,16 @@ that changes keyboard language.
|
||||
| Fn + Shift (editing a field) | Toggle Caps Lock |
|
||||
| Fn + Shift (not editing a field) | Nothing |
|
||||
| Fn + Backspace (anywhere) | Jump to Home |
|
||||
| Fn held + Enter (editing a field) | Paste the clipboard |
|
||||
| Fn + Space | Enter accent picker |
|
||||
| @ + letters (composer) | Auto-focuses the mention list — no Fn+Space needed |
|
||||
| @ + letters (composer) | Auto-focuses the mention list, no Fn+Space needed |
|
||||
| Enter | Select / send / message action menu |
|
||||
| Backspace (tap) | Delete / in a chat: jump to first unread message, or newest if none |
|
||||
| Backspace (hold 1s) | Back, or unlock if locked |
|
||||
| Hold Enter | Long press: a chat's actions, app options, a switch's settings |
|
||||
| Backspace (tap) | Delete while typing, otherwise Back |
|
||||
| Backspace (hold 1s) | Back, also while typing; or unlock if locked |
|
||||
| N (in a chat) | Jump to the first unread message, or the newest if none |
|
||||
| H on Home | Switch between the app drawer and widgets |
|
||||
| Hold H / hold S | Launcher options / control panel |
|
||||
| Space (tap) | Space |
|
||||
| Space (double-tap) | Switch keyboard language |
|
||||
| Space (hold 1s) | Lock screen |
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_16MB.csv",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"psram_type": "opi",
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth", "lora", "gps"],
|
||||
"debug": {
|
||||
"default_tool": "esp-builtin",
|
||||
"onboard_tools": ["esp-builtin"],
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino",
|
||||
"espidf"
|
||||
],
|
||||
"name": "Elecrow CrowPanel Advance 3.5 (N16R8)",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": false,
|
||||
"wait_for_upload_port": false,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://www.elecrow.com/",
|
||||
"vendor": "Elecrow"
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_16MB.csv",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"psram_type": "opi",
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth", "lora"],
|
||||
"debug": {
|
||||
"default_tool": "esp-builtin",
|
||||
"onboard_tools": ["esp-builtin"],
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino",
|
||||
"espidf"
|
||||
],
|
||||
"name": "Elecrow CrowPanel Advance 5.0 (N16R8)",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"use_1200bps_touch": false,
|
||||
"wait_for_upload_port": false,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://www.elecrow.com/",
|
||||
"vendor": "Elecrow"
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_16MB.csv",
|
||||
"memory_type": "qio_qspi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [
|
||||
[
|
||||
"0x303A",
|
||||
"0x1001"
|
||||
]
|
||||
],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": [
|
||||
"wifi",
|
||||
"bluetooth"
|
||||
],
|
||||
"debug": {
|
||||
"default_tool": "esp-builtin",
|
||||
"onboard_tools": [
|
||||
"esp-builtin"
|
||||
],
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino"
|
||||
],
|
||||
"name": "LilyGo T-Deck Max (16M Flash 8M PSRAM)",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://lilygo.cc/products/t-deck-pro",
|
||||
"vendor": "LilyGo"
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_16MB.csv",
|
||||
"memory_type": "qio_qspi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
"f_cpu": "240000000L",
|
||||
"f_flash": "80000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [["0x303A", "0x1001"]],
|
||||
"mcu": "esp32s3",
|
||||
"variant": "esp32s3"
|
||||
},
|
||||
"connectivity": ["wifi", "bluetooth"],
|
||||
"debug": {
|
||||
"default_tool": "esp-builtin",
|
||||
"onboard_tools": ["esp-builtin"],
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": ["arduino"],
|
||||
"name": "LilyGo T-Deck Pro (16M Flash 8M PSRAM)",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 327680,
|
||||
"maximum_size": 16777216,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "https://lilygo.cc/products/t-deck-pro",
|
||||
"vendor": "LilyGo"
|
||||
}
|
||||
@@ -17,9 +17,10 @@ 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
|
||||
app sandbox on internal flash, SD, or SD_MMC. Physical-SD directory metadata
|
||||
is available through `wada.sd`, which can also find and delete Windows malware on the
|
||||
card (the firmware decides what counts, so it cannot delete anything else), 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).
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "airtime",
|
||||
"name": "Airtime",
|
||||
"ver": "1.5",
|
||||
"desc": "Utilization bars with a % axis, rx/tx split, duty ceiling, budget. Reset button top-right. Keeps a Home widget with the channel load while it runs."
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
-- Airtime (Lua) — channel utilization over time: live + average bars with a %
|
||||
-- axis, rx/tx air split, duty ceiling, tx budget and packet counters. While it
|
||||
-- runs it also keeps its Home widget current (wada.widget, extended SDK boards).
|
||||
local ui, sys, mesh, timer = wada.ui, wada.sys, wada.mesh, wada.timer
|
||||
local widget = wada.widget -- nil on firmware or boards without it
|
||||
-- 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 spark, wlast = {}, 0 -- the Home widget's curve (one point per 5 s)
|
||||
|
||||
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 widget and now - wlast >= 5000 then
|
||||
wlast = now
|
||||
spark[#spark + 1] = math.floor(live * 10)
|
||||
if #spark > 24 then table.remove(spark, 1) end
|
||||
widget.set{ title = tr("Channel busy"), value = string.format("%.1f%%", live),
|
||||
line = string.format(tr("avg %.1f%%, peak %.1f%%"), avg, peak_pct), spark = spark }
|
||||
end
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"airtime","name":"Airtime","ver":"1.6","desc":"How busy the channel is: the live load, average, peak, a filled chart with the duty ceiling and the rx/tx split. Keeps a Home widget with the channel load while it runs."}
|
||||
@@ -0,0 +1,243 @@
|
||||
-- Airtime (Lua): how busy the channel is. The live load as a big figure, tiles
|
||||
-- for the average, the peak and this node's own time on the air, a filled
|
||||
-- chart of the load with its average and the duty-cycle ceiling, and the
|
||||
-- receive / transmit split as a bar. While it runs it also keeps its Home
|
||||
-- widget current (wada.widget, extended SDK boards).
|
||||
--
|
||||
-- Looks best on firmware with wada.ui.panel (beta_95 and later); on older
|
||||
-- firmware the same screen draws without the framed panels and the split bar.
|
||||
local ui, sys, mesh, timer = wada.ui, wada.sys, wada.mesh, wada.timer
|
||||
local widget = wada.widget -- nil on firmware or boards without it
|
||||
-- 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 WARN = C.warn or 0xE8A33D
|
||||
local PANEL = C.panel or 0x15181B
|
||||
local AVG = 0x4F9DF7
|
||||
local app = {}
|
||||
|
||||
local function mix(a, b, t) -- colour a over b by t (0..1)
|
||||
local function ch(s)
|
||||
local x, y = (a >> s) & 0xFF, (b >> s) & 0xFF
|
||||
return math.floor(y + (x - y) * t + 0.5) << s
|
||||
end
|
||||
return ch(16) | ch(8) | ch(0)
|
||||
end
|
||||
local AREA = { mix(C.accent, PANEL, 0.50), mix(C.accent, PANEL, 0.30), mix(C.accent, PANEL, 0.14) }
|
||||
local GRID = mix(C.sub, PANEL, 0.30)
|
||||
|
||||
local W, H, narrow, LH12, LH16
|
||||
local big, sub, tiles, cv, CW, CH, NPTS = nil, nil, {}, nil, 0, 0, 2
|
||||
local grid_lbl = {}
|
||||
local split_rx, split_tx, split_w, split_lbl, foot
|
||||
local split_x0, split_y = 0, 0
|
||||
local base, t0, last
|
||||
local peak_pct = 0
|
||||
local live_h, avg_h = {}, {} -- chart history, percent
|
||||
local top = 10 -- chart ceiling, percent
|
||||
local spark, wlast = {}, 0 -- the Home widget's curve (one point per 5 s)
|
||||
|
||||
local function panel(x, y, w, h, col)
|
||||
if ui.panel then return ui.panel(x, y, w, h, col or PANEL, 6) end
|
||||
end
|
||||
|
||||
local function load_col(p)
|
||||
return p > 50 and C.bad or p > 20 and WARN or C.good
|
||||
end
|
||||
|
||||
local function nice_top(v) -- the chart ceiling: 10, 20, 30, 50 or 100 percent
|
||||
for _, t in ipairs({ 10, 20, 30, 50, 100 }) do if v <= t then return t end end
|
||||
return 100
|
||||
end
|
||||
|
||||
local function ypix(p)
|
||||
if p < 0 then p = 0 elseif p > top then p = top end
|
||||
return (CH - 1) - math.floor(p * (CH - 1) / top)
|
||||
end
|
||||
|
||||
local function draw_chart(duty)
|
||||
if not cv then return end
|
||||
cv:fill(PANEL)
|
||||
for g = 0, 2 do
|
||||
local gy = g * (CH - 1) // 2
|
||||
cv:line(0, gy, CW - 1, gy, GRID)
|
||||
end
|
||||
local n = #live_h
|
||||
local b1, b2 = CH // 3, 2 * CH // 3
|
||||
for i = 1, n do
|
||||
local x = (NPTS - n + i - 1) * (CW - 1) // (NPTS - 1)
|
||||
local x2 = (NPTS - n + i) * (CW - 1) // (NPTS - 1)
|
||||
if x2 <= x then x2 = x + 1 end
|
||||
local y = ypix(live_h[i])
|
||||
if y < b1 then cv:rect(x, y, x2 - x, b1 - y, AREA[1]); y = b1 end
|
||||
if y < b2 then cv:rect(x, y, x2 - x, b2 - y, AREA[2]); y = b2 end
|
||||
if y < CH then cv:rect(x, y, x2 - x, CH - y, AREA[3]) end
|
||||
end
|
||||
local px, py, qx, qy
|
||||
for i = 1, n do
|
||||
local x = (NPTS - n + i - 1) * (CW - 1) // (NPTS - 1)
|
||||
local y, ya = ypix(live_h[i]), ypix(avg_h[i])
|
||||
if px then
|
||||
cv:line(px, py, x, y, C.accent, 2)
|
||||
cv:line(qx, qy, x, ya, AVG, 2)
|
||||
end
|
||||
px, py, qx, qy = x, y, x, ya
|
||||
end
|
||||
-- the duty-cycle ceiling, dashed, when the region has one and it is on the scale
|
||||
if duty and duty > 0 and duty <= top then
|
||||
local dy = ypix(duty)
|
||||
for x = 0, CW - 1, 8 do cv:line(x, dy, math.min(x + 4, CW - 1), dy, WARN) end
|
||||
end
|
||||
end
|
||||
|
||||
local function set_split(rx, tx)
|
||||
local tot = rx + tx
|
||||
local rp = tot > 0 and math.floor(rx * 100 / tot + 0.5) or 0
|
||||
local txt = tot > 0 and string.format(tr("rx %d%% tx %d%%"), rp, 100 - rp) or tr("no airtime yet")
|
||||
if not split_rx then
|
||||
if split_lbl then split_lbl:set(txt) end
|
||||
return
|
||||
end
|
||||
local wr = tot > 0 and math.floor(split_w * rx / tot + 0.5) or 0
|
||||
if tot > 0 and tx > 0 and wr >= split_w then wr = split_w - 1 end
|
||||
split_rx:size(math.max(wr, 1), 8)
|
||||
split_tx:pos(split_x0 + wr, split_y)
|
||||
split_tx:size(math.max(split_w - wr, 1), 8)
|
||||
split_tx:color(tot > 0 and tx > 0 and AVG or mix(C.sub, PANEL, 0.25))
|
||||
if tot == 0 then split_rx:color(mix(C.sub, PANEL, 0.25)) else split_rx:color(C.accent) end
|
||||
split_lbl:set(txt)
|
||||
end
|
||||
|
||||
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
|
||||
live_h, avg_h = {}, {}
|
||||
top = 10
|
||||
big:set("--"); big: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
|
||||
|
||||
live_h[#live_h + 1] = live; if #live_h > NPTS then table.remove(live_h, 1) end
|
||||
avg_h[#avg_h + 1] = avg; if #avg_h > NPTS then table.remove(avg_h, 1) end
|
||||
local hi = st.duty_pct or 0
|
||||
for i = 1, #live_h do if live_h[i] > hi then hi = live_h[i] end end
|
||||
local t = nice_top(hi * 1.15)
|
||||
if t ~= top then
|
||||
top = t
|
||||
local v = { top, top // 2, 0 }
|
||||
for g = 1, 3 do if grid_lbl[g] then grid_lbl[g]:set(v[g] .. "%") end end
|
||||
end
|
||||
draw_chart(st.duty_pct)
|
||||
|
||||
big:set(string.format("%.1f%%", live)); big:color(load_col(live))
|
||||
local m = math.floor(wall)
|
||||
sub:set(string.format(tr("over %d:%02d"), m // 60, m % 60))
|
||||
tiles[1]:set(string.format("%.1f%%", avg))
|
||||
tiles[2]:set(string.format("%.1f%%", peak_pct)); tiles[2]:color(load_col(peak_pct))
|
||||
tiles[3]:set(string.format("%d s", tx_air))
|
||||
tiles[4]:set(string.format("%d s", rx_air))
|
||||
set_split(rx_air, tx_air)
|
||||
foot:set(string.format(narrow and tr("budget %d s tx %d rx %d") or tr("budget %d s tx %d rx %d err %d duty %d%% SF%d"),
|
||||
st.tx_budget_ms // 1000, st.tx_pkts, st.rx_pkts, st.rx_err, st.duty_pct, st.sf))
|
||||
|
||||
if widget and now - wlast >= 5000 then
|
||||
wlast = now
|
||||
spark[#spark + 1] = math.floor(live * 10)
|
||||
if #spark > 24 then table.remove(spark, 1) end
|
||||
widget.set{ title = tr("Channel busy"), value = string.format("%.1f%%", live),
|
||||
line = string.format(tr("avg %.1f%%, peak %.1f%%"), avg, peak_pct), spark = spark }
|
||||
end
|
||||
end
|
||||
|
||||
function app.on_open(w, h)
|
||||
W, H, narrow = w, h, w < 280
|
||||
LH12, LH16 = ui.text_h(12), ui.text_h(16)
|
||||
local LHB = ui.text_h(24)
|
||||
|
||||
-- the live figure, what it means, and Reset
|
||||
local y = 4
|
||||
big = ui.label("--", 6, y, 24, C.accent, true)
|
||||
local bw = (ui.text_w and ui.text_w("100.0%", 24) or 70) + 10
|
||||
local l1 = ui.label(tr("channel busy now"), 6 + bw, y + 2, 12, C.sub); l1:width(W - bw - 90)
|
||||
sub = ui.label("", 6 + bw, y + 2 + LH12, 12, C.sub); sub:width(W - bw - 90)
|
||||
ui.button(tr("Reset"), W - 76, y + 1, 70, math.max(28, LHB - 2), function() restart_window() end)
|
||||
y = y + math.max(LHB, 2 * LH12 + 4, 30) + 6
|
||||
|
||||
local gap = 4
|
||||
local labels = { tr("Average"), tr("Peak"), tr("TX"), tr("RX") }
|
||||
local tw = (W - 8 - gap * 3) // 4
|
||||
local th = LH12 + LH16 + 8
|
||||
for i, k in ipairs(labels) do
|
||||
local x = 4 + (i - 1) * (tw + gap)
|
||||
panel(x, y, tw, th)
|
||||
local kl = ui.label(k, x + 6, y + 3, 12, C.sub); kl:width(tw - 8)
|
||||
tiles[i] = ui.label("--", x + 6, y + 3 + LH12, 16, C.text, true)
|
||||
end
|
||||
y = y + th + 6
|
||||
|
||||
-- the chart, with % labels in the gutter
|
||||
local AX = (ui.text_w and ui.text_w("100%", 12) or 30) + 8
|
||||
local foot_h = LH12 + 4
|
||||
local split_h = LH12 + 6
|
||||
CH = H - y - split_h - foot_h - 18
|
||||
if CH > 120 then CH = 120 end
|
||||
if CH < 40 then CH = 40 end
|
||||
panel(AX, y, W - AX - 4, CH + 6)
|
||||
CW = W - AX - 10
|
||||
cv = ui.canvas(CW, CH); cv:pos(AX + 3, y + 3)
|
||||
NPTS = math.max(CW // 2, 2)
|
||||
local v = { top, top // 2, 0 }
|
||||
for g = 0, 2 do
|
||||
local l = ui.label(v[g + 1] .. "%", 0, y + 3 + g * (CH - 1) // 2 - LH12 // 2, 12, C.sub)
|
||||
l:width(AX - 6, "right")
|
||||
grid_lbl[g + 1] = l
|
||||
end
|
||||
y = y + CH + 6 + 6
|
||||
|
||||
-- the receive / transmit split
|
||||
ui.label(tr("Air split"), 6, y, 12, C.sub)
|
||||
local kw = (ui.text_w and ui.text_w(tr("Air split"), 12) or 60) + 10
|
||||
local lw = (ui.text_w and ui.text_w("rx 100% tx 100%", 12) or 110) + 4
|
||||
split_x0, split_y = 6 + kw, y + (LH12 - 8) // 2
|
||||
split_w = W - split_x0 - lw - 12
|
||||
if ui.panel and split_w > 20 then
|
||||
ui.panel(split_x0, split_y, split_w, 8, mix(C.sub, PANEL, 0.25), 4)
|
||||
split_rx = ui.panel(split_x0, split_y, 1, 8, C.accent, 4)
|
||||
split_tx = ui.panel(split_x0 + split_w - 1, split_y, 1, 8, AVG, 4)
|
||||
split_lbl = ui.label("", W - lw - 6, y, 12, C.text); split_lbl:width(lw, "right")
|
||||
else
|
||||
split_lbl = ui.label("", 6 + kw, y, 12, C.text); split_lbl:width(W - kw - 12)
|
||||
end
|
||||
y = y + split_h
|
||||
foot = ui.label("", 6, y, 12, C.sub); foot:width(W - 12)
|
||||
|
||||
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
|
||||
+79
-8
@@ -3,14 +3,14 @@
|
||||
{
|
||||
"id": "monitor",
|
||||
"name": "RF Monitor",
|
||||
"ver": "1.3",
|
||||
"desc": "Scrollable: RSSI/noise chart with dBm axis, link margin, RX rate, radio params, heard feed."
|
||||
"ver": "1.4",
|
||||
"desc": "RSSI, noise, margin and packet-rate tiles, a filled signal chart and the recently heard packets. Tap a packet for who sent it, its route, margin, size and airtime."
|
||||
},
|
||||
{
|
||||
"id": "airtime",
|
||||
"name": "Airtime",
|
||||
"ver": "1.4",
|
||||
"desc": "Utilization bars with a % axis, rx/tx split, duty ceiling, budget. Reset button top-right."
|
||||
"ver": "1.6",
|
||||
"desc": "How busy the channel is: the live load, average, peak, a filled chart with the duty ceiling and the rx/tx split. Keeps a Home widget with the channel load while it runs."
|
||||
},
|
||||
{
|
||||
"id": "snake",
|
||||
@@ -18,23 +18,39 @@
|
||||
"ver": "1.0",
|
||||
"desc": "The classic, in Lua. Swipe to steer."
|
||||
},
|
||||
{
|
||||
"id": "tetris",
|
||||
"name": "Tetris",
|
||||
"ver": "1.0",
|
||||
"desc": "The classic falling-block puzzle. Swipe left/right to move, up to rotate, down to hard-drop. Contributed by samuelcoustet.",
|
||||
"seed": false
|
||||
},
|
||||
{
|
||||
"id": "protreck",
|
||||
"name": "ProTreck",
|
||||
"ver": "1.0",
|
||||
"desc": "Outdoor watch: sunrise/sunset + moon phase (ASTRO), chronometer with laps (CHRONO), countdown timer with alarm (TIMER), altimeter with trend graph (ALTI). Swipe left/right to switch modes. Contributed by samuelcoustet.",
|
||||
"seed": false
|
||||
},
|
||||
{
|
||||
"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."
|
||||
"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.",
|
||||
"requires": "sdk_ext"
|
||||
},
|
||||
{
|
||||
"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."
|
||||
"desc": "The classic sliding-tile puzzle. Swipe to move, tap to restart. Keeps your best score. Contributed by pisti87.",
|
||||
"seed": false
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"ver": "1.2",
|
||||
"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, then plots the run back on the map so you can check coverage before driving it again. Reference app for wada.mesh.discover.",
|
||||
"requires": "sdk_ext"
|
||||
},
|
||||
{
|
||||
@@ -50,6 +66,61 @@
|
||||
"ver": "1.0",
|
||||
"desc": "Compass rose: magnetometer or GPS course, live fix, bearing to contacts",
|
||||
"seed": false
|
||||
},
|
||||
{
|
||||
"id": "sdscan",
|
||||
"name": "SD Scan",
|
||||
"ver": "1.1",
|
||||
"desc": "Finds and removes Windows malware on the SD card, like the M9 worm.",
|
||||
"requires": "sd",
|
||||
"seed": false
|
||||
},
|
||||
{
|
||||
"id": "tripodometer",
|
||||
"name": "Trip Odometer",
|
||||
"ver": "1.0",
|
||||
"desc": "GPS trip computer: distance, time, speed, altitude. Start/Stop to track, Reset to clear. Saves your trip across restarts. Contributed by samuelcoustet.",
|
||||
"requires": "gps",
|
||||
"seed": false
|
||||
},
|
||||
{
|
||||
"id": "breakout",
|
||||
"name": "Breakout",
|
||||
"ver": "1.0",
|
||||
"desc": "Classic brick-breaker. Swipe left/right to move the paddle, swipe up to launch the ball. Clear all bricks to advance levels. Contributed by samuelcoustet.",
|
||||
"seed": false
|
||||
},
|
||||
{
|
||||
"id": "ping",
|
||||
"name": "Ping",
|
||||
"ver": "1.0",
|
||||
"desc": "Send a mesh DM ping to any contact and measure round-trip time. Includes your GPS position so both sides see distance and bearing. Auto-replies to incoming pings. Contributed by samuelcoustet.",
|
||||
"requires": "sdk_ext"
|
||||
},
|
||||
{
|
||||
"id": "mecp",
|
||||
"name": "MECP",
|
||||
"ver": "1.0",
|
||||
"desc": "Emergency messages in MECP code: pick severity and codes, add people and position, post to a channel. Decodes MECP lines into words. Code table: MECP by xiang-dev-1, CC BY 4.0.",
|
||||
"requires": "sdk_ext",
|
||||
"seed": false
|
||||
},
|
||||
{
|
||||
"id": "music",
|
||||
"name": "Music",
|
||||
"ver": "1.0",
|
||||
"desc": "MP3 and WAV from the SD card's Music folder, shuffle, repeat.",
|
||||
"icon": "music",
|
||||
"requires": "audio",
|
||||
"seed": false
|
||||
},
|
||||
{
|
||||
"id": "blackjack",
|
||||
"name": "Blackjack",
|
||||
"ver": "1.0",
|
||||
"desc": "Casino Blackjack (21) with split, double down, 6-deck shoe, persistent bankroll, and T-Deck/T-Display P4 controls. Contributed by halflive-743.",
|
||||
"icon": "game",
|
||||
"seed": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"blackjack","name":"Blackjack","ver":"1.0","desc":"Casino Blackjack (21) with split, double down, 6-deck shoe, persistent bankroll, and auto-scaling UI for T-Deck & T-Display P4 (AMOLED/LCD).","icon":"game"}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
{"id":"breakout","name":"Breakout","ver":"1.0","desc":"Classic brick-breaker. Swipe left/right to move the paddle, swipe up to launch the ball. Clear all bricks to advance levels. Contributed by samuelcoustet."}
|
||||
@@ -0,0 +1,188 @@
|
||||
-- Breakout — wadamesh Lua app
|
||||
-- Swipe left/right to move paddle | Swipe up to launch
|
||||
-- Keyboard: arrows to move, Enter / Space to launch
|
||||
-- Contributed by samuelcoustet
|
||||
|
||||
local ui, sys, store, timer = wada.ui, wada.sys, wada.store, wada.timer
|
||||
local C = ui.colors
|
||||
local app = {}
|
||||
|
||||
local COLS_B, ROWS_B = 8, 5
|
||||
local BRICK_H, BRICK_GAP = 10, 2
|
||||
local PADDLE_H, PADDLE_W = 7, 44
|
||||
local BALL_R = 5
|
||||
local TOP_H = 18 -- score strip height
|
||||
local PAD_OFF = 22 -- paddle distance from bottom
|
||||
|
||||
local COLORS = { 0xff3355, 0xff7722, 0xffdd33, 0x22cc77, 0x3399ff }
|
||||
local PTS = { 7, 5, 4, 3, 1 }
|
||||
|
||||
local cv, W, H, brick_w
|
||||
local bricks, n_alive
|
||||
local bx, by, bdx, bdy, speed
|
||||
local paddle_x
|
||||
local score, hiscore, lives, level
|
||||
local launched, over
|
||||
|
||||
local function bk_x(c) return BRICK_GAP + (c-1)*(brick_w + BRICK_GAP) end
|
||||
local function bk_y(r) return TOP_H + 4 + (r-1)*(BRICK_H + BRICK_GAP) end
|
||||
local function pad_y() return H - PAD_OFF end
|
||||
|
||||
local function new_bricks()
|
||||
bricks = {}; n_alive = 0
|
||||
for r = 1, ROWS_B do
|
||||
bricks[r] = {}
|
||||
for c = 1, COLS_B do bricks[r][c] = true; n_alive = n_alive + 1 end
|
||||
end
|
||||
end
|
||||
|
||||
local function reset_ball()
|
||||
launched = false
|
||||
bx = paddle_x
|
||||
by = pad_y() - BALL_R - 1
|
||||
bdx = 0; bdy = 0
|
||||
end
|
||||
|
||||
local function launch_ball()
|
||||
local a = sys.random(-40, 40) * math.pi / 180.0
|
||||
bdx = speed * math.sin(a)
|
||||
bdy = -speed * math.cos(a)
|
||||
launched = true
|
||||
end
|
||||
|
||||
local function draw_all()
|
||||
cv:fill(0x0a0e14)
|
||||
cv:text(6, 3, string.format("Score:%d Best:%d Lv:%d", score, hiscore, level), C.text, 11)
|
||||
for i = 1, lives do
|
||||
cv:circle(W - 6 - (i-1)*13, TOP_H // 2, 4, C.good, true)
|
||||
end
|
||||
for r = 1, ROWS_B do
|
||||
for c = 1, COLS_B do
|
||||
if bricks[r][c] then
|
||||
cv:rect(bk_x(c), bk_y(r), brick_w, BRICK_H, COLORS[r], true, 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
cv:rect(paddle_x - PADDLE_W // 2, pad_y(), PADDLE_W, PADDLE_H, C.accent, true, 3)
|
||||
cv:circle(math.floor(bx), math.floor(by), BALL_R, 0xffffff, true)
|
||||
if not launched and not over then
|
||||
cv:text(W // 2 - 40, pad_y() - 20, "swipe up to launch", C.sub, 11)
|
||||
end
|
||||
if over then
|
||||
cv:rect(W // 2 - 62, H // 2 - 22, 124, 44, 0x141a22, true, 6)
|
||||
cv:text(W // 2 - 44, H // 2 - 14, "GAME OVER", C.bad, 14)
|
||||
cv:text(W // 2 - 34, H // 2 + 4, "tap to restart", C.sub, 11)
|
||||
end
|
||||
end
|
||||
|
||||
local function check_bricks()
|
||||
for r = 1, ROWS_B do
|
||||
for c = 1, COLS_B do
|
||||
if bricks[r][c] then
|
||||
local x1, y1 = bk_x(c), bk_y(r)
|
||||
local ox = math.min(bx + BALL_R - x1, x1 + brick_w - (bx - BALL_R))
|
||||
local oy = math.min(by + BALL_R - y1, y1 + BRICK_H - (by - BALL_R))
|
||||
if ox > 0 and oy > 0 then
|
||||
bricks[r][c] = false; n_alive = n_alive - 1
|
||||
score = score + PTS[r] * level
|
||||
if ox < oy then bdx = -bdx else bdy = -bdy end
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function step()
|
||||
if over or not launched then return end
|
||||
bx = bx + bdx
|
||||
by = by + bdy
|
||||
-- walls
|
||||
if bx - BALL_R < 0 then bx = BALL_R; bdx = math.abs(bdx) end
|
||||
if bx + BALL_R > W then bx = W - BALL_R; bdx = -math.abs(bdx) end
|
||||
if by - BALL_R < TOP_H then by = TOP_H + BALL_R; bdy = math.abs(bdy) end
|
||||
-- paddle
|
||||
local py = pad_y()
|
||||
if bdy > 0 and by + BALL_R >= py and by + BALL_R <= py + PADDLE_H + speed + 1 then
|
||||
local half = PADDLE_W * 0.5
|
||||
if bx >= paddle_x - half - BALL_R and bx <= paddle_x + half + BALL_R then
|
||||
local rel = math.max(-1.0, math.min(1.0, (bx - paddle_x) / half))
|
||||
bdx = speed * rel * 0.8
|
||||
local sq = speed * speed - bdx * bdx
|
||||
bdy = -(sq > 0 and math.sqrt(sq) or speed * 0.5)
|
||||
by = py - BALL_R - 1.0
|
||||
end
|
||||
end
|
||||
-- bricks
|
||||
check_bricks()
|
||||
-- ball lost
|
||||
if by - BALL_R > H + 10 then
|
||||
lives = lives - 1
|
||||
if lives <= 0 then
|
||||
if score > hiscore then hiscore = score; store.set("hiscore", hiscore) end
|
||||
over = true; launched = false; return
|
||||
end
|
||||
paddle_x = W // 2; reset_ball()
|
||||
end
|
||||
-- level won
|
||||
if n_alive == 0 then
|
||||
level = level + 1
|
||||
speed = math.min(speed + 0.6, 9.0)
|
||||
new_bricks(); reset_ball()
|
||||
end
|
||||
end
|
||||
|
||||
local function move_paddle(dx)
|
||||
paddle_x = math.max(PADDLE_W // 2 + 2, math.min(W - PADDLE_W // 2 - 2, paddle_x + dx))
|
||||
if not launched then bx = paddle_x end
|
||||
draw_all()
|
||||
end
|
||||
|
||||
function app.on_open(w, h)
|
||||
W, H = w, h
|
||||
hiscore = store.get("hiscore", 0)
|
||||
brick_w = math.floor((W - (COLS_B + 1) * BRICK_GAP) / COLS_B)
|
||||
cv = ui.canvas(W, H); cv:pos(0, 0)
|
||||
score = 0; lives = 3; level = 1; speed = 3.5; over = false
|
||||
paddle_x = W // 2
|
||||
new_bricks(); reset_ball()
|
||||
draw_all()
|
||||
timer.every(33)
|
||||
end
|
||||
|
||||
function app.on_input(ev)
|
||||
if over then
|
||||
if ev.type == "down" or (ev.type == "key" and ev.key == "enter") then
|
||||
over = false; score = 0; lives = 3; level = 1; speed = 3.5
|
||||
paddle_x = W // 2; new_bricks(); reset_ball(); draw_all()
|
||||
end
|
||||
return
|
||||
end
|
||||
if ev.type == "swipe" then
|
||||
local d = ev.dir
|
||||
if d == "left" then move_paddle(-32)
|
||||
elseif d == "right" then move_paddle( 32)
|
||||
elseif d == "up" and not launched then launch_ball(); draw_all()
|
||||
end
|
||||
elseif ev.type == "key" then
|
||||
local k = ev.key
|
||||
if k == "left" then move_paddle(-20)
|
||||
elseif k == "right" then move_paddle( 20)
|
||||
elseif (k == "up" or k == "enter" or k == " ") and not launched then
|
||||
launch_ball(); draw_all()
|
||||
end
|
||||
elseif ev.type == "down" then
|
||||
if ev.x < W // 3 then move_paddle(-32)
|
||||
elseif ev.x > (W * 2) // 3 then move_paddle( 32)
|
||||
elseif not launched then launch_ball(); draw_all()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function app.on_tick(dt)
|
||||
step(); draw_all()
|
||||
end
|
||||
|
||||
function app.on_close() end
|
||||
|
||||
return app
|
||||
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
Loaded 100 of 521 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user