Compare commits
No files matched your search
@@ -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
|
||||
+88
-8
@@ -14,10 +14,13 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
|
||||
| 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 |
|
||||
| Elecrow CrowPanel Advanced 10.1 | ESP32-P4 + ESP32-C6, external SX1262 | 10.1" EK79007 MIPI-DSI 1024x600 touch (GT911) | Web flasher (beta) | Experimental (new in beta_97), not yet tested on hardware | One image per board revision (V1.0, V1.1, V1.2); SX1262 in the wireless slot; no battery reading, sound or GPS |
|
||||
| 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
|
||||
@@ -33,12 +36,14 @@ 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**: dedicated `LilyGo_TDeck_Pro_companion_radio_touch` development
|
||||
target for the GDEQ031T10 e-paper model. It uses a monochrome shadow buffer,
|
||||
coalesced partial refreshes, CST328/CST3530 touch detection and the Pro-specific
|
||||
TCA8418 matrix. Keyboard events are drained during the panel BUSY interval so
|
||||
a refresh cannot overflow the controller FIFO. Hardware validation is pending;
|
||||
see [variants/lilygo_tdeck_pro/PORT.md](variants/lilygo_tdeck_pro/PORT.md).
|
||||
- **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
|
||||
@@ -79,12 +84,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
|
||||
|
||||
|
||||
@@ -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,24 @@ 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)
|
||||
- Elecrow CrowPanel Advanced 10.1 (ESP32-P4): built from `crowpanel_p4_101/` (ESP-IDF), one image per board revision with `WADA_CP_REV=1.0|1.1|1.2` (experimental, not yet tested on hardware; requires an SX1262 in the wireless slot)
|
||||
- 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,
|
||||
@@ -98,28 +101,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
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
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
|
||||
|
||||
@@ -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
|
||||
+46
-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,
|
||||
@@ -42,21 +42,21 @@ 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; inside a conversation, return to the chat list (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
|
||||
@@ -84,9 +84,18 @@ 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
|
||||
@@ -96,9 +105,10 @@ 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 **#** 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.
|
||||
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.
|
||||
@@ -110,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.
|
||||
@@ -118,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
|
||||
@@ -145,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
|
||||
@@ -160,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.
|
||||
+87
-59
@@ -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,9 +248,9 @@ 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 and follows the configured
|
||||
- **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
|
||||
@@ -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,8 @@
|
||||
# build output + component manager cache (project-local ESP-IDF comes in via the ../tanmatsu symlinks)
|
||||
build/
|
||||
managed_components/
|
||||
sdkconfig
|
||||
sdkconfig.old
|
||||
dependencies.lock
|
||||
# flashable images produced by build.sh merge
|
||||
*.bin
|
||||
@@ -0,0 +1,3 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(application)
|
||||
Executable
+204
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env bash
|
||||
# wadamesh / Elecrow CrowPanel Advanced 10.1" ESP32-P4 HMI build wrapper. EXPERIMENTAL.
|
||||
# Standalone ESP-IDF app modelled on tdisplay_p4/build.sh: reuses the Tanmatsu's project-local
|
||||
# ESP-IDF 5.5.1 (esp32p4) via the ../tanmatsu symlinks and the shared P4 components.
|
||||
# ./build.sh build # board revision V1.2 (default, newest)
|
||||
# WADA_CP_REV=1.1 ./build.sh build # V1.1 (SX1262 DIO1/RST on GPIO53/54)
|
||||
# WADA_CP_REV=1.0 ./build.sh build # V1.0 (same LoRa pins as V1.1, reversed 1-bit C6 SDIO)
|
||||
# ./build.sh merge # build + one flashable image at 0x0 (build/<target>/*-merged.bin)
|
||||
# ./build.sh flash -p /dev/cu.usbserial-XXXX
|
||||
# ./build.sh fullclean # remove this revision's build tree only
|
||||
set -e
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
CP_REV="${WADA_CP_REV:-1.2}"
|
||||
case "$CP_REV" in
|
||||
1.2) CP_SDIO_FRAG="sdio_v11" ;;
|
||||
1.1) CP_SDIO_FRAG="sdio_v11" ;;
|
||||
1.0) CP_SDIO_FRAG="sdio_v10" ;;
|
||||
*) echo "[build.sh] WADA_CP_REV must be 1.0, 1.1 or 1.2 (got '$CP_REV')" >&2; exit 1 ;;
|
||||
esac
|
||||
export WADA_CP_REV="$CP_REV"
|
||||
P4_BUILD_TARGET="crowpanel_p4_101_v${CP_REV/./}"
|
||||
P4_BUILD_DIR="$PROJECT_DIR/build/$P4_BUILD_TARGET"
|
||||
|
||||
# This project intentionally applies compatibility fixes inside managed_components
|
||||
# before every build. ESP-IDF's stock fullclean also runs remove_managed_components,
|
||||
# which rejects those expected hash changes and aborts. A P4 full clean therefore
|
||||
# removes only the generated CMake/Ninja tree; dependencies stay in place and the
|
||||
# idempotent patches below remain valid.
|
||||
if [ "$#" -eq 1 ] && [ "$1" = "fullclean" ]; then
|
||||
# Plain rm -rf: cmake is not on PATH before export.sh. Each revision has its own build tree
|
||||
# (and its own sdkconfig inside it), so this removes only the selected one.
|
||||
if [ -d "$P4_BUILD_DIR" ]; then
|
||||
rm -rf "${P4_BUILD_DIR:?}" || { echo "[build.sh] FAILED to remove $P4_BUILD_DIR" >&2; exit 1; }
|
||||
fi
|
||||
if [ -d "$P4_BUILD_DIR" ]; then
|
||||
echo "[build.sh] FAILED to remove $P4_BUILD_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[build.sh] removed build/$P4_BUILD_TARGET (patched managed components preserved)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Keep the baked-in translation + Lua app tables in step with deploy/apps/.
|
||||
# Same for the seeded Lua apps. The PlatformIO envs get this from a pre: hook;
|
||||
# the IDF builds need it here or the P4 boards ship stale copies.
|
||||
python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/pre_gen_baked.py"
|
||||
# LVGL is vendored (gitignored) by fetch-deps.sh, so a copy vendored before the
|
||||
# anim_timer use-after-free fix (#428) still needs it. Idempotent; fails on drift.
|
||||
python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/patch_lvgl_anim_uaf.py" \
|
||||
--patch-file "$(cd "$(dirname "$0")" && pwd)/components/lvgl/upstream/src/misc/lv_anim.c"
|
||||
python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/patch_lvgl_arabic.py" \
|
||||
--patch-file "$(cd "$(dirname "$0")" && pwd)/components/lvgl/upstream/src/misc/lv_txt_ap.c"
|
||||
cd "$PROJECT_DIR"
|
||||
export IDF_TOOLS_PATH="$PWD/esp-idf-tools"
|
||||
# VS Code may launch this wrapper from PlatformIO's virtualenv. Pin the
|
||||
# project-local IDF environment before export.sh inspects that unrelated Python.
|
||||
if [ -z "${IDF_PYTHON_ENV_PATH:-}" ]; then
|
||||
for idf_py_env in "$IDF_TOOLS_PATH"/python_env/idf5.5_py*_env; do
|
||||
if [ -x "$idf_py_env/bin/python" ]; then
|
||||
export IDF_PYTHON_ENV_PATH="$idf_py_env"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
unset VIRTUAL_ENV CONDA_PREFIX
|
||||
# shellcheck disable=SC1091
|
||||
source esp-idf/export.sh >/dev/null 2>&1
|
||||
|
||||
WADA_FW_TAG="${WADA_FW_TAG:-$(git describe --tags --match 'beta_*' --always 2>/dev/null || echo dev)}"
|
||||
WADA_FW_DATE="$(date '+%-d %b %Y')"
|
||||
IDF_ARGS=(-B "build/$P4_BUILD_TARGET" \
|
||||
-DDEVICE=crowpanel_p4_101 \
|
||||
-DSDKCONFIG="$P4_BUILD_DIR/sdkconfig" \
|
||||
-DSDKCONFIG_DEFAULTS="sdkconfigs/general;sdkconfigs/wadamesh;sdkconfigs/crowpanel_p4_101;sdkconfigs/$CP_SDIO_FRAG" \
|
||||
-DWADA_FW_TAG="$WADA_FW_TAG" -DWADA_FW_DATE="$WADA_FW_DATE" \
|
||||
-DIDF_TARGET=esp32p4)
|
||||
|
||||
# A fresh clone has no managed_components yet. Configure once to download them,
|
||||
# then apply the compatibility patches below before the first compilation.
|
||||
if [ ! -f managed_components/espressif__libsodium/CMakeLists.txt ]; then
|
||||
idf.py "${IDF_ARGS[@]}" reconfigure
|
||||
fi
|
||||
|
||||
# --- Build-time patch: libsodium forced includes in paths with spaces -----------------------------
|
||||
# The managed component emits `SHELL:-include <absolute path>`. CMake leaves that
|
||||
# path unquoted in Ninja, so a checkout such as "T7 Shield" reaches GCC as two
|
||||
# input files. GCC accepts the joined -include<path> form, which cannot split.
|
||||
SODIUM_CMAKE="managed_components/espressif__libsodium/CMakeLists.txt"
|
||||
if [ -f "$SODIUM_CMAKE" ] && grep -q 'SHELL:-include' "$SODIUM_CMAKE"; then
|
||||
sed -i '' -e 's|SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}|-include${CMAKE_CURRENT_SOURCE_DIR}|g' \
|
||||
-e 's|SHELL:-include${CMAKE_CURRENT_SOURCE_DIR}|-include${CMAKE_CURRENT_SOURCE_DIR}|g' "$SODIUM_CMAKE"
|
||||
echo "[build.sh] patched libsodium forced includes (space-safe paths)"
|
||||
fi
|
||||
|
||||
# --- Build-time patch: SD_MMC internal pull-ups (P4 boards) ------------------------------------------
|
||||
# The board has no external pull-ups on the SD data lines; the IDF sdmmc host explicitly FLOATS the
|
||||
# pads unless SDMMC_SLOT_FLAG_INTERNAL_PULLUP is set (so pre-begin gpio_pullup_en gets undone).
|
||||
# Meck-P4's working SD init sets the flag; Arduino's SD_MMC never does and has no API for it. Patch
|
||||
# the P4's own managed copy. Idempotent.
|
||||
SDMMC_C="managed_components/espressif__arduino-esp32/libraries/SD_MMC/src/SD_MMC.cpp"
|
||||
if [ -f "$SDMMC_C" ] && ! grep -q 'wadamesh P4: internal pullups' "$SDMMC_C"; then
|
||||
sed -i '' 's|sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();|sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();\n slot_config.flags \|= SDMMC_SLOT_FLAG_INTERNAL_PULLUP; // wadamesh P4: internal pullups (no external ones on the SD lines)|' "$SDMMC_C"
|
||||
echo "[build.sh] patched arduino SD_MMC.cpp (internal pull-ups on the SD slot)"
|
||||
fi
|
||||
# --- Build-time patch: a dead C6 must not spin the SDIO rx task -----------------------------------
|
||||
# With CONFIG_ESP_HOSTED_TRANSPORT_RESTART_ON_FAILURE=n (see sdkconfigs/tdisplay_p4 for why it is
|
||||
# off), sdio_read_task posts ESP_HOSTED_EVENT_TRANSPORT_FAILURE and then `continue`s: straight back
|
||||
# into a register read that is going to fail again. On a unit whose C6 runs factory ESP-AT the
|
||||
# transport NEVER comes up, so that is an unbounded retry storm: measured at ~16 failed reads per
|
||||
# second, forever, with the whole transport re-initialising on top of it. It floods the USB-CDC the
|
||||
# companion app talks over and keeps a task busy for the life of the session.
|
||||
#
|
||||
# Turning the restart back on is not the answer: that is the boot loop this board had before.
|
||||
# Both are the same mistake: a co-processor that cannot be reached is a permanent condition, and
|
||||
# the driver treats it as transient. Park the task instead. The failure event has already been
|
||||
# posted, Wi-Fi and BLE are simply unavailable, and everything else on the device keeps running.
|
||||
# Idempotent.
|
||||
HOSTED_SDIO="managed_components/nicolaielectronics__esp-hosted-tanmatsu/host/drivers/transport/sdio/sdio_drv.c"
|
||||
if [ -f "$HOSTED_SDIO" ] && ! grep -q 'wadamesh: park instead of spinning' "$HOSTED_SDIO"; then
|
||||
python3 - "$HOSTED_SDIO" <<'PYEOF'
|
||||
import re, sys
|
||||
p = sys.argv[1]
|
||||
s = open(p).read()
|
||||
# Both failure arms look identical apart from the log line: event post, optional restart, continue.
|
||||
old = """#if H_TRANSPORT_RESTART_ON_FAILURE
|
||||
ESP_LOGI(TAG, "Host is resetting itself, to avoid any sdio race condition");
|
||||
g_h.funcs->_h_restart_host();
|
||||
#endif
|
||||
continue;"""
|
||||
new = """#if H_TRANSPORT_RESTART_ON_FAILURE
|
||||
ESP_LOGI(TAG, "Host is resetting itself, to avoid any sdio race condition");
|
||||
g_h.funcs->_h_restart_host();
|
||||
#else
|
||||
/* wadamesh: park instead of spinning. The transport is down and we are
|
||||
* not allowed to reboot the host, so retrying can only fail again at
|
||||
* full speed. The failure event is already posted; stop here and let the
|
||||
* rest of the device run without Wi-Fi/BLE. */
|
||||
ESP_LOGE(TAG, "transport down, host restart disabled: parking rx task (Wi-Fi/BLE unavailable)");
|
||||
while (1) g_h.funcs->_h_msleep(60000);
|
||||
#endif
|
||||
continue;"""
|
||||
n = s.count(old)
|
||||
if n == 0:
|
||||
sys.exit("esp-hosted sdio_drv.c: failure arm not found; patch needs updating")
|
||||
s = s.replace(old, new)
|
||||
open(p, "w").write(s)
|
||||
print(" patched %d failure arm(s)" % n)
|
||||
PYEOF
|
||||
echo "[build.sh] patched esp-hosted sdio_drv.c (park the rx task on a dead transport)"
|
||||
fi
|
||||
|
||||
# ⚠️ The P4/slot-0 branch REBUILDS slot_config from scratch with `.flags = 0`, silently discarding
|
||||
# the flag added above (that's the branch that actually runs with BOARD_SDMMC_SLOT=0): so patch that
|
||||
# struct literal too. Without the flag the IDF host explicitly FLOATS the pads -> deterministic 0x109
|
||||
# CRC at the first data read regardless of card/frequency.
|
||||
if [ -f "$SDMMC_C" ] && ! grep -q 'wadamesh P4: slot0 pullups' "$SDMMC_C"; then
|
||||
sed -i '' 's|^ .flags = 0,$| .flags = SDMMC_SLOT_FLAG_INTERNAL_PULLUP, // wadamesh P4: slot0 pullups (struct rebuilt here; the earlier flag is discarded)|' "$SDMMC_C"
|
||||
echo "[build.sh] patched arduino SD_MMC.cpp (slot-0 struct pull-up flag)"
|
||||
fi
|
||||
|
||||
# --- Build-time patch: this board's C6 ships esp-hosted, so Arduino's hostedInit() must be the real
|
||||
# one. A managed_components tree copied from a legacy ESP-AT T-Display build may carry the stub.
|
||||
HOSTED_C="managed_components/espressif__arduino-esp32/cores/esp32/esp32-hal-hosted.c"
|
||||
if [ -f "$HOSTED_C" ] && grep -q 'wadamesh P4: never start esp_hosted' "$HOSTED_C"; then
|
||||
sed -i '' 's|^static bool hostedInit() { return false; // wadamesh P4: never start esp_hosted.*$|static bool hostedInit() {|' "$HOSTED_C"
|
||||
echo "[build.sh] restored arduino hostedInit() (hosted C6 build)"
|
||||
fi
|
||||
BLEDEV_CPP="managed_components/espressif__arduino-esp32/libraries/BLE/src/BLEDevice.cpp"
|
||||
if [ -f "$BLEDEV_CPP" ] && grep -q 'int rc = ble_gap_read_local_irk(irk);' "$BLEDEV_CPP"; then
|
||||
sed -i '' 's|int rc = ble_gap_read_local_irk(irk);|int rc = 0; (void)irk; // wadamesh: arduino BLE unused; symbol renamed in IDF 5.5 NimBLE|' "$BLEDEV_CPP"
|
||||
echo "[build.sh] patched arduino BLEDevice.cpp (stub renamed ble_gap_read_local_irk)"
|
||||
fi
|
||||
|
||||
# --- Build-time patch: neutralize esp-hosted's auto-init constructor (P4-ONLY) --------------------
|
||||
# Initialization must not run before app_main: the C6 is brought up lazily through Arduino
|
||||
# hostedInit() after the UI, so a C6 that does not answer can only cost Wi-Fi/BLE.
|
||||
# Two copies get pulled in (espressif__esp_hosted via arduino-esp32, and the nicolaielectronics__
|
||||
# esp-hosted-tanmatsu fork via tanmatsu-wifi/badge-bsp). The build actually links the fork, so patch
|
||||
# BOTH so whichever is linked never grabs the SDIO.
|
||||
for HDIR in espressif__esp_hosted nicolaielectronics__esp-hosted-tanmatsu; do
|
||||
HINIT_C="managed_components/$HDIR/host/port/esp/freertos/src/port_esp_hosted_host_init.c"
|
||||
if [ -f "$HINIT_C" ] && grep -q 'ESP_ERROR_CHECK(esp_hosted_init());' "$HINIT_C"; then
|
||||
sed -i '' 's|ESP_ERROR_CHECK(esp_hosted_init());|/* wadamesh P4: C6 initialized lazily via hostedInit() after the UI */|' "$HINIT_C"
|
||||
echo "[build.sh] neutralized $HDIR auto-init constructor (defer until board power)"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$#" -eq 1 ] && [ "$1" = "merge" ]; then
|
||||
idf.py "${IDF_ARGS[@]}" build
|
||||
OUT="$P4_BUILD_DIR/wadamesh-crowpanel-p4-101-v${CP_REV/./}-merged.bin"
|
||||
# Same offsets and flash settings as scripts/build/p4-release.sh (bootloader 0x2000 on the P4).
|
||||
python -m esptool --chip esp32p4 merge_bin --flash_mode dio --flash_size 16MB --flash_freq 80m \
|
||||
-o "$OUT" \
|
||||
0x2000 "$P4_BUILD_DIR/bootloader/bootloader.bin" \
|
||||
0x8000 "$P4_BUILD_DIR/partition_table/partition-table.bin" \
|
||||
0xf000 "$P4_BUILD_DIR/ota_data_initial.bin" \
|
||||
0x20000 "$P4_BUILD_DIR/application.bin"
|
||||
echo "[build.sh] merged image: $OUT (flash at 0x0)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec idf.py "${IDF_ARGS[@]}" "$@"
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../tdisplay_p4/components/RadioLib
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../tanmatsu/components/ardlibs
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../tanmatsu/components/esp_hosted
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../tanmatsu/components/lvgl
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../tdisplay_p4/components/meshcore
|
||||
@@ -0,0 +1,22 @@
|
||||
# The wadamesh app layer (MyMesh + DataStore + helpers + transports + the LVGL UITask), compiled from
|
||||
# the repo's src/ plus variants/crowpanel_p4_101/. Mirrors tdisplay_p4/components/wadamesh_app, minus
|
||||
# the ESP-AT c6_at component (this board's C6 ships esp-hosted firmware).
|
||||
set(REPO "${CMAKE_CURRENT_LIST_DIR}/../../..")
|
||||
set(APP "${REPO}/src")
|
||||
file(GLOB_RECURSE APP_SRCS "${APP}/*.cpp" "${APP}/*.c" "${REPO}/variants/crowpanel_p4_101/*.cpp" "${REPO}/variants/crowpanel_p4_101/*.c")
|
||||
list(FILTER APP_SRCS EXCLUDE REGEX "/main\\.cpp$")
|
||||
list(FILTER APP_SRCS EXCLUDE REGEX "/helpers/input/(HeltecV4CapTouch|TDeckKeyboard|TDeckTouch|TDeckTrackball|RakTapV2Touch)\\.cpp$")
|
||||
|
||||
file(GLOB LUA_SRCS "${REPO}/lib/lua/src/*.c")
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${APP_SRCS} ${LUA_SRCS}
|
||||
INCLUDE_DIRS "${APP}" "${APP}/ui-touch" "${REPO}/include" "${REPO}/variants/crowpanel_p4_101" "${REPO}/lib/lua/src"
|
||||
REQUIRES espressif__arduino-esp32 meshcore lvgl ardlibs RadioLib nvs_flash esp_lcd fatfs
|
||||
esp_driver_i2c esp_driver_gpio esp_driver_ledc esp_hosted h2zero__esp-nimble-cpp bt
|
||||
soc esp_hw_support app_update driver esp_timer esp_partition spi_flash esp_adc esp_pm espcoredump
|
||||
espressif__esp_lcd_ek79007 esp_mm # esp_mm: esp_cache_msync (framebuffer clear)
|
||||
WHOLE_ARCHIVE
|
||||
)
|
||||
target_compile_options(${COMPONENT_LIB} PRIVATE -w
|
||||
-include "${REPO}/variants/tdisplay_p4/tdisplay_p4_compat.h") # adcAttachPin() shim (arduino 3.x dropped it)
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../tanmatsu/esp-idf
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../tanmatsu/esp-idf-tools
|
||||
@@ -0,0 +1,94 @@
|
||||
idf_build_get_property(idf_target IDF_TARGET)
|
||||
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"main.cpp"
|
||||
PRIV_REQUIRES
|
||||
esp_lcd # MIPI-DSI (EK79007)
|
||||
esp_driver_i2c
|
||||
esp_driver_gpio
|
||||
fatfs # SD_MMC storage
|
||||
nvs_flash
|
||||
bt # NimBLE companion (BLE over the C6)
|
||||
espressif__arduino-esp32
|
||||
esp_hosted # ESP32-C6 Wi-Fi/BLE over SDIO slot 1
|
||||
meshcore
|
||||
lvgl
|
||||
RadioLib # target.h (included by main.cpp) pulls <RadioLib.h>
|
||||
wadamesh_app
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
)
|
||||
|
||||
# Same warning policy as tdisplay_p4/main: vendored code must not fail the build on warnings.
|
||||
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=unused-variable" APPEND)
|
||||
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=unused-const-variable" APPEND)
|
||||
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=stringop-truncation" APPEND)
|
||||
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=stringop-overflow" APPEND)
|
||||
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=array-bounds" APPEND)
|
||||
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=restrict" APPEND)
|
||||
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=maybe-uninitialized" APPEND)
|
||||
idf_build_get_property(_co COMPILE_OPTIONS)
|
||||
list(REMOVE_ITEM _co "-Werror=all")
|
||||
idf_build_set_property(COMPILE_OPTIONS "${_co}")
|
||||
|
||||
# ---- Elecrow CrowPanel Advanced 10.1" ESP32-P4 (EXPERIMENTAL) app define set. -------------------
|
||||
# Pins: Elecrow schematics V1.0/V1.1/V1.2 + their ESP-IDF lessons; per-pin status and sources in
|
||||
# the pin table at the top of variants/crowpanel_p4_101/target.h.
|
||||
#
|
||||
# Board revision (build.sh WADA_CP_REV=1.0|1.1|1.2, default 1.2). Only two things differ:
|
||||
# - the SX1262 DIO1/NRST pair (J9.1 / J11.1): GPIO53/54 on V1.0 + V1.1, GPIO27/28 on V1.2;
|
||||
# - the C6 SDIO data-pin order (sdkconfig fragment, build.sh).
|
||||
if(NOT DEFINED ENV{WADA_CP_REV} OR "$ENV{WADA_CP_REV}" STREQUAL "1.2")
|
||||
set(CP_REV_DEFS CP_REV=12 P_LORA_DIO_1=27 P_LORA_RESET=28)
|
||||
message(STATUS "wadamesh: CrowPanel P4 10.1 board revision V1.2")
|
||||
elseif("$ENV{WADA_CP_REV}" STREQUAL "1.1")
|
||||
set(CP_REV_DEFS CP_REV=11 P_LORA_DIO_1=53 P_LORA_RESET=54)
|
||||
message(STATUS "wadamesh: CrowPanel P4 10.1 board revision V1.1")
|
||||
elseif("$ENV{WADA_CP_REV}" STREQUAL "1.0")
|
||||
set(CP_REV_DEFS CP_REV=10 P_LORA_DIO_1=53 P_LORA_RESET=54)
|
||||
message(STATUS "wadamesh: CrowPanel P4 10.1 board revision V1.0")
|
||||
else()
|
||||
message(FATAL_ERROR "WADA_CP_REV must be 1.0, 1.1 or 1.2 (got '$ENV{WADA_CP_REV}')")
|
||||
endif()
|
||||
|
||||
set(WADA_DEFS
|
||||
HAS_CROWPANEL_P4_101=1 ESP32_PLATFORM MC_VENDORED_TOUCH_APP UI_LVGL=1 HAS_TOUCH_UI=1
|
||||
${CP_REV_DEFS}
|
||||
USE_SX1262 RADIO_CLASS=CustomSX1262 WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
SX126X_RX_BOOSTED_GAIN=1 SX126X_CURRENT_LIMIT=140
|
||||
SX126X_DIO2_AS_RF_SWITCH=true SX126X_DIO3_TCXO_VOLTAGE=1.6f # Elecrow Lesson14: RadioLib defaults + 1.6 V TCXO
|
||||
MULTI_TRANSPORT_COMPANION TCP_PORT=5000 WS_PORT=8765 BLE_PIN_CODE=123456
|
||||
LORA_FREQ=869.618 LORA_BW=62.5 LORA_SF=8 # EU 868 default like the other P4 boards; region is a setting
|
||||
LORA_TX_POWER=22 MAX_LORA_TX_POWER=22
|
||||
MAX_CONTACTS=2000 MAX_GROUP_CHANNELS=40 # 32 MB PSRAM
|
||||
ENABLE_PRIVATE_KEY_IMPORT=1 ENABLE_PRIVATE_KEY_EXPORT=1
|
||||
|
||||
# --- SX1262 on the external wireless header J9/J11 (SPI3). DIO1/NRST from the revision block. ---
|
||||
P_LORA_NSS=10 P_LORA_BUSY=9 P_LORA_SCLK=8 P_LORA_MISO=7 P_LORA_MOSI=6
|
||||
|
||||
# --- Display: EK79007 1024x600 MIPI-DSI, native landscape, LVGL at native resolution. ---
|
||||
DISPLAY_CLASS=EK79007Display SCREEN_WIDTH=1024 SCREEN_HEIGHT=600
|
||||
|
||||
# --- Touch GT911 + shared board I2C0 (codec, HY2.0 port). Wire is begun by ESP32Board::begin. ---
|
||||
PIN_BOARD_SDA=45 PIN_BOARD_SCL=46 PIN_TOUCH_SDA=45 PIN_TOUCH_SCL=46 PIN_TOUCH_RST=40 PIN_TOUCH_INT=42
|
||||
|
||||
# --- microSD: SDMMC slot 0, 1-bit (only D0 is wired): CLK43 / CMD44 / D0 39. C6 is slot 1. ---
|
||||
BOARD_HAS_SDMMC=1 BOARD_SDMMC_SLOT=0
|
||||
)
|
||||
foreach(_d ${WADA_DEFS})
|
||||
idf_build_set_property(COMPILE_OPTIONS "-D${_d}" APPEND)
|
||||
endforeach()
|
||||
|
||||
# Firmware version/tag (build.sh passes WADA_FW_TAG/-DATE from git describe + date).
|
||||
if(NOT WADA_FW_TAG)
|
||||
set(WADA_FW_TAG "dev")
|
||||
endif()
|
||||
if(NOT WADA_FW_DATE)
|
||||
set(WADA_FW_DATE "")
|
||||
endif()
|
||||
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_RELEASE_TAG=\"${WADA_FW_TAG}\"" APPEND)
|
||||
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_BUILD_DATE=\"${WADA_FW_DATE}\"" APPEND)
|
||||
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_VERSION=\"wadamesh ${WADA_FW_TAG}\"" APPEND)
|
||||
# NOTE: no FIRMWARE_OTA_ENV on purpose. This is an experimental board with no release artifact on
|
||||
# firmware.wadamesh.com yet, so the on-device update UI stays hidden (CAP_OTA 0 in device_caps.h).
|
||||
@@ -0,0 +1,12 @@
|
||||
dependencies:
|
||||
idf: ">=5.5.1"
|
||||
# Same component set as the T-Display P4 (esp-hosted 2.12.3 fork via tanmatsu-wifi, arduino-esp32
|
||||
# for the shared Arduino src/, esp-nimble-cpp for BLE over the C6). badge-bsp is not initialised on
|
||||
# this board; it is here because tanmatsu-wifi depends on it.
|
||||
badgeteam/badge-bsp: "^0.9.9"
|
||||
nicolaielectronics/tanmatsu-wifi: "^1.1.2"
|
||||
nicolaielectronics/tanmatsu-lora: "^0.3.0"
|
||||
espressif/arduino-esp32: "3.3.10"
|
||||
h2zero/esp-nimble-cpp: "^2.3.0"
|
||||
# EK79007 MIPI-DSI panel driver (Elecrow's lessons pin ^1.0.2).
|
||||
espressif/esp_lcd_ek79007: "^1.0.2"
|
||||
@@ -0,0 +1,238 @@
|
||||
// wadamesh on the Elecrow CrowPanel Advanced 10.1" ESP32-P4 HMI (1024x600). EXPERIMENTAL.
|
||||
//
|
||||
// Standalone ESP-IDF app, modelled on tdisplay_p4/main/main.cpp (same P4 + C6 esp-hosted pairing,
|
||||
// same storage law: hot store on the internal LittleFS 'storage' partition, the microSD for tiles and
|
||||
// bulk). The board has no I/O expander, so bring-up is: on-chip LDO rails -> EK79007 panel -> radio
|
||||
// -> storage -> UI, and the C6 last. Board globals + radio_init() live in
|
||||
// variants/crowpanel_p4_101/target.cpp. Pin map: the table in variants/crowpanel_p4_101/target.h.
|
||||
#include "../../variants/tdisplay_p4/tdisplay_p4_compat.h" // adcAttachPin() shim, BEFORE target.h
|
||||
#include <Arduino.h>
|
||||
#include <Mesh.h>
|
||||
#include "MyMesh.h"
|
||||
#include <new>
|
||||
#include "esp_heap_caps.h"
|
||||
#include "UITask.h"
|
||||
#include "target.h"
|
||||
#include <LittleFS.h>
|
||||
#include <SD_MMC.h>
|
||||
#include <WiFi.h>
|
||||
#include "../../src/helpers/esp32/MultiTransportCompanionInterface.h"
|
||||
#include <helpers/esp32/WifiRuntimeStore.h>
|
||||
#include <helpers/esp32/TouchPrefsStore.h>
|
||||
#include "../../src/helpers/esp32/BootTimeSync.h"
|
||||
#include "esp_hosted.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_event.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "lvgl.h"
|
||||
|
||||
#ifndef TCP_PORT
|
||||
#define TCP_PORT 5000
|
||||
#endif
|
||||
#ifndef WS_PORT
|
||||
#define WS_PORT 8765
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_BT_NIMBLE_TRANSPORT_UART)
|
||||
#error "CrowPanel P4 C6 requires NimBLE hosted HCI; disable CONFIG_BT_NIMBLE_TRANSPORT_UART"
|
||||
#endif
|
||||
|
||||
extern "C" bool hostedInitBLE(); // arduino-esp32 BLE controller bring-up over esp-hosted
|
||||
extern "C" bool hostedSetPins(int8_t clk, int8_t cmd, int8_t d0, int8_t d1, int8_t d2, int8_t d3, int8_t rst);
|
||||
|
||||
// C6 SDIO pin set. Arduino's hostedInit() does NOT take these from sdkconfig: it passes the generic
|
||||
// esp32p4 variant's BOARD_SDIO_ESP_HOSTED_* (CLK18 CMD19 D0-D3 = 14,15,16,17, RST 54) to
|
||||
// esp_hosted_sdio_set_config(), which overrides Kconfig. That order happens to match the T-Display
|
||||
// P4 and the CrowPanel V1.0, but V1.1/V1.2 route D0..D3 to GPIO17..14, and C6_EN is GPIO32 on every
|
||||
// revision. Must run before anything can start esp-hosted (Wi-Fi, BLE, boot time sync).
|
||||
// The bus width (4-bit, or 1-bit on V1.0) still comes from the sdkconfig fragment.
|
||||
static void crowpanelC6Pins() {
|
||||
#if CP_REV == 10
|
||||
const bool ok = hostedSetPins(18, 19, 14, 15, 16, 17, 32);
|
||||
#else
|
||||
const bool ok = hostedSetPins(18, 19, 17, 16, 15, 14, 32);
|
||||
#endif
|
||||
printf("[BOOT] C6 SDIO pins set for V%d.%d: %s\n", CP_REV / 10, CP_REV % 10, ok ? "ok" : "FAILED");
|
||||
}
|
||||
|
||||
// Boot-trace hooks the shared code defines on the S3 boards (src/main.cpp, excluded here).
|
||||
volatile int g_boot_phase = 0;
|
||||
extern "C" void set_boot_phase(int phase) { g_boot_phase = phase; }
|
||||
|
||||
// App globals: src/main.cpp declares these on the S3 boards; this build owns them.
|
||||
DataStore store(LittleFS, rtc_clock);
|
||||
bool g_fs_ok = false; // internal 'storage' (LittleFS) mounted
|
||||
bool g_sd_ok = false; // microSD mounted
|
||||
MultiTransportCompanionInterface serial_interface;
|
||||
StdRNG fast_rng;
|
||||
SimpleMeshTables tables;
|
||||
UITask ui_task(&board, &serial_interface);
|
||||
static MyMesh& makeTheMesh() {
|
||||
void* mem = heap_caps_malloc(sizeof(MyMesh), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (!mem) mem = malloc(sizeof(MyMesh));
|
||||
return *new (mem) MyMesh(radio_driver, fast_rng, rtc_clock, tables, store, &ui_task);
|
||||
}
|
||||
MyMesh& the_mesh = makeTheMesh();
|
||||
|
||||
static void wadameshSetup() {
|
||||
Serial.begin(115200);
|
||||
delay(150);
|
||||
printf("[BOOT] wadamesh / CrowPanel P4 10.1 (board revision V%d.%d, EXPERIMENTAL)\n", CP_REV / 10, CP_REV % 10);
|
||||
|
||||
// 1. On-chip LDO rails (DSI PHY + the 3.3 V pad domain of touch/LCD-reset/SD), backlight + amp off.
|
||||
crowpanelP4PowerOn();
|
||||
|
||||
// 2. Shared I2C0 (GT911, codec, HY2.0 port) on 45/46.
|
||||
board.begin();
|
||||
|
||||
// 3. EK79007 panel, before LVGL flushes to it.
|
||||
if (!display.begin()) printf("[BOOT] EK79007 display init FAILED\n");
|
||||
|
||||
// 4. lwIP/tcpip + default event loop before any Wi-Fi/netif call (esp_wifi_remote needs it).
|
||||
esp_netif_init();
|
||||
esp_event_loop_create_default();
|
||||
crowpanelC6Pins();
|
||||
printf("[BOOT] C6 = esp-hosted (lazy Wi-Fi/BLE initialization)\n");
|
||||
|
||||
// 5. Radio (SX1262 on J9/J11).
|
||||
if (!radio_init()) printf("[BOOT] radio_init FAILED\n");
|
||||
|
||||
DisplayDriver* disp = &display;
|
||||
|
||||
// 6. Storage: internal LittleFS 'storage' = the hot store; the microSD = tiles, backups, bulk.
|
||||
g_fs_ok = LittleFS.begin(true, "/lfs", 10, "storage");
|
||||
printf("[storage] LittleFS(storage) = %s\n", g_fs_ok ? "OK" : "FAILED");
|
||||
if (g_fs_ok) { LittleFS.mkdir("/identity"); LittleFS.mkdir("/bl"); }
|
||||
// microSD on SDMMC slot 0, 1-bit: the J5 slot only has D0 routed (CLK43 / CMD44 / D0 39), as in
|
||||
// Elecrow's Lesson08. Pad power is LDO4, which crowpanelP4PowerOn() already holds, so SD_MMC must
|
||||
// not try to claim it again (-1 = external power). The card has pull-ups on CMD/D0 (R27/R28); the
|
||||
// build also enables the internal ones (build.sh patch), as Elecrow does.
|
||||
SD_MMC.setPowerChannel(-1);
|
||||
SD_MMC.setPins(43, 44, 39);
|
||||
for (int p : {44, 39}) gpio_pullup_en((gpio_num_t)p);
|
||||
{
|
||||
struct { int khz; const char* tag; } tries[] = {
|
||||
{ SDMMC_FREQ_DEFAULT, "1-bit 20MHz" },
|
||||
{ 10000, "1-bit 10MHz (Elecrow)" },
|
||||
{ 5000, "1-bit 5MHz" },
|
||||
};
|
||||
sdMountDiagBegin();
|
||||
uint32_t mounted_hz = 0;
|
||||
for (auto& t : tries) {
|
||||
const bool begin_ok = SD_MMC.begin("/sdcard", true, false, t.khz);
|
||||
g_sd_ok = begin_ok && SD_MMC.cardType() != CARD_NONE;
|
||||
sdMountDiagAttempt((uint32_t)t.khz * 1000u, begin_ok, g_sd_ok);
|
||||
printf("[storage] SD_MMC try %s -> %s\n", t.tag, g_sd_ok ? "OK" : "fail");
|
||||
if (g_sd_ok) { mounted_hz = (uint32_t)t.khz * 1000u; break; }
|
||||
SD_MMC.end();
|
||||
delay(120);
|
||||
}
|
||||
sdMountDiagSetMounted(g_sd_ok, mounted_hz);
|
||||
}
|
||||
printf("[storage] SD_MMC = %s\n", g_sd_ok ? "OK" : "no card");
|
||||
store.begin(); // internal LittleFS is the DataStore root (P4 storage law)
|
||||
|
||||
the_mesh.begin(disp != NULL);
|
||||
|
||||
wifiConfigBegin(); // NVS only, no radio
|
||||
|
||||
serial_interface.begin(Serial, TCP_PORT, WS_PORT);
|
||||
serial_interface.setBroadcastResponses(true);
|
||||
the_mesh.startInterface(serial_interface);
|
||||
|
||||
sensors.begin();
|
||||
ui_task.begin(disp, &sensors, the_mesh.getNodePrefs());
|
||||
board.onBootComplete();
|
||||
printf("[BOOT] setup done\n");
|
||||
|
||||
// ---- C6 bring-up, AFTER the UI ----------------------------------------------------------------
|
||||
// A C6 that does not answer (wrong firmware, wrong SDIO pin set for the revision) must cost Wi-Fi
|
||||
// and BLE only: display, touch and LoRa are already up by now.
|
||||
#if defined(BLE_PIN_CODE)
|
||||
char* nm = the_mesh.getNodePrefs()->node_name;
|
||||
serial_interface.prepareBle("wadamesh-", nm, the_mesh.getBLEPin());
|
||||
if (wifiConfigGetBleEnabled()) {
|
||||
if (hostedInitBLE()) serial_interface.beginBle("wadamesh-", nm, the_mesh.getBLEPin());
|
||||
else printf("[BOOT] hostedInitBLE FAILED\n");
|
||||
} else {
|
||||
printf("[BOOT] BLE off in settings: C6 not started for it\n");
|
||||
}
|
||||
#endif
|
||||
{
|
||||
uint32_t synced_epoch = 0;
|
||||
const BootTimeSyncResult r = bootTimeSyncRun(rtc_clock.timeIsCurrent(), synced_epoch);
|
||||
if (r == BootTimeSyncResult::Ok) {
|
||||
rtc_clock.setCurrentTime(synced_epoch);
|
||||
printf("[BOOT] cold-boot time sync ok: %lu\n", (unsigned long)synced_epoch);
|
||||
} else if (r != BootTimeSyncResult::Skipped) {
|
||||
printf("[BOOT] cold-boot time sync: %s\n", bootTimeSyncResultName(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void app_main(void) {
|
||||
crowpanelP4InstallFullRestart(); // before anything else registers a shutdown handler
|
||||
initArduino();
|
||||
wadameshSetup();
|
||||
|
||||
// Wi-Fi state machine + SNTP + TCP/WS companion server, as in tdisplay_p4/main/main.cpp
|
||||
// (esp-hosted backend: Arduino's real WiFi over esp_wifi_remote).
|
||||
bool wifi_started = false, wifi_radio_prev = true, wifi_radio_inited = false;
|
||||
bool sntp_kicked = false, sntp_pushed = false, modem_sleep_set = false;
|
||||
uint32_t last_wifi_retry_ms = 0;
|
||||
const uint32_t WIFI_RETRY_INTERVAL_MS = 10000;
|
||||
|
||||
for (;;) {
|
||||
ui_task.loop();
|
||||
|
||||
bool wifi_radio_en = wifiConfigWantsWifi();
|
||||
if (!wifi_radio_inited) {
|
||||
wifi_radio_inited = true;
|
||||
wifi_radio_prev = wifi_radio_en;
|
||||
} else if (wifi_radio_en != wifi_radio_prev) {
|
||||
wifi_radio_prev = wifi_radio_en;
|
||||
if (!wifi_radio_en) { WiFi.disconnect(true); delay(50); WiFi.mode(WIFI_OFF); }
|
||||
wifi_started = false;
|
||||
}
|
||||
if (wifiConfigConsumeApplyRequest()) {
|
||||
if (wifi_started) {
|
||||
if (!wifi_radio_en) { WiFi.disconnect(true); delay(50); WiFi.mode(WIFI_OFF); }
|
||||
else { WiFi.disconnect(false, false); delay(50); }
|
||||
}
|
||||
wifi_started = false; last_wifi_retry_ms = 0;
|
||||
}
|
||||
if (wifi_radio_en) {
|
||||
if (!wifi_started) {
|
||||
wifi_started = true;
|
||||
WiFi.mode(WIFI_STA);
|
||||
if (wifiConfigHasRuntime()) {
|
||||
char ssid[WIFI_CONFIG_SSID_MAX], pwd[WIFI_CONFIG_PWD_MAX];
|
||||
wifiConfigGetSsid(ssid, sizeof(ssid)); wifiConfigGetPwd(pwd, sizeof(pwd));
|
||||
if (strlen(ssid) > 0) { WiFi.begin(ssid, pwd[0] ? pwd : nullptr); last_wifi_retry_ms = millis(); }
|
||||
}
|
||||
}
|
||||
if (wifiConfigHasRuntime() && WiFi.status() != WL_CONNECTED) {
|
||||
uint32_t now = millis();
|
||||
if ((uint32_t)(now - last_wifi_retry_ms) >= WIFI_RETRY_INTERVAL_MS) {
|
||||
last_wifi_retry_ms = now;
|
||||
char ssid[WIFI_CONFIG_SSID_MAX], pwd[WIFI_CONFIG_PWD_MAX];
|
||||
wifiConfigGetSsid(ssid, sizeof(ssid)); wifiConfigGetPwd(pwd, sizeof(pwd));
|
||||
if (strlen(ssid) > 0) { WiFi.disconnect(false, true); WiFi.begin(ssid, pwd[0] ? pwd : nullptr); }
|
||||
}
|
||||
}
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
if (!modem_sleep_set) { WiFi.setSleep(true); modem_sleep_set = true; }
|
||||
serial_interface.startTcpServer(true);
|
||||
if (!sntp_pushed) {
|
||||
if (!sntp_kicked) { configTime(0, 0, "pool.ntp.org", "time.nist.gov"); sntp_kicked = true; }
|
||||
const uint32_t e = (uint32_t)time(nullptr);
|
||||
if (e > 1700000000) { rtc_clock.setCurrentTime(e); sntp_pushed = true; printf("[TIME] SNTP -> rtc %lu\n", (unsigned long)e); }
|
||||
}
|
||||
}
|
||||
}
|
||||
serial_interface.tickWebSocketHandshake();
|
||||
sensors.loop();
|
||||
the_mesh.loop();
|
||||
vTaskDelay(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# Elecrow CrowPanel P4 10.1: standalone 16 MB partition table (same layout as the T-Display P4).
|
||||
# NOT an AppFS/launcher app (that's the Tanmatsu): this flashes bootloader+app directly.
|
||||
# Dual OTA app slots (4 MB each; the P4 wadamesh image is ~2.5-3 MB) + a big FAT for
|
||||
# identity/prefs/contacts/chat history + map tiles.
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 24K,
|
||||
otadata, data, ota, 0xF000, 8K,
|
||||
phy_init, data, phy, 0x11000, 4K,
|
||||
ota_0, app, ota_0, 0x20000, 4M,
|
||||
ota_1, app, ota_1, 0x420000, 4M,
|
||||
storage, data, fat, 0x820000, 8000K,
|
||||
coredump, data, coredump, 0xFF0000, 64K,
|
||||
|
@@ -0,0 +1,58 @@
|
||||
# Elecrow CrowPanel Advanced 10.1" ESP32-P4 HMI (1024x600) board sdkconfig fragment. EXPERIMENTAL.
|
||||
# Modelled on sdkconfigs/tdisplay_p4 (same ESP32-P4NRW32 + ESP32-C6-MINI-1 esp-hosted pairing). The
|
||||
# C6 SDIO pin/width set lives in a per-revision fragment (sdio_v10 / sdio_v11), layered after this.
|
||||
CONFIG_IDF_TARGET="esp32p4"
|
||||
|
||||
# 32 MB HEX PSRAM @ 200 MHz (in-package on the P4NRW32; Elecrow's V1.0/V1.1 lessons run HEX 200M).
|
||||
# 200 MHz is behind CONFIG_IDF_EXPERIMENTAL_FEATURES on IDF 5.5; without it Kconfig drops to 20 MHz
|
||||
# and the DPI panel underruns fetching its PSRAM framebuffer.
|
||||
CONFIG_SPIRAM=y
|
||||
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
|
||||
CONFIG_SPIRAM_SPEED_200M=y
|
||||
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=65536
|
||||
|
||||
# 16 MB W25Q128 + our standalone dual-OTA partition table (same layout as the T-Display P4).
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partition_tables/crowpanel_p4_101_16M.csv"
|
||||
|
||||
# MIPI-DSI (EK79007)
|
||||
CONFIG_LCD_DSI_ISR_IRAM_SAFE=y
|
||||
|
||||
# ESP32-C6-MINI-1 co-processor over SDIO slot 1 (CLK18 / CMD19; data pins per revision fragment).
|
||||
# Factory C6 firmware is esp-hosted MCU 2.12.3 (Elecrow "Advance_P4_On-Board_ESP32C6_Firmware_V2.12.3"),
|
||||
# the same version as the hosted fork tanmatsu-wifi pulls in.
|
||||
CONFIG_SLAVE_IDF_TARGET_ESP32C6=y
|
||||
CONFIG_ESP_HOSTED_CP_TARGET_ESP32C6=y
|
||||
CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ=40000
|
||||
# C6_EN = GPIO32 (schematic, all revisions). Reset through hosted_reset_slave_callback() in
|
||||
# variants/crowpanel_p4_101/target.cpp, so the polarity and the GPIO are explicit in code.
|
||||
CONFIG_ESP_HOSTED_SLAVE_RESET_ONLY_IF_NECESSARY=y
|
||||
CONFIG_ESP_HOSTED_RESET_SLAVE_USING_CALLBACK=y
|
||||
CONFIG_ESP_HOSTED_SDIO_RESET_SLAVE_USING_CALLBACK=y
|
||||
CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE=y
|
||||
# The P4 has no Bluetooth controller of its own: NimBLE's HCI runs over esp-hosted SDIO.
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_CONTROLLER_DISABLED=y
|
||||
# CONFIG_BT_NIMBLE_TRANSPORT_UART is not set
|
||||
# A C6 that cannot be reached must cost Wi-Fi and BLE, never the device (see sdkconfigs/tdisplay_p4).
|
||||
CONFIG_ESP_HOSTED_HOST_RESTART_NO_COMMUNICATION_WITH_SLAVE=n
|
||||
CONFIG_ESP_HOSTED_TRANSPORT_RESTART_ON_FAILURE=n
|
||||
|
||||
# Panic coredump -> the 64K 'coredump' partition (ELF, decodable against the build's elf).
|
||||
CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=y
|
||||
CONFIG_ESP_COREDUMP_DATA_FORMAT_ELF=y
|
||||
CONFIG_ESP_COREDUMP_CHECKSUM_CRC32=y
|
||||
CONFIG_ESP_COREDUMP_MAX_TASKS_NUM=64
|
||||
|
||||
# DPI/video panel: the 1.2 MB framebuffer lives in PSRAM and is streamed continuously, so the same
|
||||
# underrun mitigations as the T-Display P4: run code from PSRAM (flash writes no longer drop the
|
||||
# cache under the DSI DMA), a 256 KB L2 cache with 128 B lines, -O2.
|
||||
CONFIG_SPIRAM_XIP_FROM_PSRAM=y
|
||||
CONFIG_COMPILER_OPTIMIZATION_SIZE=n
|
||||
CONFIG_COMPILER_OPTIMIZATION_PERF=y
|
||||
CONFIG_CACHE_L2_CACHE_128KB=n
|
||||
CONFIG_CACHE_L2_CACHE_256KB=y
|
||||
CONFIG_CACHE_L2_CACHE_LINE_64B=n
|
||||
CONFIG_CACHE_L2_CACHE_LINE_128B=y
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../tanmatsu/sdkconfigs/general
|
||||
@@ -0,0 +1,9 @@
|
||||
# CrowPanel P4 10.1 V1.0: the C6 data lines run the other way round (schematic: GPIO14 = SD2_D0 ->
|
||||
# C6 IO20 ... GPIO17 = SD2_D3 -> C6 IO23), and V1.0 has none of the series resistors / caps V1.1
|
||||
# added on those lines. Elecrow's V1.0 Lesson17 runs the link 1-bit (D0 = GPIO14, D1 = GPIO15 as the
|
||||
# interrupt line), so this does the same.
|
||||
CONFIG_ESP_HOSTED_SDIO_1_BIT_BUS=y
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_1=18
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_1=19
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_1=14
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_1BIT_BUS_SLOT_1=15
|
||||
@@ -0,0 +1,10 @@
|
||||
# CrowPanel P4 10.1 V1.1 and V1.2: C6 SDIO 4-bit, P4 GPIO17/16/15/14 = D0/D1/D2/D3.
|
||||
# Source: V1.1/V1.2 schematics (net SD2_D0 = GPIO17 -> C6 IO20 ... SD2_D3 = GPIO14 -> C6 IO23) and
|
||||
# Elecrow's V1.1/V1.2 Lesson17 sdkconfig (4-bit, same pins).
|
||||
CONFIG_ESP_HOSTED_SDIO_4_BIT_BUS=y
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_1=18
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_1=19
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_1=17
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_1=16
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_1=15
|
||||
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_1=14
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../tanmatsu/sdkconfigs/wadamesh
|
||||
@@ -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
|
||||
+69
-6
@@ -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,17 +18,33 @@
|
||||
"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",
|
||||
@@ -58,6 +74,53 @@
|
||||
"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
Loaded 100 of 461 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user