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
|
||||
+51
-2
@@ -15,10 +15,13 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
|
||||
| Tanmatsu | ESP32-P4 + ESP32-C6, SX1262 | 4" 800x480, 69-key keyboard (no touch) | Tanmatsu app store on the device (runs under the badge.team launcher, not web-flashable) | Store tracks the test channel | Fully supported; LoRa + Wi-Fi + Bluetooth simultaneously, standalone and companion in one |
|
||||
| Elecrow ThinkNode M9 | ESP32-S3, LR1110 | 2.4" 240x320 (no touch), I2C QWERTY + d-pad | Web flasher | Beta (new in beta_38) | Hardware-complete community port by ded (#138): GPS, microSD, buzzer, lock screen, d-pad navigation |
|
||||
| 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 |
|
||||
| Seeed SenseCAP Indicator D1L | ESP32-S3, SX1262 | 4" 480x480 touch (FT6336) | Development build only | Experimental | Hardware-tested (display, touch, radio); USB-powered, no battery; the RP2040's buzzer and microSD are not used |
|
||||
| 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 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
|
||||
@@ -98,7 +101,9 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
|
||||
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
|
||||
@@ -124,6 +129,51 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
|
||||
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`.
|
||||
- **SenseCAP Indicator D1L**: `sensecap_indicator_companion_radio_touch` drives
|
||||
the 480x480 ST7701S RGB panel, the FT6336 touch controller and the SX1262,
|
||||
whose control lines sit on a TCA9535 I/O expander. It is USB-powered with no
|
||||
battery, so the battery readouts and Power off are hidden. The RP2040
|
||||
peripherals (buzzer, microSD, Grove, D1Pro sensors) are not used, and without
|
||||
storage there are no map tiles. The D1Pro shares the ESP32 side and is expected
|
||||
to run the same image, but is untested. See
|
||||
[variants/sensecap_indicator/PORT.md](variants/sensecap_indicator/PORT.md).
|
||||
|
||||
## Requested boards
|
||||
|
||||
@@ -132,7 +182,6 @@ Open hardware requests, roughly in demand order. Ports are welcome, see
|
||||
PRs.
|
||||
|
||||
- LilyGo T-Deck Pro hardware validation / T-Deck Max variants: [#62](https://github.com/ALLFATHER-BV/wadamesh/issues/62)
|
||||
- SenseCAP Indicator D1L: [#14](https://github.com/ALLFATHER-BV/wadamesh/issues/14)
|
||||
|
||||
## Channels
|
||||
|
||||
|
||||
@@ -53,6 +53,9 @@ We do NOT bind LVGL wholesale. We bind a stable, versioned surface we own
|
||||
trackball, gps), `version()`, `toast(msg)`, `tr(s)` (the firmware's own
|
||||
translation table, so an app is not stuck in English).
|
||||
- `wada.timer` — `every(ms)` drives `on_tick` cadence (min 33 ms).
|
||||
- `wada.widget` (extended SDK) — `set{title, value, line, bar, spark}` publishes the app's
|
||||
Home widget (at most once a second), `clear()` empties it. The last one is kept after the
|
||||
app closes and written to `<root>/apps/<id>.wgt` at teardown; nothing runs headless.
|
||||
|
||||
Sandbox env (no `io`, `os`, `require`, `dofile`, `load` of new chunks;
|
||||
whitelisted stdlib: `math`, `string`, `table`, `pairs/ipairs/select/pcall/
|
||||
|
||||
@@ -88,7 +88,16 @@ driver, variants/tdisplay_p4/P4Audio.cpp)
|
||||
Espressif's public es8311.c/es8311_reg.h; P4Audio.cpp additionally
|
||||
re-bases the esp-bsp init flow onto Arduino Wire.
|
||||
|
||||
LovyanGFX (RAK WisMesh Tap V2 display backend)
|
||||
ST7701S panel init list (SenseCAP Indicator display driver,
|
||||
variants/sensecap_indicator/IndicatorDisplay.cpp)
|
||||
Copyright (c) Meshtastic
|
||||
GPL-3.0 License — https://github.com/meshtastic/device-ui
|
||||
The panel's second init command list (MADCTL vertical flip, SDIR
|
||||
horizontal flip) is taken from Meshtastic device-ui's
|
||||
include/graphics/LGFX/LGFX_INDICATOR.h (Panel_Indicator::getInitCommands).
|
||||
The rest of the driver is original code on LovyanGFX's Panel_ST7701.
|
||||
|
||||
LovyanGFX (display backend: RAK WisMesh Tap V2, SenseCAP Indicator and others)
|
||||
Copyright (c) lovyan03 — FreeBSD License — https://github.com/lovyan03/LovyanGFX
|
||||
|
||||
Camillia-MT (CrowPanel Advance 3.5 software-SPI microSD backend)
|
||||
@@ -97,5 +106,12 @@ Camillia-MT (CrowPanel Advance 3.5 software-SPI microSD backend)
|
||||
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.
|
||||
@@ -11,7 +11,7 @@ Touch-UI [MeshCore](https://github.com/meshcore-dev/MeshCore) companion-radio
|
||||
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,22 +20,25 @@ 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 + SX1262 by default, with `WADA_P4_LCD=1` for TFT-LCD and `WADA_P4_LR2021=1` for LR2021 (beta)
|
||||
- Attaky Mesh Series — env `attaky_mesh_series_companion_radio_touch` (beta)
|
||||
- Elecrow CrowPanel Advance 3.5 — env `crowpanel_35_companion_radio_touch` (beta, hardware validation pending; requires an SX1262 in the expansion slot)
|
||||
- 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)
|
||||
- Seeed SenseCAP Indicator D1L: env `sensecap_indicator_companion_radio_touch` (experimental, community port, PR #682)
|
||||
- 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,
|
||||
@@ -100,28 +103,29 @@ automatically:
|
||||
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
|
||||
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
|
||||
|
||||
+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"],
|
||||
"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,38 @@
|
||||
{
|
||||
"build": {
|
||||
"arduino": {
|
||||
"ldscript": "esp32s3_out.ld",
|
||||
"partitions": "default_8MB.csv",
|
||||
"memory_type": "qio_opi"
|
||||
},
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-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": "Seeed SenseCAP Indicator D1L (8 MB FLASH, 8 MB PSRAM)",
|
||||
"upload": {
|
||||
"flash_size": "8MB",
|
||||
"maximum_ram_size": 8388608,
|
||||
"maximum_size": 8388608,
|
||||
"require_upload_port": true,
|
||||
"speed": 921600
|
||||
},
|
||||
"url": "",
|
||||
"vendor": "Seeed Studio"
|
||||
}
|
||||
@@ -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
+245
@@ -0,0 +1,245 @@
|
||||
#!/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: the generic esp32p4 variant's SD power pin is THIS board's touch SDA ---------
|
||||
# Arduino's esp32p4 variant (the Espressif P4 Function EV board) defines BOARD_SDMMC_POWER_PIN 45, so
|
||||
# SD_MMC.begin() does pinMode(45, OUTPUT) + a 200 ms power toggle, leaves it driven LOW, and
|
||||
# SD_MMC.end() clears the pin again. On the CrowPanel GPIO45 is I2C0 SDA (GT911, codec, HY2.0 port):
|
||||
# the pinMode makes the peripheral manager detach I2C0 (i2cDeinit), so every Wire call afterwards
|
||||
# failed with "bus is not initialized" and touch never came up (beta_97, tester V1.0 log). The card
|
||||
# here is powered straight from VDD_3V3 (no switch), so the block has no job on this board at all.
|
||||
# main/CMakeLists.txt defines WADA_SDMMC_NO_POWER_PIN; the guard below makes SD_MMC honour it. Fails
|
||||
# the build if the upstream lines change, because touch depends on it. Idempotent.
|
||||
if [ -f "$SDMMC_C" ] && ! grep -q 'WADA_SDMMC_NO_POWER_PIN' "$SDMMC_C"; then
|
||||
if [ "$(grep -c '^#if defined(BOARD_SDMMC_POWER_PIN)$' "$SDMMC_C")" -lt 2 ]; then
|
||||
echo "[build.sh] SD_MMC.cpp: BOARD_SDMMC_POWER_PIN blocks not found; the GPIO45 patch needs updating" >&2
|
||||
exit 1
|
||||
fi
|
||||
sed -i '' 's|^#if defined(BOARD_SDMMC_POWER_PIN)$|#if defined(BOARD_SDMMC_POWER_PIN) \&\& !defined(WADA_SDMMC_NO_POWER_PIN) // wadamesh CrowPanel P4: GPIO45 is I2C SDA|' "$SDMMC_C"
|
||||
echo "[build.sh] patched arduino SD_MMC.cpp (no SD power pin: GPIO45 stays I2C SDA)"
|
||||
fi
|
||||
|
||||
# --- Build-time patch: remember the C6's esp-hosted version from its init event ---------------------
|
||||
# The slave reports its firmware version in the transport INIT event; the host only compares it
|
||||
# (major.minor) and logs a warning. Keep the raw value so main.cpp can say plainly which C6
|
||||
# firmware is fitted. Asking for it over RPC instead (esp_hosted_get_coprocessor_fwversion) is what
|
||||
# an old co-processor may not answer. The variable is defined in variants/crowpanel_p4_101/target.cpp.
|
||||
HOSTED_TDRV="managed_components/nicolaielectronics__esp-hosted-tanmatsu/host/drivers/transport/transport_drv.c"
|
||||
if [ -f "$HOSTED_TDRV" ] && ! grep -q 'wadamesh_c6_fw_version' "$HOSTED_TDRV"; then
|
||||
python3 - "$HOSTED_TDRV" <<'PYEOF'
|
||||
import sys
|
||||
p = sys.argv[1]
|
||||
s = open(p).read()
|
||||
old = "\t// if ESP_PRIV_FIRMWARE_VERSION was not received, slave version will be 0.0.0\n\tcompare_fw_version(slave_fw_version);\n"
|
||||
new = ("\t// if ESP_PRIV_FIRMWARE_VERSION was not received, slave version will be 0.0.0\n"
|
||||
"\t{ extern volatile uint32_t wadamesh_c6_fw_version; extern volatile int wadamesh_c6_fw_known;\n"
|
||||
"\t wadamesh_c6_fw_version = slave_fw_version; wadamesh_c6_fw_known = 1; } /* wadamesh: C6 version for the boot log */\n"
|
||||
"\tcompare_fw_version(slave_fw_version);\n")
|
||||
if s.count(old) != 1:
|
||||
sys.exit("esp-hosted transport_drv.c: version compare not found; patch needs updating")
|
||||
open(p, "w").write(s.replace(old, new))
|
||||
PYEOF
|
||||
echo "[build.sh] patched esp-hosted transport_drv.c (keep the C6 firmware version)"
|
||||
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,98 @@
|
||||
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
|
||||
# The generic esp32p4 Arduino variant names GPIO45 as an SD power switch (BOARD_SDMMC_POWER_PIN);
|
||||
# here 45 is the touch/codec I2C SDA and the card has no power switch. build.sh makes SD_MMC.cpp
|
||||
# skip that pin when this is defined; without it every SD_MMC.begin()/end() tore down I2C0.
|
||||
WADA_SDMMC_NO_POWER_PIN=1
|
||||
)
|
||||
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,277 @@
|
||||
// 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.
|
||||
// Say once, plainly, which esp-hosted firmware the C6 runs (known once the transport is up). A C6
|
||||
// older than the host (V1.0 units have shipped with 2.3.0 against this 2.12.x host) can leave RPCs
|
||||
// unanswered, each costing the 5 s RPC timeout on the calling task, so Wi-Fi reconnects are spaced
|
||||
// out further on such a C6 rather than stalling the UI loop every 10 s.
|
||||
static int s_c6_state = -1; // -1 unknown, 0 matches the host, 1 older than the host
|
||||
static void crowpanelC6ReportOnce(const char* when) {
|
||||
static bool s_done = false;
|
||||
if (s_done) return;
|
||||
char line[200];
|
||||
const int st = crowpanelC6Describe(line, sizeof line);
|
||||
if (st < 0) return;
|
||||
s_done = true;
|
||||
s_c6_state = st;
|
||||
printf("[C6] %s (seen at %s)\n", line, when);
|
||||
if (st == 1) printf("[C6] old co-processor: Wi-Fi reconnect attempts every 60 s instead of 10 s\n");
|
||||
}
|
||||
|
||||
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
|
||||
bool g_sd_no_card_at_boot = false; // boot probe: no card answered (UITask's first lazy mount skips)
|
||||
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 itself is on VDD_3V3 with no switch).
|
||||
// The card has pull-ups on CMD/D0 (R27/R28); the build also enables the internal ones (build.sh
|
||||
// patch), as Elecrow does. DAT1-DAT3 are not connected to the P4 (schematic V1.0-V1.2), so the slot
|
||||
// can only run 1-bit. V1.0 also shows a 10k pull-down on the card's DAT3 (R33, with the pull-up
|
||||
// R29 marked "10K/NC"); a card that sees DAT3 low at its first command enters SPI mode and never
|
||||
// answers in SD mode, which would look exactly like an empty slot (unverified on hardware).
|
||||
SD_MMC.setPowerChannel(-1);
|
||||
SD_MMC.setPins(43, 44, 39);
|
||||
for (int p : {44, 39}) gpio_pullup_en((gpio_num_t)p);
|
||||
// Quiet presence check first: an empty slot (or a card that never answers the first command) is
|
||||
// reported once, instead of three IDF + Arduino error pairs from the mount ladder below. The
|
||||
// ladder only runs when a card answered, which is when a lower clock can actually help.
|
||||
const esp_err_t sd_probe = crowpanelSdProbe();
|
||||
if (sd_probe == ESP_ERR_TIMEOUT) {
|
||||
g_sd_no_card_at_boot = true;
|
||||
sdMountDiagBegin();
|
||||
sdMountDiagAttempt(400000u, false, false);
|
||||
sdMountDiagSetMounted(false, 0);
|
||||
printf("[storage] SD_MMC = no card or not responding (1-bit slot, no answer at 400 kHz)\n");
|
||||
} else {
|
||||
if (sd_probe != ESP_OK)
|
||||
printf("[storage] SD_MMC probe: card answered but init failed (%s); trying the mount ladder\n",
|
||||
esp_err_to_name(sd_probe));
|
||||
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" : "card present but mount failed");
|
||||
}
|
||||
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 (BLE unavailable; LoRa, touch and the UI keep running)\n");
|
||||
crowpanelC6ReportOnce("BLE start");
|
||||
} 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;
|
||||
const uint32_t WIFI_RETRY_INTERVAL_OLD_C6_MS = 60000; // see crowpanelC6ReportOnce()
|
||||
|
||||
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();
|
||||
crowpanelC6ReportOnce("Wi-Fi start");
|
||||
const uint32_t retry_ms = (s_c6_state == 1) ? WIFI_RETRY_INTERVAL_OLD_C6_MS : WIFI_RETRY_INTERVAL_MS;
|
||||
if ((uint32_t)(now - last_wifi_retry_ms) >= retry_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; crowpanelC6ReportOnce("Wi-Fi connected"); }
|
||||
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
|
||||
+37
-8
@@ -3,14 +3,14 @@
|
||||
{
|
||||
"id": "monitor",
|
||||
"name": "RF Monitor",
|
||||
"ver": "1.3",
|
||||
"desc": "Scrollable: RSSI/noise chart with dBm axis, link margin, RX rate, radio params, heard feed."
|
||||
"ver": "1.4",
|
||||
"desc": "RSSI, noise, margin and packet-rate tiles, a filled signal chart and the recently heard packets. Tap a packet for who sent it, its route, margin, size and airtime."
|
||||
},
|
||||
{
|
||||
"id": "airtime",
|
||||
"name": "Airtime",
|
||||
"ver": "1.4",
|
||||
"desc": "Utilization bars with a % axis, rx/tx split, duty ceiling, budget. Reset button top-right."
|
||||
"ver": "1.6",
|
||||
"desc": "How busy the channel is: the live load, average, peak, a filled chart with the duty ceiling and the rx/tx split. Keeps a Home widget with the channel load while it runs."
|
||||
},
|
||||
{
|
||||
"id": "snake",
|
||||
@@ -22,7 +22,8 @@
|
||||
"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."
|
||||
"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",
|
||||
@@ -35,13 +36,15 @@
|
||||
"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",
|
||||
@@ -84,7 +87,8 @@
|
||||
"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."
|
||||
"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",
|
||||
@@ -92,6 +96,31 @@
|
||||
"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
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Loaded 100 of 435 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user