Headline: full keyboard navigation (on by default) — WASDZ spatial focus move, S select, Q back, per-tab hotkeys (E/R/T/U/I) shown on the icons, plus scroll up/down (F/C). Every key is remappable in Settings -> Keyboard. Foundation for keyboard-first devices like the Tanmatsu T-Pager (no touchscreen) and a reliable keyboard workflow on the T-Deck today. Also: #27 receive/flood reboot fix, on-boot crash-report prompt, map zoom +/- buttons (#26), route-replay wide overview, 'App drawer as home' option. The in-progress Tanmatsu (ESP32-P4) port source is now public. Mesh America catalog + release.sh kept from main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
18 KiB
Tanmatsu port — working tracker
Goal: run the full wadamesh UI + functionality on the Tanmatsu (Nicolai Electronics), kept as one codebase with the existing S3 boards so a UI change ships everywhere at once. This file is the running plan/status — update it as we go.
Status: device IN HAND (2026-06-20); ENTIRE APP COMPILES + LINKS on P4 (f04dba6) · branch
tanmatsu-port on beta_9 · the full shared src/ — MyMesh + the 28k-line LVGL UITask + all
helpers + variant glue — compiles, and the project builds application.bin · LVGL 8.4 + the
core + ~10 Arduino libs all build · next = the real app_main bring-up (the spike main still
ships, so the bin isn't the real app yet), storage-on-FAT, the 800×480 layout, the bsp_input→LVGL
indev, then on-device install via BadgeLink/AppFS.
Decision (architecture) — UPDATED 2026-06-20: wadamesh is an APP on the launcher
On the Tanmatsu everything is an app (badge.team OS + GUI launcher + AppFS app
store). So wadamesh ships as a launcher app, NOT a firmware that replaces the OS.
AppFS apps are standard ESP-IDF app binaries installed into an AppFS flash partition
(via appfs_add_file.py / BadgeLink / the badge.team app store) and started from the
launcher; a running app owns the screen + input via badge-bsp and returns to the
launcher on exit (docs.tanmatsu.cloud → AppFS).
⇒ The Tanmatsu target is an ESP-IDF app, NOT the pioarduino/Arduino standalone flash (that would overwrite the launcher). The PlatformIO route in the old plan is dropped; the "IDF subproject" that was the fallback is now the primary path — because the whole app model is IDF-native.
We still keep one repo, one src/: the shared app (UITask, MyMesh, i18n, fonts…)
compiles inside the IDF app via arduino-esp32 as an ESP-IDF component, alongside
badge-bsp (display/keyboard/power), tanmatsu-lora (LoRa — bridge already written in
variants/tanmatsu/) and esp_wifi_remote (C6 Wi-Fi/BLE). Our LVGL 8.3 flushes to
the badge-bsp esp_lcd panel (not PAX; not the LVGL-9.3 badge-bsp-lvgl). Distribution
= the badge.team app store — the Tanmatsu analog of LauncherHub. boards/tanmatsu.json
- the pioarduino env in the old plan are no longer the build; the LoRa bridge + variant wiring carry over.
Compile spike — RESULTS (2026-06-20) ✅ arduino-as-component PROVEN on P4
IDF-app build lives in tanmatsu/ (badge.team Makefile + project-local IDF v5.5.1
in tanmatsu/esp-idf, gitignored). Build via ./tanmatsu/build.sh <target> (wraps
idf.py -DDEVICE=tanmatsu -DSDKCONFIG_DEFAULTS="sdkconfigs/general;sdkconfigs/tanmatsu;sdkconfigs/wadamesh" -DIDF_TARGET=esp32p4).
Toolchain: PlatformIO already had the riscv32-esp P4 toolchain; IDF cloned via the Makefile
idf_tools.py install cmake ninja(the targetedinstall.sh esp32p4skips those).
Proven end-to-end:
- ✅ Stock badge.team template builds for P4 (baseline) — toolchain + component manager OK.
- ✅ arduino-esp32 3.3.10 compiles + links alongside badge-bsp (241 arduino objs).
esp32p4 first-class in arduino since 3.1.3. ⇒ the "one shared
src/via arduino-on-P4" architecture is validated — this was the make-or-break risk.
Fixes (all in tanmatsu/sdkconfigs/wadamesh, our overlay — badge.team files kept pristine):
CONFIG_FREERTOS_HZ=1000— arduino-esp32 hard-requires it (badge.team defaults 100).CONFIG_BT_ENABLED=n(+BT_NIMBLE_ENABLED=n,ESP_HOSTED_ENABLE_BT_NIMBLE=n) — arduino's bundled BLE won't compile vs IDF 5.5.1 NimBLE (ble_gap_read_local_irkrenamed). P4 has no native BLE + we use the separate NimBLE-Arduino lib, so BT-off compiles arduino BLE/SimpleBLE/BluetoothSerial out via their#ifguards. Revisit hosted BLE (phone companion over the C6) later. NB: arduino'sCONFIG_ARDUINO_SELECTIVE_COMPILATIONdoes NOT filter in this setup (verified) — use feature disables, not the selective knob.
Gotchas found (carry into the real src/ wiring):
- badge-bsp headers have NO
extern "C"guards → wrap every#include "bsp/*.h"inextern "C" { }from C++ or the bsp symbols mangle and fail to link. - Two esp-hosted stacks collide: arduino +
espressif/esp_wifi_remotepullespressif/esp_hosted, whose whole-archive C6 SDIO driver duplicates badge.team'snicolaielectronics/esp-hosted-tanmatsu→ "multiple definition". Fix:tanmatsu/components/esp_hosted/(empty forwarder,REQUIRES esp-hosted-tanmatsu) shadows the registry component by name (local components win) → one esp-hosted, the hardware fork, and arduino'sWiFi.hrides on it. Runtime TODO: theESP_HOSTED_GPIO_SLAVE_RESET_SLAVEconfig warning (C6 reset pin) — set it when WiFi is brought up live.
Spike C1 — MeshCore core compiles on P4 (2026-06-20) ✅
The core is vendored by tanmatsu/fetch-deps.sh from .pio/libdeps/.../MeshCore into
components/meshcore/core/ (gitignored) under a committed components/meshcore/CMakeLists.txt.
The Arduino libs it needs (Crypto, RTClib, Adafruit BusIO, Adafruit Unified Sensor, Melopero
RV3028, CayenneLPP, ArduinoJson) all go in ONE committed ardlibs component (so cross-lib
#includes resolve). Build: ./build.sh build. What it took:
- vendored lib dir names must be space-free (spaces break IDF's component-req matching →
BUG:). - the core's bundled C ed25519 (
lib/ed25519/) must be added explicitly (PlatformIO auto-compiles it as a nested lib; IDF doesn't). ardlibsneedsnvs_flash(Crypto's RNG seeds from NVS);meshcoreneedsnvs_flash+efuse.- strip IDF's
-Werror=allfromCOMPILE_OPTIONS— vendored code trips reorder/format/ class-memaccess, and a bare-Wno-errordoes NOT cancel-Werror=all(done in main/CMakeLists). - exclude over-globbed files vs
build_as_lib.py: non-ESP32 platform helpers (nrf52/stm32/rp2040),helpers/radiolib/*(→ LoRa bridge), the S3 display drivers,ESPNOWRadio,SerialBLEInterface(BT off), and theMC_VENDORED_TOUCH_APPfiles (our own src/ ships them).
Remaining for a running app — ordered worklist (next session):
- Tanmatsu define set (
main/CMakeLists.txtbuild flags) — a curated subset of the S3 touchbuild_flags: KEEPESP32_PLATFORM,MC_VENDORED_TOUCH_APP,UI_LVGL=1,HAS_TOUCH_UI=1,MULTI_TRANSPORT_COMPANION,LORA_FREQ/BW/SF,LV_CONF_PATH=lv_conf.h+LV_CONF_INCLUDE_SIMPLE=1+LV_INDEV_DEF_SCROLL_THROW=7,MAX_CONTACTS,ENABLE_PRIVATE_KEY_*,ENV_INCLUDE_GPS,-I src -I src/ui-touch -I include. DROP every S3-board define (HELTEC_LORA_V4*,USE_SX1262,RADIO_CLASS/WRAPPER_CLASS, allP_LORA_*/PIN_*/TFT/CHSC6x,ST7789_*,RADIOLIB_*). Give the Tanmatsu its own board/display identity (badge-bsp, NOTDISPLAY_CLASS=ST7789). wadamesh_appcomponent — globsrc/MINUSmain.cpp(own app_main) + the S3 input helpers (HeltecV4CapTouch,TDeckKeyboard/Touch/Trackball→ Tanmatsu input isbsp_input); addui-touch/once LVGL is in.- LVGL component — vendor lvgl 8.3 + the repo
lv_conf.h. - Transports — re-add AsyncTCP+ESPAsyncWebServer with a WDT-config fix (
CONFIG_ESP_TASK_WDT_*) or the ESP32Async fork; OR ship companion over USB-serial first (off the critical path). variants/tanmatsu/glue —TanmatsuBoard(board API over bsp/power), the LVGL→badge-bsp display driver (flush_cb→bsp_display_blit, 800×480 RGB565), absp_input→lv_indevkeyboard/pointer,radio_init→theTanmatsuLoraRadiobridge, storage→the FATlocfdpartition.app_main— replicate setup()/loop(): board.begin → radio_init → storage → the_mesh.begin → ui_task.begin(disp) → loop{ui_task.loop; the_mesh.loop}.- Install —
make build→application.bin→ BadgeLink/AppFS → the badge.team app store.
Biggest chunks: UITask (28k lines) vs the Tanmatsu defines + LVGL, and the 800×480 display flush (768 KB/frame → PSRAM). All methodical integration on the proven foundation.
Dep status: vendored + compiling = MeshCore core, ed25519, Crypto, RTClib, Adafruit BusIO, Adafruit Unified Sensor, Melopero RV3028, CayenneLPP, ArduinoJson, MicroNMEA. Still to add = LVGL, AsyncTCP + ESPAsyncWebServer (WDT fix), and (only if telemetry wanted) the Adafruit sensor libs.
Why this is feasible (verified 2026-06-18)
- Arduino-ESP32 supports the ESP32-P4 — core 3.3.10 (2026-06-05) has P4
boards incl. MIPI-display variants (
ESP32P4_4DS_MIPI). - PlatformIO can target it via the pioarduino platform (rel
55.03.39), whose board list already includesm5stack-tab5-p4— a P4 + MIPI display + C6-coprocessor-radio handheld, i.e. a structural near-twin of the Tanmatsu. Crib display/WiFi bring-up from that board. - Radio maps cleanly:
mesh::Radio(7 pure virtuals) ↔tanmatsu-lora'slora.h(raw 256-byte tx/rx, settable freq/bw/sf/cr/sync/preamble/power, RSSI/SNR). Same SX1262/1268 family the S3 boards use. Bridge written (below). - No driver writing: display/keyboard/power/wifi/lora all exist as IDF
components (
badge-bsp,tanmatsu-wifi,tanmatsu-lora).
Hardware / platform facts
| (confirmed from docs.tanmatsu.cloud + hackster/CNX) | ||
|---|---|---|
| Main SoC | ESP32-P4 dual-core RISC-V | @ 400 MHz, 32 MB PSRAM, 16 MB flash, microSD (SDIO) |
| Display | 3.97" MIPI-DSI, 800×480, RGB565 | esp_lcd panel from badge-bsp; we flush LVGL 8.3 to it (not PAX) |
| Input | 69-key QWERTY + 6 fn keys | events via bsp_input_get_queue() |
| Coprocessors | WCH CH32V203 (keyboard + power) · ESP32-C6 (WiFi/BLE/802.15.4 via tanmatsu-wifi) · SX1262 LoRa 433/868-915 (via tanmatsu-lora, "remote") |
the P4 has no native radio |
| Native SDK | ESP-IDF ≥ 5.5, CMake, C, PAX graphics | template: Nicolai-Electronics/tanmatsu-template |
Research findings (2026-06-18, no hardware)
Radio bridge — fully validated against tanmatsu-lora/lora.c; the code was already correct.
lora_packet_stats_t = {rssi_pkt_raw u8, snr_pkt_raw i8, signal_rssi_pkt_raw u8};rssi_dbm = -rssi_pkt_raw/2,snr_db = snr_pkt_raw/4. ✓ matches the bridge.lora_send_packet()is blocking (waits ≤2 s on the transaction semaphore) →isSendComplete()latch is correct. ✓config.bandwidth= kHz, valid {7,10,15,20,31,41,62,125,250,500} (62.5→62);config.coding_rate= 5..8 → 4/5..4/8.setParams()is correct. ✓- Only remaining radio tuning (small, MeshCore-side):
sync_word/preamble_length/powerto matchRadioLibWrappers.cppso it interoperates with the existing mesh.
Display/input — DECISION: keep wadamesh's LVGL 8.3 and write our own flush; do NOT adopt badge-bsp-lvgl.
badge-bspexposes the screen as anesp_lcdpanel (bsp_display_get_panel/_get_panel_io/_get_parameters→ h_res/v_res/color_format) and the keyboard as a FreeRTOS event queue (bsp_input_get_queue). Reference wiring:badgeteam/konsool-template-lvgl.- BUT the badge LVGL component (
badgeteam/esp32-component-badge-bsp-lvgl) targets LVGL 9.3, and wadamesh's UITask is LVGL 8.3. Adopting it forces an 8→9 migration of ~30k lines — not worth it. So: bring up our own LVGL 8.3 (esp_lvgl_port supports 8.x, or a manualflush_cb) flushing to the badge-bspesp_lcdpanel, + anlv_indevthat reads thebsp_inputqueue. - UI layout target: 800×480 landscape — make UITask responsive for it (it already branches on
hor_res).
Repos: badgeteam/esp32-component-badge-bsp (BSP) · badgeteam/konsool-template-lvgl (LVGL-on-bsp reference) · Nicolai-Electronics/esp32-component-tanmatsu-lora (lora.c).
Plan / status
- Confirm framework gap is bridgeable (Arduino-P4 + pioarduino + Tab5 precedent)
- Map
mesh::Radio↔lora.h; writeTanmatsuLoraRadio(full drop-in forradio_driver: setParams/setTxPower/setRxBoostedGainMode/idle/getPacketsRecvErrors/RSSI/SNR) idf_component.ymlfor the BSP/wifi/lora componentsboards/tanmatsu.json(based on pioarduinom5stack-tab5-p4)variants/tanmatsu/{target.h,target.cpp,TanmatsuBoard.h}— wiresradio_driver=bridge,board,radio_init(),radio_new_identity()(StdRNG)- Compile spike — get
src/to build for P4 under pioarduino (no HW needed; see below) [env:tanmatsu_companion_radio_touch]inplatformio.ini— port the shared touch flags, drop S3 LoRa pins; resolve the RadioLib build issue (next item)- RadioLib build question — RESOLVED: keep RadioLib in
lib_deps+ the sameRADIOLIB_*flags as the S3 envs.RadioLibWrappers.cppstill compiles, but since the Tanmatsutarget.cppnever instantiatesWRAPPER_CLASS/RADIO_CLASS, the unused code is linker-stripped. Net: the env mirrors the T-Deck's RadioLib deps/flags; only theradio_drivertype differs (set invariants/tanmatsu/target.h). Nobuild_src_filtergymnastics needed. - Wire the IDF components into the pio build (component manager) ⚠️ main risk
- Headless bring-up: call
radio_init()→TanmatsuLoraRadiojoins the mesh (serial log) - WiFi/BLE via
esp_wifi_remote/tanmatsu-wifi(likely close to free in Arduino) - LVGL display driver: flush →
bsp_display_blit; LVGL indev ←bsp/input(keyboard) - Make UITask responsive for the larger screen (it already branches on
hor_res) - Tune the
TanmatsuLoraRadioTODO(device) items - Release path (separate bin; not covered by the
wadamesh-releaseskill yet)
What's done this session (no hardware)
A structurally-complete variant skeleton — the radio + board wiring is real (mirrors the T-Deck variant); display/input/wifi glue + the env are the device/spike work left.
variants/tanmatsu/TanmatsuLoraRadio.{h,cpp}— the LoRa bridge, real code against the actuallora.hv0.3.0 API +mesh::Radiocontract, reusing MeshCore's own airtime/packetScore math. A full drop-in forradio_driver(every method the app calls inMyMesh.cpp). Compiles-ready pending theTODO(device)items.variants/tanmatsu/target.{h,cpp}— wiresradio_driver(the bridge),board,radio_init()(→lora_init_remote),radio_new_identity()(seeded via StdRNG).variants/tanmatsu/TanmatsuBoard.h—: public ESP32Board; battery/power via badge-bsp left as TODO(device).variants/tanmatsu/idf_component.yml— the managed-component deps.boards/tanmatsu.json— P4/16MB/PSRAM board def (based onm5stack-tab5-p4; confirm flash size + f_cpu on the real unit).- This tracker + the
tanmatsu-portmemory.
TanmatsuLoraRadio — TODO(device) (confirm/tune on hardware)
lora_packet_stats_tfield names + raw→real RSSI/SNR conversion (assumed-raw/2,raw/4).bandwidthunits (assumed kHz) andcoding_rateencoding (assumed 4/x, x=5..8).sync_word/preamble_length— matchRadioLibWrappers.cppso it interoperates with the existing mesh.- Whether
lora_send_packet()is blocking (assumed yes →isSendComplete()latch is fine). - Where the variant creates the radio object + calls
begin()/setParams()(mirror the S3 radio glue in the variant target).
Compile spike (run first, needs only the toolchain)
# add the env (below) to platformio.ini + a stub boards/tanmatsu.json, then:
PIO=/Users/kaj/Library/Python/3.9/bin/pio
$PIO run -e tanmatsu_companion_radio_touch # expect: platform/toolchain resolves, src/ starts compiling
Outcome tells us: does pioarduino-P4 + Arduino build the shared src/? Compiles →
proceed with display/radio glue. Fails on the IDF-component wiring → that's the known
risk; fall back to the IDF-subproject layout.
env block to add to platformio.ini (draft)
[env:tanmatsu_companion_radio_touch]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip
framework = arduino
board = tanmatsu ; boards/tanmatsu.json — model on pioarduino m5stack-tab5-p4
build_flags =
${env.build_flags} ; reuse the shared touch flags
-D HAS_TANMATSU=1
-I variants/tanmatsu
; board_build.partitions = variants/tanmatsu/partitions_tanmatsu.csv ; TODO
; the BSP/wifi/lora IDF components are pulled via variants/tanmatsu/idf_component.yml
Risks / open questions
- IDF components inside PlatformIO (
badge-bsp/tanmatsu-lora) — the fiddliest part; pioarduino supports the component manager but expect friction. Still the #1 risk. - Display = our own LVGL 8.3 flush onto the badge-bsp
esp_lcdpanel (decided; don't use the LVGL-9.3badge-bsp-lvgl). Work = the flush_cb + thebsp_input→lv_indevglue + an 800×480 layout pass on UITask. Bounded now that the BSP API + konsool reference are known. - WiFi/BLE via the C6 (
esp_wifi_remote+esp_hostedover SDIO). ArduinoWiFi.hon the P4 is backed byesp_wifi_remotein arduino-esp32 3.x, so wadamesh's WiFi (tile fetch, setup wizard, network tab) can likely run mostly unchanged — but it needs the C6 on the rightesp_hostedslave firmware + the components wired in, and it's a known-finicky area in practice (cf. esphome#10956). The template uses IDF wifi helpers (wifi_remote.h), not ArduinoWiFi.h, so confirm Arduino WiFi coexists withtanmatsu-wifi's C6 bring-up during the spike. A real task + risk, not free — but not a rewrite. - Heap/PSRAM — fine: 32 MB PSRAM, and an 800×480 RGB565 buffer is only ~768 KB. Lots of headroom vs the S3 boards.
References
- Template: https://github.com/Nicolai-Electronics/tanmatsu-template (ESP-IDF, P4, PAX, badge-bsp)
- LoRa component: https://github.com/Nicolai-Electronics/esp32-component-tanmatsu-lora (
include/lora.h, v0.3.0) - Precedent board: pioarduino
m5stack-tab5-p4(P4 + MIPI + C6) mesh::Radiobase: monoreposrc/Dispatcher.h; example backendsrc/helpers/esp32/ESPNOWRadio.h