Compare commits

..
43 Commits
Author SHA1 Message Date
Kaj Schittecat 1e00ac3876 chore: refresh Mesh America catalog for beta_19 2026-06-27 15:09:40 +02:00
Kaj SchittecatandClaude Opus 4.8 2b45225e0b touch: beta_19 — scroll-popup crash fix, Radio airtime/telemetry, V4 Home fit
- Fix LVGL scroll-momentum use-after-free reboot when a scrollable overlay
  (emoji / quick-reply / add-channel / channel sheets, Wi-Fi scan, pickers,
  app drawer) is freed mid-throw: new closeScrollOverlay() helper
  (lv_indev_wait_release + lv_obj_del_async). Root-caused from field beta_18
  coredumps.
- Radio & Mesh settings: airtime / duty-cycle readout + "Answer telemetry
  requests" toggle, grouped into RADIO / MESH / SIGNAL sections.
- Accent popup dismissed on Enter-to-send (T-Deck physical keyboard).
- Heltec V4: Expansion-Kit Home env line + chart flow below the stats line
  so they fit the 240x320 screen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:08:03 +02:00
Kaj SchittecatandClaude Opus 4.8 dad7d295dc touch: airtime/duty-cycle readout + answer-telemetry toggle in Radio settings
Two firmware-only additions surfacing capability already in the stack:

- Airtime / duty-cycle readout: the radio modal exposed airtime_factor only as a raw
  'AF' number. Add a derived line — '≈ N% max duty cycle · ~M ms / 40-byte packet' —
  computing duty% from MeshCore's af=(100/duty)-1 and the per-packet time-on-air from
  the Semtech closed-form (new loraToaMs helper). Lets EU-868 users see compliance at
  a glance with no measurement.
- Answer telemetry requests toggle: MeshCore already gates telemetry responses on
  _prefs.telemetry_mode_base/env (0=DENY/2=ALLOW_ALL) but it wasn't in the touch UI.
  Add a switch (new UITask::setTelemetryAllow) that sets battery+environment modes;
  location telemetry keeps its own separate setting so a simple 'allow' can't leak
  position.

(RX boosted gain was already a switch in Experimental settings; per-packet RSSI/SNR
is already in the Monitor; region presets already bundle SF/BW/CR — so those research
items were already shipped and not re-added.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:21:12 +02:00
Kaj SchittecatandClaude Opus 4.8 2d49fb69ec site: add Tanmatsu support; Telegram -> Discord
- List the Tanmatsu (ESP32-P4) in the hero + meta description.
- Add a Tanmatsu box to the standalone installer: no USB flashing — install from the Tanmatsu app store on-device — plus a link to Nicolai Electronics' shop. Boards grid now auto-fits 3 cards.
- Replace the Telegram floating button with Discord (discord.gg/T8X2UaQ7Sm).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:45:47 +02:00
Kaj Schittecat 94ef98a12f chore: refresh Mesh America catalog for beta_18 2026-06-26 19:54:08 +02:00
Kaj Schittecat 2db46ec190 release: beta_18 notes 2026-06-26 19:51:31 +02:00
Kaj SchittecatandClaude Opus 4.8 11775ce5ac tanmatsu(lora): apply runtime radio-config changes to the C6 modem
The Tanmatsu's LoRa modem lives on the C6 coprocessor, shared with the Tanmatsu OS's own
LoRa settings. pushConfig() no-op'd once RX polling started (the remote link can block
under RX load), so a runtime frequency/SF/BW/CR/power change from the wadamesh radio
settings only updated _cfg and took effect on the NEXT REBOOT — the radio stayed on
whatever the OS or boot last set. That is the 'change the wadamesh frequency but receive
nothing on it' report, and why wadamesh's settings never overrode the Tanmatsu OS's.

Now a runtime change marks _cfg dirty and recvRaw() applies it on the RX-poll thread,
between polls: drop to STANDBY (so the link isn't under active-RX load and lora_set_config
can't block), push, re-arm continuous RX. wadamesh's settings now reach the modem live
and win over the OS config. The '0.00001 MHz' the OS page showed is the C6 frequency
corrupted by the two configs fighting; wadamesh consistently owning the config should
clear it — needs on-device confirmation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:44:54 +02:00
Kaj SchittecatandClaude Opus 4.8 7d3b4a4336 tanmatsu: the_mesh in PSRAM on the P4 too (build fix for b986f49)
b986f49 moved the_mesh to PSRAM and changed MyMesh.h's extern to 'MyMesh&', but the
Tanmatsu has its OWN main.cpp (tanmatsu/main/main.cpp, separate from src/main.cpp) that
still declared a plain 'MyMesh the_mesh' global — a conflicting declaration that broke
the P4 build (only the S3 envs were built for b986f49). Apply the same placement-new-
into-PSRAM + reference pattern here; the P4's 32 MB PSRAM gets the same DRAM win.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:44:54 +02:00
Kaj SchittecatandClaude Opus 4.8 f78e3ed493 touch: settings category labels fit on one line (no mid-word split)
The Settings landing tiles centred their label in card_w - 80 px (40 px margins each
side). On the T-Deck's 2-column landscape layout that left only ~64 px, so a single
word ('Bluetooth') split mid-word and 'Radio & Mesh' / 'Quick replies' wrapped to two
lines. Lay the label out to the RIGHT of the left accent icon and centre within that
region (~100 px usable) instead of symmetric centring — every English category name
now fits on one line, unsplit. Wide cards (V4 portrait) are unaffected; long
translations still wrap at word boundaries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:24:53 +02:00
Kaj SchittecatandClaude Opus 4.8 b986f49e1a core: move the_mesh (~42 KB) to PSRAM — reclaim internal DRAM
the_mesh was the single biggest static internal-DRAM consumer at 42 KB (dominated
by the MAX_CONTACTS=100 ContactInfo array inside it). Internal DRAM (~320 KB) is the
scarce resource on the ESP32-S3 — Wi-Fi/BLE DMA + heap fight over it, and the V4 was
hitting ~99% on map-tile load (issue #47b). PSRAM (8 MB) is plentiful.

Place the whole the_mesh object in PSRAM via placement-new into a heap_caps_malloc
(MALLOC_CAP_SPIRAM) block and bind a reference, so every the_mesh.foo() call site is
unchanged. The constructor still runs at the SAME static-init point as the old direct
global (PSRAM is already up then), so timing and behaviour are identical; only the
address moves off internal DRAM. Falls back to internal RAM if PSRAM is absent.
ESP32-only (STM32/RP2040 keep the plain global). The contacts array rides along
inside the object — no vendored-core change.

Measured on heltec_v4 (.dram0.bss): 124160 -> 82000 B = ~41 KB internal DRAM freed.
Verified on the T-Deck: clean boot, [BOOT] mesh ok, reason=0, zero panic markers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:11:13 +02:00
f6192aa6e5 touch+deploy: opt-in OpenTopoMap style + persist map link-lines (open PRs)
Implements the three open PRs, with OpenStreetMap kept as the shipping default and
OpenTopoMap added as a user toggle (the deploy PRs flipped the default to topo —
this keeps OSM default and makes topo opt-in).

PR #61 (@Yazutsu) — persist the map 'Show link lines' toggle across reboots:
TouchCfg gains map_show_links (cfg v20, default 1); saved in mapOptLinesCb, loaded
in begin().

PR #52/#53 (@samuelcoustet) — OpenTopoMap tile proxy, adapted:
- deploy: tiles.wadamesh.com.conf keeps the default /{z}/{x}/{y}.jpg on OSM and
  adds /opentopo/ (OpenTopoMap) + an explicit /osm/ alias; tile-transcode.py grows
  _fetch_tile_png/_tile_response_from_png helpers feeding /osm + /opentopo while the
  root route stays OSM.
- firmware: new map_style pref (cfg v21, 0=OSM default, 1=OpenTopoMap) + a
  'Topographic map' switch in Map → Options. Topo tiles are namespaced on disk
  (/tiles/topo) and fetched via the proxy's /opentopo route, so the OSM and topo
  caches never collide and toggling is instant for already-cached tiles. Topo
  bypasses the OSM-only microSD packs and always fetches online; the shared
  (z,x,y) fetch-dedup ring is cleared on toggle so the new style re-queues.

Legal: OpenTopoMap map tiles are © OpenTopoMap (CC-BY-SA) over © OpenStreetMap
contributors (ODbL) + SRTM. The on-map © attribution and the Options → Info credits
sheet switch to that text when topo is active; the proxy keeps a contactable
User-Agent + 14-day cache per OpenTopoMap's tile-usage policy.

Topo needs the VPS deploy (restart wadamesh-tile-transcode + reload nginx) before
the /opentopo route serves; until then topo tiles 404 (handled as a permanent miss,
no retry hammer). OSM default is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Yazutsu <andrzej@gruziel.pl>
2026-06-26 18:49:41 +02:00
Kaj SchittecatandClaude Opus 4.8 cff58bbaa2 touch: move Discovered sort to a header icon (always visible)
The inline sort button sat after the empty-list early-return, so a Discovered
list with no entries (the common case when auto-add is on) never showed it, and
even when populated it didn't read as a control. Move it to a ⇄ icon button in the
modal header next to the cog — always present, same proven render path as the
cog/purge, opens the same sort sheet. Purge shifts one slot left to make room.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:25:31 +02:00
Kaj SchittecatandClaude Opus 4.8 b31e37cbbe touch: contacts Sort sheet closes on a choice + the close-X works
Regression from the previous commit: making the Sort sheet stay open (for the
asc/desc flip) meant picking a sort no longer dismissed it, and the close-X was a
no-op because it reused the backdrop callback whose target==current_target guard
(meant to ignore child taps on the backdrop) rejected the tap on the badge.

- ctSortOptCb applies the choice and closes the sheet again; flip a mode's
  direction by re-opening and tapping the active row (the ▲/▼ arrow shows it).
- The option-sheet close-X now uses an unconditional close handler instead of the
  guarded backdrop one, so it always dismisses (also fixes the Filter + Discovered
  sort sheets, which share the shell).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:16:11 +02:00
Kaj SchittecatandClaude Opus 4.8 76f0754d0d touch: emoji additions, Chats-header button reorder + QR icon, distance sort
Emoji: bake 4 more Noto colour glyphs (soccer, american football, moai,
transgender flag) and add them to the picker. The trans flag is a ZWJ sequence
the single-codepoint imgfont lookup can't render directly, so its combined Noto
image is keyed on the lead codepoint (U+1F3F3) with the trailing symbol mapped to
the zero-width glyph — the whole sequence renders as the one flag. Only the 4 new
glyphs are baked + spliced (scripts/build/add-emoji.py); the existing 252 stay
byte-identical (no Noto-main art drift).

Chats header: group the three actions at the LEFT edge in the order + (add) /
✓ (mark all read) / QR (share), and draw a real baked QR glyph (qr_icon.h, from
scripts/build/gen-qr-icon.py, recoloured to the text colour) instead of the old
LV_SYMBOL_IMAGE 'picture' stand-in.

Contacts sort: every sort option now toggles ascending/descending — re-tapping the
active row in the Sort sheet flips it and shows a ▲/▼ arrow. New 'Nearest first'
(distance) sort orders by distance from our GPS and excludes contacts that have
shared no location.

Discovered list: gains the same sort sheet (Recently heard / Name / Nearest, each
asc/desc) via a sort button at the top of the list; distance likewise excludes
no-location entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:06:24 +02:00
Kaj SchittecatandClaude Opus 4.8 87cb1c3880 touch: retry transiently-failed map tiles instead of pinning them blank (#20)
renderMapTiles already queues every visible tile (edges + corners) on pan,
and queueTileForFetch marks each one 'seen' the instant it is queued. But the
fetch worker only cleared that mark on success — so a tile whose download hit a
TRANSIENT failure stayed flagged forever and was never re-queued. The heap-
safety gate is the common trigger: during a pan the render thread is decoding a
burst of tiles (heavy internal-DRAM use), so the worker's <12 KB-free skip trips
for several of the freshly-queued edge/corner tiles, which then sit blank until
the user forces a full 'reload tiles'. Wi-Fi blips, a full tile cache and proxy
5xx/timeouts had the same one-shot-and-stuck behaviour.

Add tileFetchForget() and drop the 'seen' mark on every transient failure
(heap gate, Wi-Fi down, cache full, negative/5xx/408/429 HTTP, disk write
hiccup) so the next render miss re-queues the tile automatically. A permanent
4xx rejection still stays marked so we don't re-hammer genuinely-absent tiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:28:41 +02:00
Kaj SchittecatandClaude Opus 4.8 f39e072bd7 touch: stop debug prints corrupting the binary companion stream (#25, #54, #23)
The companion link is a BINARY protocol over Serial (USB-CDC). Unconditional debug
prints in operational paths — [ROOM] login send/resp, [TILE] fetch, [JPG] decode —
interleaved text into that stream and corrupted it. The MeshCore app then throws
'Bad state: Streamsink is bound to a stream' (#54) and the PC-app connect (#25) and
member-room login (#23) fail. Route them through a gated WIRE_DBG macro
(COMPANION_WIRE_DEBUG, off in release) like the existing SYNC_DEBUG_PRINTLN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:18:07 +02:00
Kaj SchittecatandClaude Opus 4.8 bed04417ed touch(v4): narrow the status-bar name window so a long name clears the clock (#47a)
On the 240px V4 bar the node-name window (74px) left only ~4px before the clock, so a
long name visually ran into the time. Shrink it to 66px for clearance. (Best-effort
without a V4 on the bench; clock position may need a follow-up once verified on-device.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:02:38 +02:00
Kaj SchittecatandClaude Opus 4.8 b1adef7364 touch: keyboard-nav toggle no longer snaps the settings list to the top (#45)
beta_15 preserved scroll on close, but turning keyboard-nav ON still jumped to the
top: the focus group was empty (cleared while nav was off) so navMaybeRebuild had no
focus to keep and fell to the first item, scrolling there. Add a one-shot focus hint
that pins focus to the switch you just toggled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 16:49:09 +02:00
Kaj SchittecatandClaude Opus 4.8 36e53d9a1c crash report: only prompt on a crash-boot, not every reset
'The device restarted after a crash' re-appeared after a manual reset / power-cycle:
the prompt gated only on a coredump EXISTING in flash, and the dump persists across
resets until exported. Gate crashReportMaybePrompt() on this boot's reset reason
(panic / task-WDT / int-WDT) instead. The dump stays exportable from About + esptool.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 16:37:50 +02:00
Kaj SchittecatandClaude Opus 4.8 7498c502cc touch: widen task-watchdog grace to 20s to stop flash-op WDT reboots (#56)
Decoded coredumps (device + the beta_16 ELFs) are all task-watchdog panics:
CPU0=IDLE0 CPU1=IDLE1, core 0 in spi_flash_op_block_func. A legitimate-but-slow
flash burst (SPIFFS GC, or a bulk DataStore save = thousands of writes) parks both
cores with the cache disabled, starving both IDLE tasks past the ~5s default so the
TWDT panics. The burst can't be chunked under the limit easily and the per-core
WdtHeavyGuard only covers core 0, so re-init the TWDT with a 20s grace (still
catches a genuine multi-second hang) instead. esp_task_wdt_init re-init reconfigures
the running TWDT (old API on this arduino-esp32).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 16:30:33 +02:00
Kaj SchittecatandClaude Opus 4.8 bbb7d31975 touch(tdeck): fix kbd backlight stuck-on after flash (#33) + lock-screen burn-in (#55)
#33: tdeckKeyboardSetBacklight only marked the I2C write dirty on a value CHANGE, and
the cached default is 0. A reflash resets the ESP32 but not the keyboard's C3, which
keeps its lit backlight, so the boot 'off' request (0==0) was never sent. Force the
first write so the C3's retained state is always overwritten.

#55: a hard-locked LIT panel never dimmed (clock burning in) because reveals/taps kept
_last_input_ms fresh. Add a burn-in guard: arm _lock_lit_ms only on a real off->lit
transition and dim the locked panel after a bounded window regardless of the screen
timeout or held/ghost touches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 08:39:40 +02:00
Kaj SchittecatandClaude Opus 4.8 83cf0202f7 tanmatsu app-store: pre-flight validate name/description vs store regex
Catch a comma/em-dash/over-length description locally and abort, instead of
discovering it as a failed verify_metadata check on the upstream PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:56:33 +02:00
Kaj SchittecatandClaude Opus 4.8 142ea87064 tanmatsu app-store: description without comma/em-dash (verify_metadata regex)
The launcher store's verify_metadata check restricts description to
^[A-Za-z0-9_\- .!@#$%^&*()/\\]+$ (no commas, no em-dash). Reword so the
template passes on its own instead of needing a manual PR edit each release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:53:36 +02:00
Kaj SchittecatandClaude Opus 4.8 1ca2f4e3e6 scripts: mark tanmatsu-appstore-publish.sh executable
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:32:21 +02:00
Kaj SchittecatandClaude Opus 4.8 59b1b2ab09 touch(tanmatsu): beta_17 — Vol± gestures, message LED, microSD, backups on FFat, status-bar fixes
- Vol+ short = top-bar dropdown toggle; long hold = sound master toggle
- Message-notification LED: flash on new message, soft green glow on unread; toggle in Display settings
- Backups use the internal FFat partition (export/scan/import) + Import button on the Backups page
- microSD browsable in the file manager (P4 SDMMC slot 0; the C6 radio's slot 1 untouched)
- Status-bar icon overlaps fixed (charging-slide + language indicator now SC-scaled with the cluster)
- Vol- screen sleep/wake/lock (screen-lock work)
- Companion link: TCP server start + BLE bond NVS persistence + app-sent channel-message mirror

All Tanmatsu changes gated #if defined(HAS_TANMATSU); T-Deck/V4 bins stay byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:30:55 +02:00
Kaj SchittecatandClaude Opus 4.8 c62107c28a tanmatsu: untrack esp-idf/esp-idf-tools (were self-referential symlinks)
main committed tanmatsu/esp-idf + esp-idf-tools as symlinks pointing at their
own absolute path (mode 120000). A fresh 'git checkout main' materialises those
self-loops over the real local ESP-IDF install — breaking the build ('too many
levels of symbolic links' at source export.sh) and deleting the gitignored IDF.
Untrack + gitignore them like the build setup expects; the project-local
ESP-IDF is installed locally, never committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:45:04 +02:00
Kaj Schittecat eb0f3cf0b0 chore: refresh Mesh America catalog for beta_16 2026-06-24 15:45:04 +02:00
Kaj SchittecatandClaude Opus 4.8 36a328a4f7 release: beta_16 notes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:42:41 +02:00
Kaj SchittecatandClaude Opus 4.8 13fca6369f touch: OTA previous-version — one button + version popup + confirm
Reworks the earlier-version OTA UI from three inline buttons into a single
'Install a previous version' button that opens a popup of the available older
betas; picking one shows an are-you-sure confirm (reusing showConfirm) before it
closes and hands off to the existing download/verify/reboot flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:21:37 +02:00
Kaj SchittecatandClaude Opus 4.8 abdb878006 touch: OTA — install a previous version (up to 3 back)
Adds a 'Reinstall an earlier version' section to the Wi-Fi OTA UI (native
dual-OTA boards only): up to three buttons for beta_(latest-1..3), each flashing
that exact published bin via the same download/verify/reboot path. The worker URL
is parameterised on a target version; the existing 'Install update' button now
routes through the same otaStartInstall(latest) helper. Buttons auto-hide when
there aren't that many earlier releases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:08:18 +02:00
Kaj SchittecatandClaude Opus 4.8 2b4f2bc29e touch: fix status-bar clock overlapping the node name on the V4
The clock's runtime placement used the wide-bar -142 offset on every board,
overriding the V4's intended -126 build slot. On the narrow V4 portrait bar that
pulled the clock left into the width-capped node-name window — the long-standing
'clock overlaps the name' issue. Make the placement bar-width-aware: <300 px bars
(V4) use -126/-94, wide bars (T-Deck/Tanmatsu) keep -142/-110.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:47:15 +02:00
Kaj SchittecatandClaude Opus 4.8 79b4f10c25 touch: V4 Sensors tab + Home env widget only when sensors present
Refines the Expansion-Kit UI (#29) so it doesn't intrude on a bare V4: the
Sensors tab and the Home environment widget now appear only when an actual
environment sensor (BME280 / GXHTV3) is detected at boot — battery and GPS no
longer count, so a V4 without the Expansion Kit board shows the normal 5-tab
layout and a clean Home (no empty "Batt ----" / "Temp ...." lines). Adds a
"Show Sensors tab" toggle in Display settings to hide it even when sensors are
present (applies on restart). Tab indices resolve at boot so Settings shifts
back to slot 4 when the Sensors tab is absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:34:29 +02:00
42746ef387 touch: Heltec V4 Expansion Kit UI + mesh console (issue #29)
Heltec V4 Expansion Kit local-sensor UI, board-gated behind HAS_EXPANSION_KIT
(V4 touch env only — T-Deck/Tanmatsu unaffected): a Sensors bottom tab
(atmosphere / GPS / battery + history charts), a Home env summary widget, an
Expansion Kit card in Device settings, and a Local Sensors detail page. Reads
on-board MeshCore sensors (BME280, SHT4x, battery, GPS, buzzer) via the standard
sensor/telemetry API — no new hardware drivers.

Mesh console: enable the existing Terminal on the V4 (HAS_TOUCH_UI), renamed
"Mesh console", with wifi/tcp/ble on-off + ota status quick commands. The
SD-backed file-manager pieces stay T-Deck-only (V4/Tanmatsu get a no-op path).

Tab indices are conditional so non-Expansion boards keep 5 tabs (Settings=4) and
the V4 gets 6 (Sensors=4, Settings=5). The PR's GPS-nav/waypoint and weather
pressure-alarm bundles were intentionally left out.

Ported from PR #29 by @samuelcoustet, re-applied against current main.

Co-Authored-By: samuelcoustet <257299705+samuelcoustet@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 13:57:40 +02:00
300bf89e1a scripts: add companion-protocol serial test harness
Standalone dev tool (pyserial) that drives the companion binary protocol over a
serial port — DEVICE_QUERY/APP_START, battery/storage, stats, telemetry with a
Cayenne-LPP decoder, and meshcomod CLI commands. No firmware coupling; dev-only.

Ported verbatim from PR #29 by @samuelcoustet.

Co-Authored-By: samuelcoustet <257299705+samuelcoustet@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:41:50 +02:00
45e46cd3ab touch: EU keyboard layouts (FR/NL/DE/ES/IT) + auto-select by UI language
Adds French (AZERTY), Dutch, German (QWERTZ), Spanish and Italian layouts — both
the on-screen keyboards and the T-Deck physical-keyboard maps (KeyboardLayoutId
FR..IT). Picking a UI language now auto-enables and selects the matching keyboard
layout, so choosing French gives AZERTY without a separate trip to keyboard
settings. Accented letters still come from the long-press accent popup; no new
fonts required. Extended the settings layout-enable list to match the larger enum
(was a latent out-of-bounds once the layout count grew).

Ported from PR #29 by @samuelcoustet — the layout data applied cleanly; the
UITask wiring was re-applied by hand against current main (the PR was cut from
beta_12 and no longer merges).

Co-Authored-By: samuelcoustet <257299705+samuelcoustet@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:41:50 +02:00
Kaj SchittecatandClaude Opus 4.8 4bb7c08bf0 site: standalone — 'install a previous version' picker (latest 4 betas + notes)
Collapsible version picker in the standalone install guide: a dropdown of the latest 4 betas (from the GitHub releases API — the firmware-host listing has no CORS), rendered release notes per version, and selecting a version re-points both Install buttons at that build (client-built manifest -> the CORS-enabled firmware-host -merged.bin). Defaults to latest; switching back restores the /latest/ manifest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 12:35:20 +02:00
Kaj Schittecat 97b24c4268 Merge PR #48: per-channel scope shows default region as placeholder (tun0) 2026-06-24 12:11:23 +02:00
Kaj Schittecat 981fcba226 Merge PR #49: contact action-sheet landscape overflow fix (tun0) 2026-06-24 12:11:23 +02:00
Kaj Schittecat 50f73f41b0 Merge PR #50: edge-scroll — modals block background scroll (tun0) 2026-06-24 12:11:23 +02:00
Kaj SchittecatandClaude Opus 4.8 c5c0a04484 touch: restore Wi-Fi OTA self-update on native dual-OTA boards
Re-enables the over-the-air update that #74 stubbed out — the block then was
undersized 1.5 MB OTA slots, and both touch tables are now 3.875 MB A/B. Wi-Fi
self-update runs again on natively-flashed boards (every V4 + standalone T-Deck);
Launcher single-slot installs and the Tanmatsu (AppFS) keep the manual path.

- Gated on touchHasOtaUpdateSlot() (a spare A/B slot exists) — already drives the
  update badge, so Launcher/Tanmatsu never reach the install path.
- Self-contained Arduino Update flow in the touch app (the vendored core's OTA
  allow-list is meshcomod-only, so we don't route through board.startHttpOtaFromUrl).
  Fetches the immutable releases/TOUCH/beta_<N>/<board>.bin over plain HTTP from
  firmware.wadamesh.com on the tile worker (off the UI thread); progress polled by
  a UI timer.
- Update.begin(len) does the spare-slot select AND the size check, so a legacy
  too-small slot fails cleanly and falls back to the manual flasher message.
- Verify (MD5) + set-bootable via Update.end(true), then rebootDevice() (saves
  chat history first). A corrupt/short download leaves the running slot intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:56:04 +02:00
Ruben Laban 10ea0d8399 touch: fix contact action sheet overflow on landscape display
The grid_items count missed the conditional "Show on map" button,
causing the card height to be calculated short when the contact has a
GPS fix. Additionally, on landscape displays (sh=240) the backdrop is
only 218px, which repeater cards with many buttons already exceeded.

Fix both: pre-check the GPS condition and include it in grid_items,
and cap the card's displayed height to the available backdrop space
with vertical scroll enabled when the content overflows. The close
badge is FLOATING so it stays anchored regardless of scroll position.
2026-06-24 11:11:56 +02:00
Ruben Laban ab52385f98 touch: edge-scroll — don't fall through to background when dialog is at scroll limit
If lv_layer_top() has a visible widget at the search coordinate (i.e. a
modal or dialog is open), stop the layer search there regardless of whether
a scrollable was found. Prevents the content behind the dialog from being
scrolled when the dialog itself has no more room to scroll.
2026-06-24 10:49:40 +02:00
Ruben Laban 80af33e7f9 touch: show default region scope as placeholder in per-channel scope field
When a global region scope is configured, the per-channel override input
now shows it as greyed-out placeholder text instead of the generic "#region",
so users can see at a glance what scope an empty field will inherit.
Falls back to "(no default)" when no global scope is set.
2026-06-24 10:29:35 +02:00
37 changed files with 5209 additions and 420 deletions

No files matched your search

+14 -1
View File
@@ -3,12 +3,25 @@
Distribution stack for wadamesh: a **VPS nginx origin behind Cloudflare**.
```
tiles.wadamesh.com →CF (HTTP, edge-cached) → nginx → tile.openstreetmap.org
tiles.wadamesh.com →CF (HTTP, edge-cached) → nginx → OpenStreetMap / OpenTopoMap
firmware.wadamesh.com →CF (cache bins) → nginx → /srv/wadamesh/firmware
flasher.wadamesh.com →CF (HTTPS) → web flasher (TODO — see below)
wadamesh.com →CF (HTTPS) → landing page (later)
```
**Map tile styles.** The default `/{z}/{x}/{y}.jpg` route serves **OpenStreetMap**
(the firmware default). An opt-in **OpenTopoMap** topographic style is served from
`/opentopo/{z}/{x}/{y}.jpg` (explicit OSM alias at `/osm/...`); the device requests
it only when the user enables *Map → Options → Topographic map*. Legal: OpenTopoMap
map tiles are **© OpenTopoMap (CC-BY-SA)** over **© OpenStreetMap contributors
(ODbL) + SRTM** — the touch UI shows that attribution when topo is active, and the
14-day disk cache keeps each tile hitting OpenTopoMap at most once per fortnight
(their tile-usage policy asks for a contactable UA + caching, both of which the
transcode service provides). Deploying the topo routes = update
`tiles.wadamesh.com.conf` + `tile-transcode.py`, then
`systemctl restart wadamesh-tile-transcode && nginx -t && systemctl reload nginx`
and purge the Cloudflare cache for `tiles.wadamesh.com/opentopo/*`.
The firmware fetches **tiles + the update-check over plain HTTP** (on-device HTTPS
isn't viable — mbedTLS needs ~30 KB heap, only ~5 KB is free post-Wi-Fi), so the
tile + firmware hosts must stay reachable over HTTP. Cloudflare provides the edge
+8 -8
View File
@@ -16,19 +16,19 @@
"role": "gui",
"title": "Touch UI",
"version": {
"beta_15": {
"notes": "Power-saving now shows as an amber battery icon in the status bar — like iPhone's Low Power Mode — instead of a separate moon icon (T-Deck).\nNew @-mention picker: type @ in the message box to pick a contact from a list and drop their name in (thanks robinshizuyo-dev, #42).\nClosing a settings item, menu or chat no longer snaps the list back to the top — it stays where you left it (thanks ZorkaTheDog, #45).\nMessages you send from the MeshCore companion app now also show up on the device, in the right conversation (thanks HonzaDandy, #46).\nTyping is more reliable on the T-Deck and Tanmatsu: tapping a text field — even one that's already highlighted — starts typing right away, and the accent pop-up no longer interrupts typing outside of chats.\nThe text cursor now only appears when a field is actually ready for input, so it's a dependable \"you can type now\" cue.\nSmoother map panning: tile rendering no longer trips the watchdog and reboots during heavy scrolling.\nKeyboard navigation is now OFF by default. If you prefer driving the UI with the trackball / arrow keys, switch it back on in Settings -> Keyboard (your choice sticks until you change it). The Tanmatsu keeps it on, since it has no touchscreen.\nIncludes everything from beta_14 and the earlier betas.",
"beta_19": {
"notes": "Radio & Mesh settings now show an airtime / duty-cycle readout: roughly how much of the airtime budget your current spreading factor, bandwidth and coding rate use, plus the time-on-air per packet — handy for staying inside regional limits like EU 868.\nA new \"Answer telemetry requests\" toggle in Radio & Mesh settings controls whether other nodes can pull your battery and environment telemetry.\nThe Radio & Mesh settings page is now split into clear RADIO / MESH / SIGNAL sections so the options are easier to find.\nFixed a reboot that could happen when you closed a scrollable popup — the emoji picker, quick replies, the Wi-Fi scan list, the app drawer and similar — while it was still coasting from a flick. Thanks to everyone who sent in crash reports.\nThe accent popup no longer lingers on screen after you send a message on the T-Deck keyboard.\nHeltec V4: the Expansion Kit environment line and mini chart on the Home screen no longer overlap the status text or run off the bottom of the display.\nEverything from beta_18 is included.",
"files": [
{
"type": "flash-wipe",
"name": "wadamesh-heltec-v4-tft-merged.bin",
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_15/wadamesh-heltec-v4-tft-merged.bin",
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_19/wadamesh-heltec-v4-tft-merged.bin",
"title": "Full install (bootloader + firmware)"
},
{
"type": "flash-update",
"name": "wadamesh-heltec-v4-tft.bin",
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_15/wadamesh-heltec-v4-tft.bin",
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_19/wadamesh-heltec-v4-tft.bin",
"title": "Update (app only)"
}
]
@@ -48,19 +48,19 @@
"role": "gui",
"title": "Touch UI",
"version": {
"beta_15": {
"notes": "Power-saving now shows as an amber battery icon in the status bar — like iPhone's Low Power Mode — instead of a separate moon icon (T-Deck).\nNew @-mention picker: type @ in the message box to pick a contact from a list and drop their name in (thanks robinshizuyo-dev, #42).\nClosing a settings item, menu or chat no longer snaps the list back to the top — it stays where you left it (thanks ZorkaTheDog, #45).\nMessages you send from the MeshCore companion app now also show up on the device, in the right conversation (thanks HonzaDandy, #46).\nTyping is more reliable on the T-Deck and Tanmatsu: tapping a text field — even one that's already highlighted — starts typing right away, and the accent pop-up no longer interrupts typing outside of chats.\nThe text cursor now only appears when a field is actually ready for input, so it's a dependable \"you can type now\" cue.\nSmoother map panning: tile rendering no longer trips the watchdog and reboots during heavy scrolling.\nKeyboard navigation is now OFF by default. If you prefer driving the UI with the trackball / arrow keys, switch it back on in Settings -> Keyboard (your choice sticks until you change it). The Tanmatsu keeps it on, since it has no touchscreen.\nIncludes everything from beta_14 and the earlier betas.",
"beta_19": {
"notes": "Radio & Mesh settings now show an airtime / duty-cycle readout: roughly how much of the airtime budget your current spreading factor, bandwidth and coding rate use, plus the time-on-air per packet — handy for staying inside regional limits like EU 868.\nA new \"Answer telemetry requests\" toggle in Radio & Mesh settings controls whether other nodes can pull your battery and environment telemetry.\nThe Radio & Mesh settings page is now split into clear RADIO / MESH / SIGNAL sections so the options are easier to find.\nFixed a reboot that could happen when you closed a scrollable popup — the emoji picker, quick replies, the Wi-Fi scan list, the app drawer and similar — while it was still coasting from a flick. Thanks to everyone who sent in crash reports.\nThe accent popup no longer lingers on screen after you send a message on the T-Deck keyboard.\nHeltec V4: the Expansion Kit environment line and mini chart on the Home screen no longer overlap the status text or run off the bottom of the display.\nEverything from beta_18 is included.",
"files": [
{
"type": "flash-wipe",
"name": "wadamesh-tdeck-merged.bin",
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_15/wadamesh-tdeck-merged.bin",
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_19/wadamesh-tdeck-merged.bin",
"title": "Full install (bootloader + firmware)"
},
{
"type": "flash-update",
"name": "wadamesh-tdeck.bin",
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_15/wadamesh-tdeck.bin",
"url": "https://firmware.wadamesh.com/releases/TOUCH/beta_19/wadamesh-tdeck.bin",
"title": "Update (app only)"
}
]
+46 -2
View File
@@ -1,4 +1,5 @@
# tiles.wadamesh.com — OSM tile proxy for the wadamesh map tab.
# tiles.wadamesh.com — map-tile proxy for the wadamesh map tab. Default style is
# OpenStreetMap; an opt-in OpenTopoMap topographic style is served from /opentopo/.
#
# The firmware fetches plain HTTP (on-device HTTPS isn't workable: mbedTLS wants
# ~30 KB heap for a handshake and only ~5 KB is free after Wi-Fi associates), so
@@ -62,6 +63,49 @@ server {
proxy_connect_timeout 5s;
}
# OpenTopoMap (topographic) — OPT-IN alternate style. The firmware requests
# this path only when the user enables "Topographic map"; the default
# /{z}/{x}/{y}.jpg above stays OpenStreetMap. Legal: map © OpenTopoMap
# (CC-BY-SA), data © OpenStreetMap contributors + SRTM — the touch UI shows
# that attribution when topo is selected. The 14-day disk cache means each
# tile hits OpenTopoMap at most once per fortnight (their tile policy asks it).
location ~ "^/opentopo/(?<z>[0-9]{1,2})/(?<x>[0-9]{1,7})/(?<y>[0-9]{1,7})\.jpg$" {
proxy_pass http://127.0.0.1:5005/opentopo/$z/$x/$y.jpg;
proxy_set_header Host "tiles.wadamesh.com";
proxy_cache wadamesh_tiles;
proxy_cache_key "opentopo-jpg:$z/$x/$y";
proxy_cache_valid 200 14d;
proxy_cache_valid 404 1d;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status always;
add_header Cache-Control "public, max-age=2592000" always;
proxy_buffering on;
proxy_read_timeout 20s;
proxy_connect_timeout 5s;
}
# Explicit OpenStreetMap alias (identical to the default root path above).
location ~ "^/osm/(?<z>[0-9]{1,2})/(?<x>[0-9]{1,7})/(?<y>[0-9]{1,7})\.jpg$" {
proxy_pass http://127.0.0.1:5005/osm/$z/$x/$y.jpg;
proxy_set_header Host "tiles.wadamesh.com";
proxy_cache wadamesh_tiles;
proxy_cache_key "osm-jpg:$z/$x/$y";
proxy_cache_valid 200 14d;
proxy_cache_valid 404 1d;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status always;
add_header Cache-Control "public, max-age=2592000" always;
proxy_buffering on;
proxy_read_timeout 20s;
proxy_connect_timeout 5s;
}
# Elevation profile for the line-of-sight analyzer (same local service).
location = /elev {
proxy_pass http://127.0.0.1:5005;
@@ -98,7 +142,7 @@ server {
# Tiny landing so a human hitting the bare hostname isn't met with 404.
location = / {
default_type text/plain;
return 200 "wadamesh tile proxy\n\nUsage: GET /{z}/{x}/{y}.jpg (HTTP only)\nUpstream: tile.openstreetmap.org (PNG transcoded to JPEG)\n";
return 200 "wadamesh tile proxy (HTTP only)\n\n /{z}/{x}/{y}.jpg -> OpenStreetMap (default)\n /osm/{z}/{x}/{y}.jpg -> OpenStreetMap\n /opentopo/{z}/{x}/{y}.jpg -> OpenTopoMap (CC-BY-SA)\n\nUpstream PNG transcoded to JPEG.\n";
}
location / { return 404; }
+76 -7
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WADAMESH — a touchscreen for your mesh radio</title>
<meta name="description" content="Open-source MeshCore touch-UI firmware for the LilyGo T-Deck and Heltec V4. Map, chat, channels, contacts — install from your browser.">
<meta name="description" content="Open-source MeshCore touch-UI firmware for the LilyGo T-Deck, Heltec V4 and Tanmatsu. Map, chat, channels, contacts — install from your browser.">
<link rel="icon" href="wadamesh-badge.svg">
<meta name="theme-color" content="#ffffff">
<link rel="preconnect" href="https://fonts.googleapis.com">
@@ -135,7 +135,7 @@
.step p{color:var(--mut); font-size:.92rem}
.step code{background:#fff; border:1px solid var(--line); border-radius:6px; padding:1px 6px; font-size:.85em; font-family:'JetBrains Mono',monospace}
.boards{display:grid; gap:14px; grid-template-columns:1fr 1fr; max-width:760px; margin:18px auto 0}
.boards{display:grid; gap:14px; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); max-width:760px; margin:18px auto 0}
@media(max-width:560px){.boards{grid-template-columns:1fr}}
.board{border:1px solid var(--line); background:var(--card); border-radius:14px; padding:18px; display:flex;
flex-direction:column; gap:10px; align-items:flex-start; box-shadow:0 8px 26px -22px rgba(21,24,30,.5)}
@@ -147,6 +147,22 @@
.dl:hover{background:var(--teal-ink)}
.dl svg{width:15px;height:15px}
.hint{max-width:760px;margin:18px auto 0;color:var(--mut);font-size:.86rem;text-align:center}
/* "install a previous version" picker */
.verpick{max-width:760px; margin:16px auto 0; border:1px solid var(--line); border-radius:12px; background:var(--bg2); text-align:left}
.verpick summary{cursor:pointer; padding:12px 16px; font-weight:600; font-size:.92rem; color:var(--ink); list-style:none; user-select:none}
.verpick summary::-webkit-details-marker{display:none}
.verpick summary::before{content:"\25B8"; color:var(--teal); margin-right:9px; display:inline-block; transition:transform .15s}
.verpick[open] summary::before{transform:rotate(90deg)}
.verpick-body{padding:2px 16px 16px}
.verpick-row{display:flex; align-items:center; gap:10px}
.verpick-row label{font-size:.9rem; color:var(--mut)}
#verSel{font:inherit; font-size:.92rem; padding:7px 11px; border:1px solid var(--line); border-radius:8px; background:#fff; color:var(--ink); cursor:pointer}
.verpick-note{margin:9px 0 0; font-size:.86rem; color:var(--teal-ink)}
.rn{margin-top:12px; max-height:280px; overflow:auto; font-size:.88rem; color:var(--mut); line-height:1.55}
.rn .rn-h{font-size:.92rem; color:var(--ink); font-weight:700; margin:13px 0 5px} .rn .rn-h:first-child{margin-top:0}
.rn ul{margin:5px 0 0; padding-left:18px} .rn li{margin:3px 0}
.rn p{margin:6px 0} .rn code{background:#fff; border:1px solid var(--line); border-radius:5px; padding:1px 5px; font-size:.86em}
.rn a{color:var(--teal-ink)}
.noserial{display:none;color:#b4631a;text-align:center;margin-top:14px}
body.no-serial .noserial{display:block}
@@ -198,8 +214,8 @@
<!-- floating quick links -->
<div class="fab-rail left">
<a class="fab" href="https://t.me/+fHuhG67-RFJlNmFk" target="_blank" rel="noopener" aria-label="Join us on Telegram" title="Join us on Telegram">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9.04 15.47 8.7 19.9c.5 0 .72-.22.98-.47l2.36-2.22 4.89 3.53c.9.5 1.53.24 1.77-.83l3.2-14.5c.3-1.33-.5-1.85-1.36-1.53L1.9 9.46c-1.3.5-1.28 1.22-.22 1.55l5.05 1.57L18.4 5.9c.55-.36 1.05-.16.64.2L9.04 15.47z"/></svg>
<a class="fab" href="https://discord.gg/T8X2UaQ7Sm" target="_blank" rel="noopener" aria-label="Join us on Discord" title="Join us on Discord">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.317 4.369a19.79 19.79 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.249a18.27 18.27 0 0 0-5.487 0 12.6 12.6 0 0 0-.617-1.249.077.077 0 0 0-.079-.037A19.74 19.74 0 0 0 3.677 4.369a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.2 14.2 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.1 13.1 0 0 1-1.872-.892.077.077 0 0 1-.008-.128c.126-.094.252-.192.372-.291a.074.074 0 0 1 .078-.01c3.927 1.793 8.18 1.793 12.061 0a.074.074 0 0 1 .079.009c.12.099.246.198.373.292a.077.077 0 0 1-.006.127 12.3 12.3 0 0 1-1.873.891.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.84 19.84 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.06.06 0 0 0-.031-.029zM8.02 15.331c-1.182 0-2.157-1.086-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.332-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.086-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.332-.946 2.418-2.157 2.418z"/></svg>
</a>
<a class="fab" href="#videos" aria-label="Community videos" title="Community videos">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
@@ -256,7 +272,7 @@
<span class="byline">by meshcomod</span>
</div>
<p class="lede">A real touchscreen UI for your mesh radio.</p>
<p class="sub">MeshCore MESHCOMOD firmware for the <b>LilyGo&nbsp;T-Deck</b> and <b>Heltec&nbsp;V4</b> — map, chat, channels and contacts on the device itself, <b>and</b> a companion radio for the <b>MeshCore app</b> at the same time.</p>
<p class="sub">MeshCore MESHCOMOD firmware for the <b>LilyGo&nbsp;T-Deck</b>, <b>Heltec&nbsp;V4</b> and <b>Tanmatsu</b> — map, chat, channels and contacts on the device itself, <b>and</b> a companion radio for the <b>MeshCore app</b> at the same time.</p>
<div class="pills">
<span class="pill" tabindex="0"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 20l-6-3V4l6 3 6-3 6 3v13l-6-3-6 3z"/><path d="M9 7v13M15 4v13"/></svg>Offline map
<span class="pop"><b>Offline map</b>A real OpenStreetMap you can pan and zoom on the device. Pre-load tile packs to an SD card for fully offline use, or fetch over Wi-Fi — and see your contacts' GPS positions plotted on it.</span></span>
@@ -312,10 +328,21 @@
</div>
<div class="boards">
<div class="board"><h4>Heltec V4 + TFT</h4><p>ESP32-S3 with CHSC6x touch.</p>
<esp-web-install-button manifest="https://firmware.wadamesh.com/latest/manifest-heltec-v4-tft.json"></esp-web-install-button></div>
<esp-web-install-button id="inst-heltec" manifest="https://firmware.wadamesh.com/latest/manifest-heltec-v4-tft.json"></esp-web-install-button></div>
<div class="board"><h4>LilyGo T-Deck / Plus</h4><p>ESP32-S3 keyboard handheld.</p>
<esp-web-install-button manifest="https://firmware.wadamesh.com/latest/manifest-tdeck.json"></esp-web-install-button></div>
<esp-web-install-button id="inst-tdeck" manifest="https://firmware.wadamesh.com/latest/manifest-tdeck.json"></esp-web-install-button></div>
<div class="board"><h4>Tanmatsu</h4><p>ESP32-P4 keyboard handheld.</p>
<p style="color:var(--mut);font-size:.84rem;margin:0;line-height:1.5">Already have one? No USB flashing needed — install WADAMESH straight from the <b>Tanmatsu app store</b> on the device.</p>
<a class="dl" href="https://shop.nicolaielectronics.nl/" target="_blank" rel="noopener"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>Get a Tanmatsu</a></div>
</div>
<details class="verpick">
<summary>Install a previous version</summary>
<div class="verpick-body">
<div class="verpick-row"><label for="verSel">Version</label><select id="verSel" aria-label="Firmware version"></select></div>
<p class="verpick-note" id="verActive" hidden></p>
<div id="verNotes" class="rn"></div>
</div>
</details>
<div class="noserial">Your browser can't flash here — use <b>Chrome</b> or <b>Edge</b> on a desktop.</div>
<p class="hint">T-Deck not detected? Hold the trackball while tapping reset to force download mode.</p>
</div>
@@ -497,6 +524,48 @@ We use no tracking, analytics, or advertising cookies. Our CDN and security prov
const docsBtn = document.getElementById('docsBtn');
if (docsBtn) docsBtn.addEventListener('click', () => toast('Documentation — coming soon!'));
// ---- standalone: install a previous version (latest 4 betas + release notes) ----
(function(){
const sel = document.getElementById('verSel'); if (!sel) return;
const wrap = sel.closest('.verpick');
const notesEl = document.getElementById('verNotes'), activeEl = document.getElementById('verActive');
const btnH = document.getElementById('inst-heltec'), btnT = document.getElementById('inst-tdeck');
const latestH = btnH && btnH.getAttribute('manifest'), latestT = btnT && btnT.getAttribute('manifest');
const num = t => parseInt((String(t).match(/beta_(\d+)/) || [])[1] || '-1', 10);
let notes = {};
const esc = s => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const inl = s => esc(s).replace(/\*\*(.+?)\*\*/g,'<b>$1</b>').replace(/`([^`]+?)`/g,'<code>$1</code>')
.replace(/\[(.+?)\]\((https?:\/\/[^)\s]+)\)/g,'<a href="$2" target="_blank" rel="noopener">$1</a>');
function md(t){ let out='', ul=false; const close=()=>{ if(ul){ out+='</ul>'; ul=false; } };
(t||'').split(/\r?\n/).forEach(line=>{ line=line.trim(); let m;
if(!line) close();
else if(m=line.match(/^#{1,6}\s+(.*)/)){ close(); out+='<div class="rn-h">'+inl(m[1])+'</div>'; }
else if(m=line.match(/^[-*]\s+(.*)/)){ if(!ul){ out+='<ul>'; ul=true; } out+='<li>'+inl(m[1])+'</li>'; }
else { close(); out+='<p>'+inl(line)+'</p>'; } });
close(); return out; }
function showNotes(tag){ notesEl.innerHTML = notes[tag] ? md(notes[tag])
: '<p>Release notes on <a href="https://github.com/ALLFATHER-BV/wadamesh/releases/tag/'+tag+'" target="_blank" rel="noopener">GitHub</a>.</p>'; }
function blobManifest(tag, board){ const m = { name:'wadamesh '+tag, version:tag, new_install_prompt_erase:true,
builds:[{ chipFamily:'ESP32-S3', parts:[{ path:'https://firmware.wadamesh.com/releases/TOUCH/'+tag+'/wadamesh-'+board+'-merged.bin', offset:0 }] }] };
return URL.createObjectURL(new Blob([JSON.stringify(m)], {type:'application/json'})); }
function apply(){ const tag = sel.value, isLatest = sel.selectedIndex === 0;
if (isLatest){ if(btnH&&latestH) btnH.setAttribute('manifest', latestH); if(btnT&&latestT) btnT.setAttribute('manifest', latestT); activeEl.hidden = true; }
else { if(btnH) btnH.setAttribute('manifest', blobManifest(tag,'heltec-v4-tft')); if(btnT) btnT.setAttribute('manifest', blobManifest(tag,'tdeck'));
activeEl.hidden = false; activeEl.innerHTML = 'The <b>Install</b> buttons above now flash <b>'+tag+'</b>.'; }
showNotes(tag); }
// GitHub API gives version list + notes in one CORS-enabled call (the firmware
// host listing has no CORS); bins are still pulled from the firmware host.
fetch('https://api.github.com/repos/ALLFATHER-BV/wadamesh/releases?per_page=12').then(r=>r.ok?r.json():[]).then(rs=>{
const rels = (Array.isArray(rs)?rs:[]).filter(r=>r&&/^beta_\d+$/.test(r.tag_name||''));
rels.forEach(r=>{ notes[r.tag_name]=r.body||''; });
const tags = rels.map(r=>r.tag_name).sort((a,b)=>num(b)-num(a)).slice(0,4);
if (!tags.length){ if(wrap) wrap.hidden = true; return; }
sel.innerHTML = tags.map((t,i)=>'<option value="'+t+'">'+t+(i===0?' — latest':'')+'</option>').join('');
showNotes(tags[0]);
sel.addEventListener('change', apply);
}).catch(()=>{ if(wrap) wrap.hidden = true; });
})();
</script>
<!-- animated mesh backdrop — Vanta NET (3D dot-and-line network) in wadamesh teal on white.
@@ -0,0 +1,44 @@
# WadaMesh
A full **[MeshCore](https://github.com/meshcore-dev/MeshCore)** client for the Tanmatsu.
It runs the LoRa mesh, Wi-Fi and Bluetooth radios **all at the same time**, so the device
works as a complete standalone communicator *and* as a phone companion at once — no
mode-switching, no rebooting to swap radios.
MeshCore is an encrypted, infrastructure-free LoRa mesh network — messages hop from device
to device with no cell tower, Wi-Fi or internet in between. WadaMesh gives it a full
graphical front-end on the Tanmatsu's own screen and keyboard.
## Why WadaMesh
- **Multi-radio, simultaneously.** LoRa, Wi-Fi and Bluetooth all live at once. Chat on the
mesh while Wi-Fi streams map tiles and a phone stays paired over Bluetooth — concurrently.
- **Standalone *and* companion at the same time.** A complete client on its own screen and
keyboard, while a phone app can connect over Bluetooth or Wi-Fi in parallel. You don't
have to pick a role.
- **Live mesh map.** Plot yourself and your contacts on a real OpenStreetMap from their
shared GPS positions (tiles fetched and cached over Wi-Fi).
- **Built for the keyboard.** Arrow-key navigation, type-to-edit and function-key shortcuts —
designed around the Tanmatsu's hardware, not bolted on from a touchscreen.
- **Polished and localised.** Full-colour emoji and 11 interface languages.
## Features
- **Messaging** — direct messages and channels with unread markers, a "new messages"
divider and quick replies.
- **Contacts & map** — contact list with live signal and distance, plus the mesh map.
- **Radio control** — frequency / spreading factor / bandwidth / TX power, and GPS.
- **Connectivity** — LoRa, Wi-Fi and Bluetooth at once; use it standalone or as a Wi-Fi +
Bluetooth companion for a phone.
## Links
- Source & releases: <https://github.com/ALLFATHER-BV/wadamesh>
- Web flasher & project info: <https://wadamesh.com>
## Licence
WadaMesh is distributed under **GPL-3.0** — see [LICENSE](LICENSE). It incorporates
[MeshCore](https://github.com/meshcore-dev/MeshCore) (MIT, © Scott Powell /
rippleradios.com); MeshCore-derived files keep their MIT notices, and the combined
work is GPL (MIT is GPL-compatible). See [NOTICE](NOTICE) for third-party licenses.
Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,30 @@
{
"name": "WadaMesh",
"description": "MeshCore client running LoRa Wi-Fi & Bluetooth at once. A standalone unit and phone companion in one.",
"categories": [
"tools",
"utils",
"hardware"
],
"version": "1.0.0",
"icon": {
"16x16": "icon16.png",
"32x32": "icon32.png",
"64x64": "icon64.png"
},
"author": "Kaj Schittecat - ALLFATHER BV",
"license_type": "GPL-3.0",
"license_file": "LICENSE",
"application": [
{
"targets": [
"tanmatsu",
"konsool"
],
"revision": 1,
"type": "appfs",
"executable": "wadamesh.bin",
"assets": []
}
]
}
+36 -11
View File
@@ -39,8 +39,17 @@ except ImportError:
sys.exit("ERROR: pip install flask pillow requests")
OSM_URL = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
# OSM tile policy: identify yourself with a contactable UA. This is what OSM
# sees — the device never talks to OSM directly.
# OpenTopoMap (topographic / terrain-relief style) — an OPT-IN alternate style.
# The firmware default stays OpenStreetMap; the device only requests /opentopo/
# when the user enables it. Like OSM it serves PNG, so it goes through the same
# fetch+transcode path. Legal: map style © OpenTopoMap (CC-BY-SA), underlying
# data © OpenStreetMap contributors (ODbL) + SRTM — the touch UI shows that
# attribution whenever topo is the selected style. OpenTopoMap's tile-usage
# policy asks for a contactable User-Agent + caching; nginx caches results 14
# days so each unique tile hits OpenTopoMap at most once per fortnight.
OPENTOPO_URL = "https://tile.opentopomap.org/{z}/{x}/{y}.png"
# Tile policy: identify yourself with a contactable UA. This is what the upstream
# tile servers see — the device never talks to them directly.
OSM_UA = "wadamesh-tile-proxy/1.0 (+https://wadamesh.com)"
JPEG_QUALITY = 80 # slippy tiles compress well; 80 ≈ visually lossless
MAX_TILE_BYTES = 256 * 1024
@@ -57,40 +66,56 @@ _session = requests.Session()
_session.headers.update({"User-Agent": OSM_UA, "Accept": "image/png,image/*"})
@app.get("/<int:z>/<int:x>/<int:y>.jpg")
def tile(z: int, x: int, y: int):
# Range-check before touching OSM so we can't be turned into an open proxy.
def _fetch_tile_png(upstream_url: str, z: int, x: int, y: int) -> bytes:
# Range-check before touching the upstream so we can't be turned into an open proxy.
if not (0 <= z <= 19):
abort(404)
n = 1 << z
if not (0 <= x < n and 0 <= y < n):
abort(404)
try:
r = _session.get(OSM_URL.format(z=z, x=x, y=y), timeout=REQUEST_TIMEOUT,
r = _session.get(upstream_url.format(z=z, x=x, y=y), timeout=REQUEST_TIMEOUT,
stream=True)
except requests.RequestException:
abort(502)
if r.status_code != 200:
# Pass OSM's status through (404 = empty/sea tile — nginx caches it).
# Pass the upstream status through (404 = empty/sea tile — nginx caches it).
abort(r.status_code if r.status_code in (404, 429) else 502)
raw = r.raw.read(MAX_TILE_BYTES + 1, decode_content=True)
if len(raw) > MAX_TILE_BYTES:
abort(502)
return raw
def _tile_response_from_png(raw: bytes) -> Response:
try:
img = Image.open(io.BytesIO(raw)).convert("RGB")
out = io.BytesIO()
img.save(out, "JPEG", quality=JPEG_QUALITY, optimize=True)
except Exception:
abort(502)
return Response(out.getvalue(), mimetype="image/jpeg",
headers={"Cache-Control": "public, max-age=2592000"})
# Default path the firmware uses → OpenStreetMap (the shipping default style).
@app.get("/<int:z>/<int:x>/<int:y>.jpg")
def tile(z: int, x: int, y: int):
return _tile_response_from_png(_fetch_tile_png(OSM_URL, z, x, y))
# Explicit OpenStreetMap alias (identical to the root path).
@app.get("/osm/<int:z>/<int:x>/<int:y>.jpg")
def tile_osm(z: int, x: int, y: int):
return _tile_response_from_png(_fetch_tile_png(OSM_URL, z, x, y))
# OpenTopoMap (topographic) — opt-in alternate style selected on the device.
@app.get("/opentopo/<int:z>/<int:x>/<int:y>.jpg")
def tile_opentopo(z: int, x: int, y: int):
return _tile_response_from_png(_fetch_tile_png(OPENTOPO_URL, z, x, y))
@app.get("/elev")
def elev():
"""Elevation profile for the line-of-sight analyzer.
+3
View File
@@ -127,6 +127,9 @@ build_flags =
-I src/ui-touch
-D UI_LVGL=1
-D HAS_TOUCH_UI=1
; Heltec V4 Expansion Kit: local environment sensors UI (Sensors tab, Home env
; widget, Local-Sensors page). Board-gated — only this env compiles it.
-D HAS_EXPANSION_KIT=1
-D FIRMWARE_OTA_ENV='"heltec_v4_tft_companion_radio_usb_tcp_touch"'
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf.h
+16
View File
@@ -0,0 +1,16 @@
# beta_16 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
Heltec V4 Expansion Kit support: a dedicated Sensors tab (temperature, humidity, pressure, GPS and battery, with small history charts), a Home-screen environment readout, and an on-device "mesh console" for poking at the radio. It only turns up when a sensor board is actually attached, so a bare V4 looks exactly as it did before. This one was a real grind to land — it came in as a huge pull request built on an old beta, and getting it in cleanly meant re-doing most of it by hand against the current code and carefully fencing it all off so it can't bloat or disturb the T-Deck and Tanmatsu builds. Thanks to Samuel Coustet for the original work (#29).
French, Dutch, German, Spanish and Italian keyboards, on-screen and on the T-Deck's physical keys. Choosing one of those as your interface language now auto-selects the matching layout. Also from Samuel Coustet (#29).
Wi-Fi firmware updates are back: on a normally-flashed device (not the Launcher) you can update straight from Settings -> About, no cable needed.
You can also roll back now — "Install a previous version" lets you reinstall one of the last few releases over Wi-Fi, with a confirmation first since it's a downgrade.
# --- Fixed ---
The status-bar clock no longer overlaps the node name on the V4 (it had been crammed against it for a good while).
Closing a dialog no longer lets a scroll carry through to the page behind it (thanks tun0, #50).
The contact action sheet no longer overflows the screen in landscape (thanks tun0, #49).
The per-channel region field now shows your default region as the hint, instead of a generic placeholder (thanks tun0, #48).
# --- Also ---
Everything from beta_15 is included.
+23
View File
@@ -0,0 +1,23 @@
# beta_18 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
A topographic map style: turn on Map -> Options -> "Topographic map" for terrain-relief tiles (OpenStreetMap stays the default). Thanks to Samuel Coustet for the proxy work (#52).
New emoji in the picker: soccer ball, american football, moai, and the transgender flag.
Contacts can now sort ascending or descending, and there's a new "Nearest first" sort that orders contacts by distance from you (contacts without a shared location are left out).
The Discovered list gets the same sorting — by most-recently-heard, name, or distance — from a new sort button in its header.
The Chats header is tidied up: the add / mark-all-read / share-contact buttons are grouped together on the left, and sharing your contact now uses a proper QR icon.
# --- Fixed ---
The USB/PC companion connection no longer drops or throws "bad state" errors mid-session. The same fix also clears up repeater admin mode disconnecting and not being able to log in to member rooms.
Map tiles along the edges and corners of the view now fill in automatically as you pan, instead of needing a manual "reload tiles".
Reclaimed about 41 KB of memory by moving a large internal buffer to PSRAM — this fixes the Heltec V4 running out of RAM and rebooting when it loaded map tiles, and leaves more headroom on every board.
The map's "Show link lines" toggle is now remembered across reboots. Thanks to Yazutsu (#61).
Settings category names (Bluetooth, Radio & Mesh, Quick replies, ...) now fit on one line instead of wrapping or splitting mid-word.
Turning keyboard navigation on or off no longer snaps the settings list back to the top.
Heltec V4: the node name in the status bar no longer covers the clock.
T-Deck: the keyboard backlight no longer gets stuck on after flashing when it's set to Off.
Crash reporting is more reliable (it only prompts after an actual crash, not on every restart), and there are fewer stray reboots while saving settings.
Tanmatsu: changing the LoRa radio settings (frequency and so on) now takes effect immediately, without needing a reboot, and the app's settings now drive the radio.
# --- Also ---
Everything from beta_16 (and the Tanmatsu's beta_17) is included.
+14
View File
@@ -0,0 +1,14 @@
# beta_19 (one user-facing note per non-blank, non-# line; # lines are section comments)
# --- New ---
Radio & Mesh settings now show an airtime / duty-cycle readout: roughly how much of the airtime budget your current spreading factor, bandwidth and coding rate use, plus the time-on-air per packet — handy for staying inside regional limits like EU 868.
A new "Answer telemetry requests" toggle in Radio & Mesh settings controls whether other nodes can pull your battery and environment telemetry.
The Radio & Mesh settings page is now split into clear RADIO / MESH / SIGNAL sections so the options are easier to find.
# --- Fixed ---
Fixed a reboot that could happen when you closed a scrollable popup — the emoji picker, quick replies, the Wi-Fi scan list, the app drawer and similar — while it was still coasting from a flick. Thanks to everyone who sent in crash reports.
The accent popup no longer lingers on screen after you send a message on the T-Deck keyboard.
Heltec V4: the Expansion Kit environment line and mini chart on the Home screen no longer overlap the status text or run off the bottom of the display.
# --- Also ---
Everything from beta_18 is included.
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
# Bake a few EXTRA Noto colour emoji into src/ui-touch/emoji_data.{c,h} WITHOUT
# regenerating (and re-downloading) the existing 252 — those stay byte-identical,
# so there's no Noto-`main` art drift. Splices new glyph arrays + sorted lookup
# entries into the file in place. Re-run is idempotent (new cps replace, existing
# untouched). PX must match the existing bake (16) and the imgfont in UITask.
#
# python3 scripts/build/add-emoji.py
#
# Source art: googlefonts/noto-emoji png/128 (OFL). RGB565+alpha, swap=0.
import os, re, sys, urllib.request
from PIL import Image
PX = 16
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
OUTDIR = os.path.join(ROOT, "src", "ui-touch")
OUTC = os.path.join(OUTDIR, "emoji_data.c")
OUTH = os.path.join(OUTDIR, "emoji_data.h")
CACHE = os.path.join(ROOT, "data", "emoji-cache")
BASE = "https://raw.githubusercontent.com/googlefonts/noto-emoji/main/png/128/emoji_u{}.png"
os.makedirs(CACHE, exist_ok=True)
# Single-codepoint emoji: (codepoint, noto-filename-stem)
SINGLE = [
(0x26BD, "26bd"), # soccer ball âš½
(0x1F3C8, "1f3c8"), # american football ðŸ
(0x1F5FF, "1f5ff"), # moai 🗿
]
# ZWJ sequences baked as ONE combined image, keyed on the lead codepoint, with the
# trailing visible symbol(s) mapped to the zero-width glyph so the whole UTF-8
# sequence renders as the single combined emoji. The bare lead cp (a plain waving
# white flag U+1F3F3) is not offered anywhere in the UI, so reusing it as the key
# is safe. (noto-emoji drops the FE0F variation selectors from png filenames.)
SEQ = [
# (key_cp, noto-stem, [extra cps -> zero])
(0x1F3F3, "1f3f3_200d_26a7", [0x26A7]), # transgender flag ðŸ³ï¸â€âš§ï¸
]
# Country/region flags: a regional-indicator PAIR (e.g. LS = U+1F1F1 U+1F1F8) baked
# as ONE image keyed on the LEAD indicator, the trailing indicator mapped to the
# zero-width glyph (same trick as SEQ). Noto's png/128 ships no flags, so pull the
# flat rectangle from the same Noto project's third_party/region-flags. No bare
# regional indicator is ever offered in the UI, so reusing the lead cp as the key
# is safe — and the picker currently has no other flag using these indicators.
FLAGS = [
# (lead_cp, trail_cp, ISO-3166-alpha-2)
(0x1F1F1, 0x1F1F8, "LS"), # Lesotho
]
def fetch(stem):
fn = os.path.join(CACHE, "u_{}.png".format(stem))
if os.path.exists(fn) and os.path.getsize(fn) > 0:
return fn
for url in (BASE.format(stem), BASE.format(stem + "_fe0f")):
try:
req = urllib.request.Request(url, headers={"User-Agent": "emoji-gen"})
data = urllib.request.urlopen(req, timeout=20).read()
with open(fn, "wb") as f:
f.write(data)
return fn
except Exception:
continue
return None
def fetch_flag(iso):
fn = os.path.join(CACHE, "flag_{}.png".format(iso))
if os.path.exists(fn) and os.path.getsize(fn) > 0:
return fn
url = ("https://raw.githubusercontent.com/googlefonts/noto-emoji/main"
"/third_party/region-flags/png/{}.png".format(iso))
try:
req = urllib.request.Request(url, headers={"User-Agent": "emoji-gen"})
data = urllib.request.urlopen(req, timeout=20).read()
with open(fn, "wb") as f:
f.write(data)
return fn
except Exception:
return None
def to_rgb565a8(img):
# IDENTICAL to gen-emoji-font.py: autocrop transparent margin, bottom-align to
# the text baseline, emit [lo, hi, alpha] per pixel (LV_COLOR_16_SWAP=0).
PAD_BOTTOM = 0
img = img.convert("RGBA")
bbox = img.split()[3].getbbox()
if bbox:
img = img.crop(bbox)
w, h = img.size
avail_h = PX - PAD_BOTTOM
scale = min(PX / w, avail_h / h)
nw, nh = max(1, round(w * scale)), max(1, round(h * scale))
content = img.resize((nw, nh), Image.LANCZOS)
canvas = Image.new("RGBA", (PX, PX), (0, 0, 0, 0))
canvas.paste(content, ((PX - nw) // 2, PX - PAD_BOTTOM - nh))
out = bytearray()
for (r, g, b, a) in canvas.getdata():
v = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
out += bytes((v & 0xFF, (v >> 8) & 0xFF, a))
return out
# ---- bake the new glyphs ----
new_defs = [] # (cp, ename, bytes)
new_entries = [] # (cp, ref)
for cp, stem in SINGLE:
fn = fetch(stem)
if not fn:
sys.exit("FAILED to fetch U+{:X} ({})".format(cp, stem))
new_defs.append((cp, "e_{:x}".format(cp), to_rgb565a8(Image.open(fn))))
new_entries.append((cp, "d_{:x}".format(cp)))
for key_cp, stem, zeros in SEQ:
fn = fetch(stem)
if not fn:
sys.exit("FAILED to fetch combined {} for U+{:X}".format(stem, key_cp))
new_defs.append((key_cp, "e_{:x}".format(key_cp), to_rgb565a8(Image.open(fn))))
new_entries.append((key_cp, "d_{:x}".format(key_cp)))
for z in zeros:
new_entries.append((z, "d_zero"))
for lead_cp, trail_cp, iso in FLAGS:
fn = fetch_flag(iso)
if not fn:
sys.exit("FAILED to fetch flag {} for U+{:X}".format(iso, lead_cp))
new_defs.append((lead_cp, "e_{:x}".format(lead_cp), to_rgb565a8(Image.open(fn))))
new_entries.append((lead_cp, "d_{:x}".format(lead_cp)))
new_entries.append((trail_cp, "d_zero"))
# ---- splice into emoji_data.c ----
src = open(OUTC).read()
TYPE_MARK = "\ntypedef struct { uint32_t cp;"
FUNC_MARK = "const lv_img_dsc_t* emojiGlyphLookup(uint32_t cp) {"
head = src[:src.index(TYPE_MARK)] # header + includes + all glyph defs
func = src[src.index(FUNC_MARK):] # the binary-search getter (verbatim)
# Idempotency: on a RE-RUN, `head` still holds the e_/d_ glyph defs spliced in by a
# PRIOR run (they live before the typedef), so blindly re-appending new_defs would
# redefine them ("redefinition of e_26bd"). Strip any def for a cp we're about to
# re-bake; the lookup table dedups separately (bycp), so this only touches defs.
for _cp, _ename, _data in new_defs:
_hx = "{:x}".format(_cp)
head = re.sub(r"static const uint8_t e_" + _hx + r"\[\] = [^;]*;\n", "", head)
head = re.sub(r"static const lv_img_dsc_t d_" + _hx + r" = [^;]*;\n", "", head)
# Existing lookup entries (includes the FE0F/FE0E/200D -> d_zero rows).
existing = [(int(cp, 16), ref) for cp, ref in
re.findall(r"\{\s*(0x[0-9A-Fa-f]+)u,\s*&(\w+)\s*\}", src)]
bycp = {cp: ref for cp, ref in existing}
for cp, ref in new_entries: # new cps win; never demote a real glyph to zero
if not (ref == "d_zero" and bycp.get(cp, "d_zero") != "d_zero"):
bycp[cp] = ref
entries = sorted(bycp.items())
count = sum(1 for _, ref in entries if ref != "d_zero")
# New glyph array + dsc definitions (appended after the existing ones).
defs = []
for cp, ename, data in new_defs:
defs.append("static const uint8_t {}[] = {{{}}};".format(ename, ",".join(str(b) for b in data)))
defs.append("static const lv_img_dsc_t d_{:x} = {{ {{ LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, {}, {} }}, sizeof({}), {} }};"
.format(cp, PX, PX, ename, ename))
out = []
out.append(head.rstrip("\n"))
out.append("")
out.append("\n".join(defs))
out.append("")
out.append("typedef struct { uint32_t cp; const lv_img_dsc_t* dsc; } EmojiGlyph;")
out.append("static const EmojiGlyph kGlyphs[] = {")
for cp, ref in entries:
out.append(" {{ 0x{:X}u, &{} }},".format(cp, ref))
out.append("};")
out.append("const uint16_t kEmojiGlyphCount = {};".format(count))
out.append("")
out.append(func.rstrip("\n"))
out.append("")
open(OUTC, "w").write("\n".join(out))
# ---- header comment count ----
h = open(OUTH).read()
h = re.sub(r"// \d+ Noto colour emoji", "// {} Noto colour emoji".format(count), h, count=1)
open(OUTH, "w").write(h)
print("added {} glyphs -> {} total ({} lookup rows incl zero-width)".format(
len(new_defs), count, len(entries)))
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
# Generate src/ui-touch/qr_icon.h: a small, recolour-able "QR code" glyph baked to
# an LVGL TRUE_COLOR_ALPHA lv_img_dsc (white opaque modules, transparent ground —
# tinted to the button text colour at runtime via img_recolor). There is no QR
# glyph in the Montserrat/symbol font, so the Chats-header Share button draws this
# instead of the old stand-in LV_SYMBOL_IMAGE ("picture") icon.
#
# python3 scripts/build/gen-qr-icon.py
#
# Same pixel encoding as the emoji bake: [lo, hi, alpha], LV_COLOR_16_SWAP=0.
import os
SCALE = 2 # px per module -> 9 modules * 2 = 18x18 px
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
OUT = os.path.join(ROOT, "src", "ui-touch", "qr_icon.h")
# 9x9 module grid. Three finder patterns (ring: 111/101/111) at TL/TR/BL corners,
# scattered data modules + a small alignment block bottom-right so it reads as a QR.
P = [
[1,1,1,0,1,0,1,1,1],
[1,0,1,0,0,0,1,0,1],
[1,1,1,0,1,0,1,1,1],
[0,0,0,0,0,0,0,0,0],
[1,0,1,0,1,1,0,1,0],
[0,0,0,0,1,1,0,0,1],
[1,1,1,0,0,0,1,0,1],
[1,0,1,0,1,0,0,1,0],
[1,1,1,0,0,1,1,0,1],
]
MODS = len(P)
W = H = MODS * SCALE
data = bytearray()
for y in range(H):
for x in range(W):
on = P[y // SCALE][x // SCALE]
if on:
data += bytes((0xFF, 0xFF, 0xFF)) # white RGB565 (lo,hi) + alpha 255
else:
data += bytes((0x00, 0x00, 0x00)) # transparent
with open(OUT, "w") as f:
f.write("// AUTO-GENERATED by scripts/build/gen-qr-icon.py — do not edit.\n")
f.write("// %dx%d recolour-able QR glyph for the Chats-header Share button.\n#pragma once\n" % (W, H))
f.write('#include "lvgl.h"\n\n')
f.write("static const uint8_t qr_icon_map[] = {%s};\n" % ",".join(str(b) for b in data))
f.write("static const lv_img_dsc_t qr_icon_dsc = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, %d, %d }, sizeof(qr_icon_map), qr_icon_map };\n"
% (W, H))
print("wrote %s (%dx%d, %d bytes)" % (OUT, W, H, len(data)))
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
#
# Publish the Tanmatsu build of WadaMesh to the Nicolai-Electronics app-repository
# (the official Tanmatsu launcher app-store). Run this as part of cutting a beta.
#
# scripts/tanmatsu-appstore-publish.sh beta_17 # -> version 1.0.17, revision 17
# scripts/tanmatsu-appstore-publish.sh 1.2.0 8 # -> explicit version 1.2.0, revision 8
# scripts/tanmatsu-appstore-publish.sh beta_17 --skip-build # use the existing build
#
# What it does:
# 1. builds the Tanmatsu app (unless --skip-build) and verifies it
# 2. syncs our fork (B0N3-GD/app-repository) to upstream main
# 3. (re)stages deploy/tanmatsu-app/com.wadamesh.wadamesh/ + the fresh binary,
# setting `version` and every application `revision`
# 4. pushes an `update-wadamesh-<version>` branch and opens/updates a PR to
# Nicolai-Electronics/app-repository
#
# NB:
# - WadaMesh on the Tanmatsu is KEYBOARD-driven (no touchscreen). Keep the
# listing wording free of "touch" — the canonical text lives in the template.
# - For an update you MUST bump BOTH `version` AND `revision` or the launcher
# won't offer it. This script bumps both.
# - The CLA is signed once per GitHub user (Kaj), so follow-up PRs auto-pass.
# - First run can't sync until PR #55 (the initial add) is merged; until then
# the fork's main lacks the folder, but step 3 re-stages it in full anyway.
set -euo pipefail
# ---- args ---------------------------------------------------------------
ARG="${1:?usage: tanmatsu-appstore-publish.sh <beta_N | version> [revision] [--skip-build]}"
shift || true
SKIP_BUILD=0; REV_ARG=""
for a in "$@"; do
case "$a" in
--skip-build) SKIP_BUILD=1 ;;
*) REV_ARG="$a" ;;
esac
done
if [[ "$ARG" =~ ^beta_([0-9]+)$ ]]; then
N="${BASH_REMATCH[1]}"; VERSION="1.0.$N"; REVISION="${REV_ARG:-$N}"
else
VERSION="$ARG"; REVISION="${REV_ARG:?explicit version needs a revision: tanmatsu-appstore-publish.sh <version> <revision>}"
fi
# ---- paths --------------------------------------------------------------
SLUG=com.wadamesh.wadamesh
WADAMESH="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # repo root
TPL="$WADAMESH/deploy/tanmatsu-app/$SLUG" # versioned template (icons/README/metadata)
BIN="$WADAMESH/tanmatsu/build/tanmatsu/application.bin" # IDF build output
FORK_DIR="${WADAMESH_APPSTORE_FORK:-/Users/kaj/wadamesh-appstore-fork}"
UPSTREAM=Nicolai-Electronics/app-repository
GHUSER="$(gh api user -q .login)"
echo "==> WadaMesh app-store publish: version $VERSION, revision $REVISION"
# ---- 1. build + verify the Tanmatsu app --------------------------------
if [ "$SKIP_BUILD" -eq 0 ]; then
echo "==> building Tanmatsu app (./build.sh build)..."
LOG="$(mktemp)"
( cd "$WADAMESH/tanmatsu" && ./build.sh build ) >"$LOG" 2>&1 || true # app_check_size exit!=0 is EXPECTED
# real compile/link errors abort; the cosmetic 'app partitions too small' does not
if grep -E "error:|undefined reference|was not declared" "$LOG" | grep -vE "app_check_size|too small|partitions are too small"; then
echo "!! real build error above — aborting"; rm -f "$LOG"; exit 1
fi
rm -f "$LOG"
fi
[ -f "$BIN" ] || { echo "!! no Tanmatsu binary at $BIN — build first (drop --skip-build)"; exit 1; }
echo "==> binary present: $(wc -c <"$BIN") bytes"
# ---- 2. sync the fork to upstream main ---------------------------------
[ -d "$FORK_DIR/.git" ] || { echo "!! fork clone missing at $FORK_DIR — clone $GHUSER/app-repository there first"; exit 1; }
git -C "$FORK_DIR" remote get-url upstream >/dev/null 2>&1 || \
git -C "$FORK_DIR" remote add upstream "https://github.com/$UPSTREAM.git"
git -C "$FORK_DIR" fetch -q upstream
git -C "$FORK_DIR" checkout -q main 2>/dev/null || git -C "$FORK_DIR" checkout -qb main upstream/main
git -C "$FORK_DIR" reset -q --hard upstream/main
# ---- 3. stage the app folder (template + fresh binary; set version/rev) -
APP="$FORK_DIR/$SLUG"
mkdir -p "$APP"
cp "$TPL/icon16.png" "$TPL/icon32.png" "$TPL/icon64.png" "$TPL/README.md" "$APP/"
cp "$WADAMESH/LICENSE" "$APP/LICENSE"
cp "$WADAMESH/NOTICE" "$APP/NOTICE" # MeshCore (MIT) + other third-party attribution
cp "$BIN" "$APP/wadamesh.bin"
python3 - "$TPL/metadata.json" "$APP/metadata.json" "$VERSION" "$REVISION" <<'PY'
import json, sys, re
src, dst, ver, rev = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4])
m = json.load(open(src))
# The launcher store's verify_metadata check restricts name/description to this set
# (NO commas, NO em-dash). Fail loudly HERE instead of as a red check on the PR.
pat = r'^[A-Za-z0-9_\- .!@#$%^&*()\/\\]+$'
for f in ("name", "description"):
v = m.get(f, "")
if not re.match(pat, v) or len(v) > 256:
sys.stderr.write(f"!! metadata '{f}' fails the store regex (no comma / em-dash, <=256 chars): {v!r}\n")
sys.exit(1)
m["version"] = ver
for app in m["application"]:
app["revision"] = rev
json.dump(m, open(dst, "w"), indent=4); open(dst, "a").write("\n")
PY
echo "==> staged $SLUG (version $VERSION, revision $REVISION)"
# ---- 4. commit on a branch, push, open/update the PR -------------------
BR="update-wadamesh-$VERSION"
git -C "$FORK_DIR" checkout -qB "$BR"
git -C "$FORK_DIR" add "$SLUG"
GIT_COMMITTER_NAME="Kaj Schittecat" GIT_COMMITTER_EMAIL="kaj@schittecat.com" \
git -C "$FORK_DIR" commit -q --author="Kaj Schittecat <kaj@schittecat.com>" \
-m "Update WadaMesh to $VERSION (revision $REVISION)" || echo " (nothing new to commit)"
git -C "$FORK_DIR" push -fq -u origin "$BR"
if gh pr create --repo "$UPSTREAM" --base main --head "$GHUSER:$BR" \
--title "Update WadaMesh to $VERSION" \
--body "Updates WadaMesh ($SLUG) to v$VERSION (revision $REVISION) — new Tanmatsu build." 2>/dev/null; then
echo "==> opened PR for $VERSION"
else
echo "==> PR already open for $BR (force-push updated it)"
fi
echo "==> done. Track it: gh pr list --repo $UPSTREAM --author $GHUSER"
+487
View File
@@ -0,0 +1,487 @@
#!/usr/bin/env python3
import argparse
import struct
import sys
import time
try:
import serial
except ImportError:
print("Missing dependency: pyserial", file=sys.stderr)
print("Install with: python -m pip install pyserial", file=sys.stderr)
sys.exit(2)
CMD_APP_START = 1
CMD_SEND_TXT_MSG = 2
CMD_SYNC_NEXT_MESSAGE = 10
CMD_GET_BATT_AND_STORAGE = 20
CMD_DEVICE_QUERY = 22
CMD_SEND_TELEMETRY_REQ = 39
CMD_GET_CUSTOM_VARS = 40
CMD_GET_STATS = 56
RESP_CODE_SELF_INFO = 5
RESP_CODE_SENT = 6
RESP_CODE_CONTACT_MSG_RECV_V3 = 16
RESP_CODE_NO_MORE_MESSAGES = 10
RESP_CODE_BATT_AND_STORAGE = 12
RESP_CODE_DEVICE_INFO = 13
RESP_CODE_CUSTOM_VARS = 21
RESP_CODE_STATS = 24
PUSH_CODE_SEND_CONFIRMED = 0x82
PUSH_CODE_TELEMETRY_RESPONSE = 0x8B
PUSH_CODE_BINARY_RESPONSE = 0x8C
TXT_TYPE_CLI_DATA = 1
STATS_TYPE_CORE = 0
STATS_TYPE_RADIO = 1
STATS_TYPE_PACKETS = 2
MESHCOM_PREFIX = b"MESHCM"
def build_frame(payload: bytes) -> bytes:
return b"<" + struct.pack("<H", len(payload)) + payload
def read_frame(ser: serial.Serial, timeout_s: float = 2.0):
deadline = time.time() + timeout_s
while time.time() < deadline:
b = ser.read(1)
if not b:
continue
if b != b">":
continue
hdr = ser.read(2)
if len(hdr) != 2:
return None
length = struct.unpack("<H", hdr)[0]
payload = ser.read(length)
if len(payload) != length:
return None
return payload
return None
def send_and_collect(ser: serial.Serial, name: str, payload: bytes, listen_s: float = 1.0):
print(f"\n== {name} ==")
print("TX:", payload.hex(" "))
ser.write(build_frame(payload))
ser.flush()
frames = []
deadline = time.time() + listen_s
while time.time() < deadline:
frame = read_frame(ser, timeout_s=min(0.35, max(0.05, deadline - time.time())))
if frame is None:
continue
frames.append(frame)
if not frames:
print("RX: <timeout>")
return []
for idx, frame in enumerate(frames, start=1):
print(f"RX[{idx}]:", frame.hex(" "))
decode_frame(frame)
return frames
def decode_frame(frame: bytes):
code = frame[0]
print(f"Code: 0x{code:02X} ({code})")
if code == RESP_CODE_DEVICE_INFO:
decode_device_info(frame)
elif code == RESP_CODE_BATT_AND_STORAGE:
decode_batt_and_storage(frame)
elif code == RESP_CODE_SELF_INFO:
print("Self info response received")
elif code == RESP_CODE_CUSTOM_VARS:
decode_custom_vars(frame)
elif code == RESP_CODE_STATS:
decode_stats(frame)
elif code == RESP_CODE_CONTACT_MSG_RECV_V3:
decode_contact_msg_recv_v3(frame)
elif code == RESP_CODE_NO_MORE_MESSAGES:
print("No more queued messages")
elif code == RESP_CODE_SENT:
decode_sent(frame)
elif code == PUSH_CODE_SEND_CONFIRMED:
decode_send_confirmed(frame)
elif code == PUSH_CODE_TELEMETRY_RESPONSE:
decode_telemetry(frame)
elif code == PUSH_CODE_BINARY_RESPONSE:
decode_binary_response(frame)
else:
print("Unhandled response")
def decode_c_string(raw: bytes) -> str:
return raw.split(b"\x00", 1)[0].decode("utf-8", errors="replace")
def decode_device_info(frame: bytes):
if len(frame) < 80:
print("Short DEVICE_INFO frame")
return
fw_ver_code = frame[1]
max_contacts_x2 = frame[2]
max_channels = frame[3]
ble_pin = struct.unpack("<I", frame[4:8])[0]
build_date = decode_c_string(frame[8:20])
manufacturer = decode_c_string(frame[20:60])
version = decode_c_string(frame[60:80]) if len(frame) >= 80 else ""
print("Firmware code:", fw_ver_code)
print("Max contacts:", max_contacts_x2 * 2)
print("Max channels:", max_channels)
print("BLE pin:", ble_pin)
print("Build date:", build_date)
print("Manufacturer:", manufacturer)
if version:
print("Version:", version)
def decode_batt_and_storage(frame: bytes):
if len(frame) < 11:
print("Short BATT_AND_STORAGE frame")
return
battery_mv = struct.unpack("<H", frame[1:3])[0]
used_kb = struct.unpack("<I", frame[3:7])[0]
total_kb = struct.unpack("<I", frame[7:11])[0]
print("Battery:", f"{battery_mv} mV ({battery_mv / 1000.0:.2f} V)")
print("Storage used:", f"{used_kb} KB")
print("Storage total:", f"{total_kb} KB")
def decode_custom_vars(frame: bytes):
raw = frame[1:].decode("utf-8", errors="replace")
print("Custom vars:", raw if raw else "<empty>")
def decode_stats(frame: bytes):
if len(frame) < 2:
print("Short STATS frame")
return
stats_type = frame[1]
if stats_type == STATS_TYPE_CORE and len(frame) >= 11:
battery_mv = struct.unpack("<H", frame[2:4])[0]
uptime_secs = struct.unpack("<I", frame[4:8])[0]
err_flags = struct.unpack("<H", frame[8:10])[0]
queue_len = frame[10]
print("Stats type: core")
print("Battery:", f"{battery_mv} mV ({battery_mv / 1000.0:.2f} V)")
print("Uptime:", f"{uptime_secs} s")
print("Error flags:", f"0x{err_flags:04X}")
print("Outbound queue:", queue_len)
elif stats_type == STATS_TYPE_RADIO and len(frame) >= 14:
noise_floor = struct.unpack("<h", frame[2:4])[0]
last_rssi = struct.unpack("<b", frame[4:5])[0]
last_snr_q4 = struct.unpack("<b", frame[5:6])[0]
tx_air_secs = struct.unpack("<I", frame[6:10])[0]
rx_air_secs = struct.unpack("<I", frame[10:14])[0]
print("Stats type: radio")
print("Noise floor:", f"{noise_floor} dBm")
print("Last RSSI:", f"{last_rssi} dBm")
print("Last SNR:", f"{last_snr_q4 / 4.0:.2f} dB")
print("TX airtime:", f"{tx_air_secs} s")
print("RX airtime:", f"{rx_air_secs} s")
elif stats_type == STATS_TYPE_PACKETS and len(frame) >= 30:
values = struct.unpack("<IIIIIII", frame[2:30])
labels = [
"recv",
"sent",
"sent_flood",
"sent_direct",
"recv_flood",
"recv_direct",
"recv_errors",
]
print("Stats type: packets")
for label, value in zip(labels, values):
print(f"{label}:", value)
else:
print(f"Unknown stats subtype {stats_type} or short frame")
def decode_sent(frame: bytes):
if len(frame) < 10:
print("Short SENT frame")
return
flood = frame[1]
tag = struct.unpack("<I", frame[2:6])[0]
est_timeout = struct.unpack("<I", frame[6:10])[0]
print("Message accepted")
print("Flood:", flood)
print("Tag:", tag)
print("Estimated timeout:", est_timeout)
def decode_send_confirmed(frame: bytes):
if len(frame) < 9:
print("Short SEND_CONFIRMED frame")
return
ack = struct.unpack("<I", frame[1:5])[0]
trip_time = struct.unpack("<I", frame[5:9])[0]
print("Send confirmed")
print("Ack/tag:", ack)
print("Trip time:", trip_time)
def decode_binary_response(frame: bytes):
if len(frame) < 6:
print("Short BINARY_RESPONSE frame")
return
reserved = frame[1]
tag = struct.unpack("<I", frame[2:6])[0]
text = frame[6:].decode("utf-8", errors="replace").rstrip("\x00")
print("Binary response reserved:", reserved)
print("Binary response tag:", tag)
print("Binary response text:")
print(text if text else "<empty>")
def decode_contact_msg_recv_v3(frame: bytes):
if len(frame) < 16:
print("Short CONTACT_MSG_RECV_V3 frame")
return
snr_q4 = struct.unpack("<b", frame[1:2])[0]
reserved1 = frame[2]
reserved2 = frame[3]
pubkey_prefix = frame[4:10].hex()
path_len = frame[10]
txt_type = frame[11]
timestamp = struct.unpack("<I", frame[12:16])[0]
text = frame[16:].decode("utf-8", errors="replace").rstrip("\x00")
print("Queued contact message")
print("SNR:", f"{snr_q4 / 4.0:.2f} dB")
print("Reserved:", reserved1, reserved2)
print("From pubkey prefix:", pubkey_prefix)
print("Path len:", path_len)
print("Text type:", txt_type)
print("Timestamp:", timestamp)
print("Text:")
print(text if text else "<empty>")
def _read_i16_be(buf: bytes) -> int:
return struct.unpack(">h", buf)[0]
def _read_u16_be(buf: bytes) -> int:
return struct.unpack(">H", buf)[0]
def _read_u32_be(buf: bytes) -> int:
return struct.unpack(">I", buf)[0]
def _read_i24_be(buf: bytes) -> int:
val = int.from_bytes(buf, "big", signed=False)
if val & 0x800000:
val -= 1 << 24
return val
def parse_lpp(payload: bytes):
i = 0
items = []
while i + 2 <= len(payload):
channel = payload[i]
data_type = payload[i + 1]
i += 2
if data_type in (0, 1, 102, 120, 142):
if i + 1 > len(payload):
break
value = payload[i]
i += 1
items.append((channel, data_type, value))
elif data_type in (2, 3):
if i + 2 > len(payload):
break
value = _read_i16_be(payload[i:i + 2]) / 100.0
i += 2
items.append((channel, data_type, value))
elif data_type in (101, 115, 125, 128, 132):
if i + 2 > len(payload):
break
value = _read_u16_be(payload[i:i + 2])
i += 2
if data_type == 115:
value /= 10.0
items.append((channel, data_type, value))
elif data_type == 121:
if i + 2 > len(payload):
break
value = _read_i16_be(payload[i:i + 2])
i += 2
items.append((channel, data_type, value))
elif data_type == 103:
if i + 2 > len(payload):
break
value = _read_i16_be(payload[i:i + 2]) / 10.0
i += 2
items.append((channel, data_type, value))
elif data_type == 104:
if i + 1 > len(payload):
break
value = payload[i] / 2.0
i += 1
items.append((channel, data_type, value))
elif data_type in (116,):
if i + 2 > len(payload):
break
value = _read_u16_be(payload[i:i + 2]) / 100.0
i += 2
items.append((channel, data_type, value))
elif data_type in (117,):
if i + 2 > len(payload):
break
value = _read_u16_be(payload[i:i + 2]) / 1000.0
i += 2
items.append((channel, data_type, value))
elif data_type in (100, 118, 131, 133):
if i + 4 > len(payload):
break
value = _read_u32_be(payload[i:i + 4])
i += 4
if data_type == 131:
value /= 1000.0
items.append((channel, data_type, value))
elif data_type == 130:
if i + 4 > len(payload):
break
value = _read_u32_be(payload[i:i + 4]) / 1000.0
i += 4
items.append((channel, data_type, value))
elif data_type in (113, 134):
if i + 6 > len(payload):
break
x = _read_i16_be(payload[i:i + 2])
y = _read_i16_be(payload[i + 2:i + 4])
z = _read_i16_be(payload[i + 4:i + 6])
i += 6
scale = 1000.0 if data_type == 113 else 100.0
items.append((channel, data_type, (x / scale, y / scale, z / scale)))
elif data_type == 136:
if i + 9 > len(payload):
break
lat = _read_i24_be(payload[i:i + 3]) / 10000.0
lon = _read_i24_be(payload[i + 3:i + 6]) / 10000.0
alt = _read_i24_be(payload[i + 6:i + 9]) / 100.0
i += 9
items.append((channel, data_type, (lat, lon, alt)))
else:
items.append((channel, data_type, "<unknown>"))
break
return items
def format_lpp_item(channel: int, data_type: int, value):
names = {
0: "digital_input",
1: "digital_output",
2: "analog_input",
3: "analog_output",
100: "generic_sensor",
101: "luminosity",
102: "presence",
103: "temperature_c",
104: "humidity_pct",
113: "accelerometer_g",
115: "barometric_hpa",
116: "voltage_v",
117: "current_a",
118: "frequency_hz",
120: "percentage",
121: "altitude_m",
125: "concentration_ppm",
128: "power_w",
130: "distance_m",
131: "energy_kwh",
132: "direction_deg",
133: "unixtime",
134: "gyrometer_dps",
136: "gps",
142: "switch",
}
return f"ch{channel} {names.get(data_type, f'type_{data_type}')} = {value}"
def drain_sync_messages(ser: serial.Serial, max_reads: int = 8):
for idx in range(max_reads):
frames = send_and_collect(
ser,
f"CMD_SYNC_NEXT_MESSAGE #{idx + 1}",
bytes([CMD_SYNC_NEXT_MESSAGE]),
listen_s=0.7,
)
if not frames:
break
if any(frame[:1] == bytes([RESP_CODE_NO_MORE_MESSAGES]) for frame in frames):
break
def decode_telemetry(frame: bytes):
if len(frame) < 8:
print("Short TELEMETRY_RESPONSE frame")
return
reserved = frame[1]
pubkey_prefix = frame[2:8].hex()
lpp = frame[8:]
print("Telemetry reserved:", reserved)
print("Telemetry pubkey prefix:", pubkey_prefix)
print("Telemetry raw LPP:", lpp.hex(" "))
items = parse_lpp(lpp)
if not items:
print("Telemetry parsed: <empty>")
return
print("Telemetry parsed:")
for channel, data_type, value in items:
print(format_lpp_item(channel, data_type, value))
def build_meshcomod_cli(command: str) -> bytes:
return (
bytes([CMD_SEND_TXT_MSG, TXT_TYPE_CLI_DATA, 0])
+ struct.pack("<I", 0)
+ MESHCOM_PREFIX
+ command.encode("utf-8")
)
def main():
parser = argparse.ArgumentParser(description="Probe WADAMESH companion protocol over serial")
parser.add_argument("--port", required=True, help="Serial port, e.g. COM4")
parser.add_argument("--baud", type=int, default=115200, help="Serial baud rate")
args = parser.parse_args()
with serial.Serial(args.port, args.baud, timeout=0.25) as ser:
time.sleep(0.2)
try:
ser.reset_input_buffer()
ser.reset_output_buffer()
except Exception:
pass
device_query = bytes([CMD_DEVICE_QUERY, 3])
app_start = bytes([CMD_APP_START]) + bytes(7) + b"wadamesh-probe"
send_and_collect(ser, "CMD_DEVICE_QUERY", device_query, listen_s=0.8)
send_and_collect(ser, "CMD_APP_START", app_start, listen_s=0.8)
drain_sync_messages(ser, max_reads=4)
send_and_collect(ser, "CMD_GET_BATT_AND_STORAGE", bytes([CMD_GET_BATT_AND_STORAGE]), listen_s=0.8)
send_and_collect(ser, "CMD_GET_CUSTOM_VARS", bytes([CMD_GET_CUSTOM_VARS]), listen_s=0.8)
send_and_collect(ser, "CMD_GET_STATS core", bytes([CMD_GET_STATS, STATS_TYPE_CORE]), listen_s=0.8)
send_and_collect(ser, "CMD_GET_STATS radio", bytes([CMD_GET_STATS, STATS_TYPE_RADIO]), listen_s=0.8)
send_and_collect(ser, "CMD_GET_STATS packets", bytes([CMD_GET_STATS, STATS_TYPE_PACKETS]), listen_s=0.8)
send_and_collect(ser, "CMD_SEND_TELEMETRY_REQ self", bytes([CMD_SEND_TELEMETRY_REQ, 0, 0, 0]), listen_s=1.2)
send_and_collect(ser, "CMD_SEND_TXT_MSG meshcomod status", build_meshcomod_cli("status"), listen_s=1.5)
drain_sync_messages(ser, max_reads=4)
send_and_collect(ser, "CMD_SEND_TXT_MSG meshcomod wifi status", build_meshcomod_cli("wifi status"), listen_s=1.5)
drain_sync_messages(ser, max_reads=4)
send_and_collect(ser, "CMD_SEND_TXT_MSG meshcomod ota status", build_meshcomod_cli("ota status"), listen_s=1.5)
drain_sync_messages(ser, max_reads=4)
if __name__ == "__main__":
main()
+6
View File
@@ -94,6 +94,12 @@ public:
uint32_t ack_hash) {
(void)to_pub; (void)to_name; (void)text; (void)ack_hash;
}
/** Mirror an app-originated CHANNEL message into the on-device UI — the channel-send path, like the
* DM path above, never shows companion-originated sends on screen otherwise. text is NUL-terminated
* by the caller. Default no-op for non-touch UIs. */
virtual void appSentMsgToChannel(const char* channel_name, const char* text) {
(void)channel_name; (void)text;
}
virtual void notify(UIEventType t = UIEventType::none) = 0;
virtual void appendDiag(const char* message) { (void)message; }
/** Notify UI of an advert reception. `is_new=true` means the contact is NOT in
+7 -1
View File
@@ -2201,7 +2201,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
// Diagnostic (room-server login trace): the login response we matched +
// whether we accepted it. resp byte: 0x4F 'O' = legacy "OK"; otherwise a
// RESP_SERVER_LOGIN_OK / fail code. Pair with "[ROOM] login send".
Serial.printf("[ROOM] login resp '%s' resp=0x%02x ok=%d perms=%d\n",
WIRE_DBG("[ROOM] login resp '%s' resp=0x%02x ok=%d perms=%d\n",
contact.name, data[4], (int)ok, (int)perms);
#ifdef DISPLAY_CLASS
// Notify the touch UI so the admin console can flip from "logging in…"
@@ -2880,6 +2880,12 @@ void MyMesh::handleCmdFrame(size_t len) {
bool success = getChannel(channel_idx, channel);
if (success && sendGroupMessage(msg_timestamp, channel.channel, _prefs.node_name, text, len - i)) {
writeOKFrame();
#if defined(HAS_TANMATSU)
// Mirror the app-sent channel message into the on-device touch UI — the channel-send path
// otherwise never shows companion-originated channel sends on screen (the DM path does, via
// appSentMsgToContact). NUL-terminate the text in-place first, exactly as the DM path does.
if (_ui) { cmd_frame[len] = 0; _ui->appSentMsgToChannel(channel.name, text); }
#endif
// Sent channel messages are not added to shared history / broadcast: channel_idx and
// frame format are device-specific and clients (e.g. HA) without channel support can
// misparse or show "text as sender"; also avoids failed-to-sync when versions differ.
+19 -1
View File
@@ -6,6 +6,20 @@
#include <helpers/MeshTouchTxTrace.h>
#include "AbstractUITask.h"
// Operational wire-debug. The companion link is a BINARY protocol over Serial (USB-CDC); writing raw
// text to Serial during a session interleaves into that stream and corrupts it — the MeshCore app
// then throws "Bad state: Streamsink is bound to a stream" and the connect / room login / repeater
// admin all fail (GH #25, #54, #23). Compiled out in release; build -DCOMPANION_WIRE_DEBUG=1 to get
// these traces back (only safe over a non-companion debug console).
#ifndef COMPANION_WIRE_DEBUG
#define COMPANION_WIRE_DEBUG 0
#endif
#if COMPANION_WIRE_DEBUG
#define WIRE_DBG(...) Serial.printf(__VA_ARGS__)
#else
#define WIRE_DBG(...) do {} while (0)
#endif
/*------------ Frame Protocol --------------*/
// Keep the fork's companion-protocol lineage (27 — a superset of upstream's 13)
// so the existing meshcomod web/phone client still matches. 1.16's new companion
@@ -328,7 +342,7 @@ public:
// Diagnostic (room-server login trace): which contact/type we sent the login
// to and the send result. Pair with the "[ROOM] login resp" line in
// onContactResponse to see exactly where a room login breaks.
Serial.printf("[ROOM] login send '%s' type=%u pwlen=%u sync_since=%lu -> r=%d\n",
WIRE_DBG("[ROOM] login send '%s' type=%u pwlen=%u sync_since=%lu -> r=%d\n",
recipient.name, (unsigned)recipient.type,
(unsigned)(password ? strlen(password) : 0),
(unsigned long)recipient.sync_since, r);
@@ -937,4 +951,8 @@ private:
uint32_t _ui_trace_ping_tag = 0;
};
#if defined(ESP32_PLATFORM)
extern MyMesh& the_mesh; // PSRAM-resident (placement-new'd in main.cpp); reference keeps call sites unchanged
#else
extern MyMesh the_mesh;
#endif
+50 -1
View File
@@ -35,7 +35,7 @@ static bool s_begun = false;
// short read (→ treat as absent → defaults); `ver` lets later builds add fields.
static const char* KEY_CFG = "cfg";
static const uint16_t TOUCH_CFG_MAGIC = 0x5743; // 'WC' (WadaCfg)
static const uint8_t TOUCH_CFG_VER = 18; // v2 sig_probe/poll; v3 tz_zone; v4 hide_node_name; v5 map_night/map_zoom; v6 map text/marker visibility; v7 app_grid_large; v8 ui_scale; v9 tb_keypad; v10 sleep_idle; v11 nav_keys; v12 map_zoom_buttons; v13 nav_dir_keys; v14 home_is_drawer; v15 kbd_nav default ON (one-time migrate); v16 nav_scroll_keys; v17 notify_new_contact; v18 kbd_nav OFF by default (reverses v15; T-Deck/V4 only, Tanmatsu stays on)
static const uint8_t TOUCH_CFG_VER = 21; // v2 sig_probe/poll; v3 tz_zone; v4 hide_node_name; v5 map_night/map_zoom; v6 map text/marker visibility; v7 app_grid_large; v8 ui_scale; v9 tb_keypad; v10 sleep_idle; v11 nav_keys; v12 map_zoom_buttons; v13 nav_dir_keys; v14 home_is_drawer; v15 kbd_nav default ON (one-time migrate); v16 nav_scroll_keys; v17 notify_new_contact; v18 kbd_nav OFF by default (reverses v15; T-Deck/V4 only, Tanmatsu stays on); v19 show_sensors_tab; v20 map_show_links; v21 map_style (0=OSM default, 1=OpenTopoMap)
// Defaults (kept identical to the historical per-key defaults).
static const uint16_t DEFAULT_SCREEN_TIMEOUT_S = 20;
@@ -89,6 +89,9 @@ struct __attribute__((packed)) TouchCfg {
uint8_t home_is_drawer; // Home tab defaults to the app drawer (1) vs the Commander screen (0, default) — v14 (trailing)
uint8_t nav_scroll_keys[2]; // keyboard-nav scroll keys (ASCII): scroll-up, scroll-down — v16 (trailing)
uint8_t notify_new_contact;// toast/chip when a contact is auto-discovered (bool) — v17 (trailing)
uint8_t show_sensors_tab; // V4 Expansion Kit: show the Sensors tab + Home env widget (bool, default 1) — v19 (trailing)
uint8_t map_show_links; // show self->contact link lines on the map (bool, default 1) — v20 (trailing)
uint8_t map_style; // map tile style: 0=OpenStreetMap (default), 1=OpenTopoMap — v21 (trailing)
};
static TouchCfg s_cfg;
@@ -166,6 +169,9 @@ static void cfgSetDefaults(TouchCfg& c) {
c.nav_scroll_keys[0] = 'f'; c.nav_scroll_keys[1] = 'c'; // default scroll-up F / scroll-down C
#endif
c.notify_new_contact = 1; // default: show the new-contact toast (preserve prior behaviour)
c.show_sensors_tab = 1; // default: show the V4 Expansion-Kit Sensors tab + Home env widget
c.map_show_links = 1; // default: show self->contact link lines (PR #61)
c.map_style = 0; // default: OpenStreetMap (OpenTopoMap is opt-in)
}
// Persist the whole blob using the same end()/begin(RW)/put/end()/begin(RO)
@@ -756,6 +762,20 @@ bool touchPrefsSetNewContactToast(bool on) {
return cfgFlush();
}
// Heltec V4 Expansion Kit: show the Sensors tab + the Home env widget. Default
// ON. The UI also requires an ENVIRONMENT sensor to actually be present (checked
// at runtime in buildUiTree), so a bare V4 hides the UI even with this on.
bool touchPrefsGetShowSensorsTab() {
if (!s_begun) touchPrefsBegin();
return s_cfg.show_sensors_tab != 0; // default = show the Sensors tab
}
bool touchPrefsSetShowSensorsTab(bool on) {
if (!s_begun) touchPrefsBegin();
s_cfg.show_sensors_tab = on ? 1 : 0;
return cfgFlush();
}
bool touchPrefsGetMapNight() {
if (!s_begun) touchPrefsBegin();
return s_cfg.map_night != 0;
@@ -811,6 +831,24 @@ bool touchPrefsSetMapShowContacts(bool on) {
s_cfg.map_show_contacts = on ? 1 : 0;
return cfgFlush();
}
bool touchPrefsGetMapShowLinks() {
if (!s_begun) touchPrefsBegin();
return s_cfg.map_show_links != 0;
}
bool touchPrefsSetMapShowLinks(bool on) {
if (!s_begun) touchPrefsBegin();
s_cfg.map_show_links = on ? 1 : 0;
return cfgFlush();
}
uint8_t touchPrefsGetMapStyle() {
if (!s_begun) touchPrefsBegin();
return s_cfg.map_style;
}
bool touchPrefsSetMapStyle(uint8_t style) {
if (!s_begun) touchPrefsBegin();
s_cfg.map_style = style;
return cfgFlush();
}
bool touchPrefsGetAppGridLarge() {
if (!s_begun) touchPrefsBegin();
return s_cfg.app_grid_large != 0;
@@ -1255,6 +1293,11 @@ void touchPrefsSetEdgeScroll(bool on) { if (!s_begun) touchPrefsBegin(); pr
bool touchPrefsGetLockOnScreenOff() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("lock_off", 0) != 0; }
void touchPrefsSetLockOnScreenOff(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("lock_off", on ? 1 : 0); }
#if defined(HAS_TANMATSU) // only the Tanmatsu has the message LED — keep S3 (T-Deck/V4) bins unchanged
bool touchPrefsGetMsgLed() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("msg_led", 1) != 0; } // default ON
void touchPrefsSetMsgLed(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("msg_led", on ? 1 : 0); }
#endif
// Generic blob (used to persist the discovered-nodes list across reboots).
size_t touchPrefsGetBlob(const char* key, uint8_t* out, size_t maxlen) {
if (!s_begun) touchPrefsBegin();
@@ -1279,6 +1322,12 @@ uint8_t touchPrefsGetSoundVolume() {
return v > 100 ? 100 : v;
}
void touchPrefsSetSoundVolume(uint8_t vol) { if (vol > 100) vol = 100; if (!s_begun) touchPrefsBegin(); prefsPutUChar("snd_vol", vol); }
uint8_t touchPrefsGetKbdBacklight() {
if (!s_begun) touchPrefsBegin();
uint8_t v = s_prefs.getUChar("kbd_bl", 100);
return v > 100 ? 100 : v;
}
void touchPrefsSetKbdBacklight(uint8_t pct) { if (pct > 100) pct = 100; if (!s_begun) touchPrefsBegin(); prefsPutUChar("kbd_bl", pct); }
// Per-channel mute (name-keyed NVS blob "chm" + tiny RAM cache) --------------
static const char* KEY_CHM = "chm";
+25
View File
@@ -98,6 +98,12 @@ bool touchPrefsGetMapShowTileXYZ();
bool touchPrefsSetMapShowTileXYZ(bool on);
bool touchPrefsGetMapShowContacts();
bool touchPrefsSetMapShowContacts(bool on);
bool touchPrefsGetMapShowLinks();
bool touchPrefsSetMapShowLinks(bool on);
/** Map tile style: 0 = OpenStreetMap (default), 1 = OpenTopoMap (topographic, opt-in). */
uint8_t touchPrefsGetMapStyle();
bool touchPrefsSetMapStyle(uint8_t style);
/* App drawer: large grid (one fewer column → bigger icons + labels, for low vision).
* Default false = the compact grid (T-Deck 4 cols / Heltec V4 3 cols). */
@@ -153,6 +159,13 @@ bool touchPrefsSetHideNodeName(bool hide);
bool touchPrefsGetNewContactToast();
bool touchPrefsSetNewContactToast(bool on);
/** Heltec V4 Expansion Kit only: show the bottom "Sensors" tab + the Home
* environment widget. Default true. Combined with a runtime check for any
* ENVIRONMENT sensor actually being present, so a bare V4 hides the UI even
* with this on. Read in buildUiTree → applies after a restart. */
bool touchPrefsGetShowSensorsTab();
bool touchPrefsSetShowSensorsTab(bool on);
/** Store all device data (identity/prefs/contacts/channels) on the SD card under
* /meshcomod instead of internal SPIFFS. T-Deck only; read at boot before data
* loads, so changing it requires a reboot. Default false (SPIFFS). */
@@ -257,6 +270,11 @@ bool touchPrefsGetSoundMentions(); // default true
void touchPrefsSetSoundMentions(bool on);
uint8_t touchPrefsGetSoundVolume(); // 0..100, default 70
void touchPrefsSetSoundVolume(uint8_t vol);
/** Tanmatsu keyboard backlight brightness, 0–100 %. Default 100. Separate from
* screen brightness (touchPrefsGet/SetBrightness) and volume (…SoundVolume). */
uint8_t touchPrefsGetKbdBacklight();
void touchPrefsSetKbdBacklight(uint8_t pct);
bool touchPrefsGetEnterSends(); // Enter key sends a chat message (default true)
void touchPrefsSetEnterSends(bool on);
bool touchPrefsGetClock12h(); // 12-hour clock (default false = 24h)
@@ -339,6 +357,13 @@ bool touchPrefsSetLockTextColor(uint32_t rgb);
bool touchPrefsGetColorfulBubbles();
bool touchPrefsSetColorfulBubbles(bool on);
/** Tanmatsu message-notification LED: flash the envelope-icon LED on a new message and breathe it
* softly while there are unread messages. Default ON. Tanmatsu-only (no such LED on T-Deck/V4). */
#if defined(HAS_TANMATSU)
bool touchPrefsGetMsgLed();
void touchPrefsSetMsgLed(bool on);
#endif
/** UI accent colour (buttons, active tab, keyboard, highlights) as 0xRRGGBB.
* Default = the stock neutral gray; the picker keeps it dark enough that the
* off-white text stays readable. */
+6 -1
View File
@@ -26,7 +26,12 @@ void tdeckKeyboardBegin() {
}
void tdeckKeyboardSetBacklight(uint8_t level) {
if (level != s_bl_desired) { s_bl_desired = level; s_bl_dirty = true; }
// Force the FIRST write even when the requested level matches our cached default (0). A reflash
// resets the ESP32 but NOT the keyboard's C3 — it keeps its previously-lit backlight — so without
// this the boot "off" request (0 == cached 0) was never sent and the backlight stayed on despite
// the setting reading "Off" (issue #33). After the first write, change-detection resumes.
static bool forced = false;
if (level != s_bl_desired || !forced) { s_bl_desired = level; s_bl_dirty = true; forced = true; }
}
void tdeckKeyboardPoll() {
+36
View File
@@ -1,6 +1,10 @@
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include "MyMesh.h"
#if defined(ESP32_PLATFORM)
#include <new> // placement-new for the PSRAM-resident the_mesh
#include "esp_heap_caps.h" // heap_caps_malloc(MALLOC_CAP_SPIRAM)
#endif
#if defined(ESP32_PLATFORM) && defined(HAS_TOUCH_UI)
#include <Preferences.h>
#include <esp_system.h>
@@ -127,11 +131,31 @@ static uint32_t _atoi(const char* sp) {
StdRNG fast_rng;
SimpleMeshTables tables;
#if defined(ESP32_PLATFORM)
// the_mesh is ~42 KB (dominated by the MAX_CONTACTS ContactInfo array) and was the
// single biggest static internal-DRAM consumer. Place the whole object in PSRAM —
// the contacts array rides along inside it — and bind a reference so every
// `the_mesh.foo()` call site is unchanged. The constructor still runs HERE at
// static-init (PSRAM is already up; the UITask psAlloc statics rely on the same),
// so timing/behaviour are identical to the old direct global — only the address
// moves off internal DRAM. heap_caps falls back to internal RAM if PSRAM is absent.
static MyMesh& makeTheMesh() {
void* mem = heap_caps_malloc(sizeof(MyMesh), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!mem) mem = malloc(sizeof(MyMesh)); // no-PSRAM fallback: behaves as before
return *new (mem) MyMesh(radio_driver, fast_rng, rtc_clock, tables, store
#ifdef DISPLAY_CLASS
, &ui_task
#endif
);
}
MyMesh& the_mesh = makeTheMesh();
#else
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
#ifdef DISPLAY_CLASS
, &ui_task
#endif
);
#endif
/* END GLOBAL OBJECTS */
@@ -151,11 +175,23 @@ void halt() {
unsigned long last_wifi_reconnect_attempt = 0;
#endif
#include "esp_task_wdt.h" // task-watchdog reconfigure — see setup() (GH #56)
void setup() {
Serial.begin(115200);
delay(200);
Serial.println("[BOOT] setup start");
// Widen the task-watchdog grace period. The ~5 s default trips during a legitimate-but-slow flash
// burst — a SPIFFS garbage-collect, or a bulk save (DataStore issues ~12 flash ops per contact,
// thousands for a full address book). A flash op parks BOTH cores with the cache disabled, so both
// IDLE tasks miss their reset and the watchdog panics (decoded coredumps: task_wdt CPU0=IDLE0
// CPU1=IDLE1, core 0 in spi_flash_op_block_func). The burst can't be chunked under the limit easily
// and the per-core WdtHeavyGuard only covers core 0, so give the dog enough headroom to ride the
// burst out while still catching a genuine multi-second hang. (Fixes the random WDT reboots, GH #56.)
esp_task_wdt_init(20, true); // 20 s grace (was ~5 s), keep panic. Re-init reconfigures the
// already-running TWDT + keeps the idle-task subscriptions.
#if defined(ESP32_PLATFORM) && defined(HAS_TOUCH_UI)
// Record which slot we booted from so the recovery's "Boot firmware" can return
// here. Recovery-first itself is enforced by the CUSTOM bootloader (it boots
+251
View File
@@ -221,6 +221,155 @@ static const lv_btnmatrix_ctrl_t kb_en_upper_ctrl[] = {
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
};
/* ---------- French (AZERTY) on-screen keyboard ---------- */
/* Keeps the proven LVGL control rows/punctuation, but swaps the alpha rows to
* a familiar French AZERTY order. Accents still come from the existing popup. */
static const char* kb_fr_lower[] = {
"1#", "a", "z", "e", "r", "t", "y", "u", "i", "o", "p", LV_SYMBOL_BACKSPACE, "\n",
"ABC", "q", "s", "d", "f", "g", "h", "j", "k", "l", "m", LV_SYMBOL_NEW_LINE, "\n",
"_", "-", "w", "x", "c", "v", "b", "n", ".", ",", ":", "", "\n",
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
};
static const lv_btnmatrix_ctrl_t kb_fr_lower_ctrl[] = {
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_HIDDEN,
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
};
static const char* kb_fr_upper[] = {
"1#", "A", "Z", "E", "R", "T", "Y", "U", "I", "O", "P", LV_SYMBOL_BACKSPACE, "\n",
"abc", "Q", "S", "D", "F", "G", "H", "J", "K", "L", "M", LV_SYMBOL_NEW_LINE, "\n",
"_", "-", "W", "X", "C", "V", "B", "N", ".", ",", ":", "", "\n",
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
};
static const lv_btnmatrix_ctrl_t kb_fr_upper_ctrl[] = {
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_HIDDEN,
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
};
/* Dutch largely stays QWERTY, but a dedicated IJ key makes the most common
* digraph directly reachable without forcing users through the accent popup. */
static const char* kb_nl_lower[] = {
"1#", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", LV_SYMBOL_BACKSPACE, "\n",
"ABC", "a", "s", "d", "f", "g", "h", "j", "k", "l", LV_SYMBOL_NEW_LINE, "\n",
"_", "-", "z", "x", "c", "v", "b", "n", "m", ".", ",", "ij", "\n",
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
};
static const lv_btnmatrix_ctrl_t kb_nl_lower_ctrl[] = {
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(2),
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
};
static const char* kb_nl_upper[] = {
"1#", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", LV_SYMBOL_BACKSPACE, "\n",
"abc", "A", "S", "D", "F", "G", "H", "J", "K", "L", LV_SYMBOL_NEW_LINE, "\n",
"_", "-", "Z", "X", "C", "V", "B", "N", "M", ".", ",", "IJ", "\n",
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
};
static const lv_btnmatrix_ctrl_t kb_nl_upper_ctrl[] = {
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(2),
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
};
/* German QWERTZ keeps the proven LVGL control rows and swaps the Y/Z alpha
* positions. Umlauts / eszett still come from the accent popup. */
static const char* kb_de_lower[] = {
"1#", "q", "w", "e", "r", "t", "z", "u", "i", "o", "p", LV_SYMBOL_BACKSPACE, "\n",
"ABC", "a", "s", "d", "f", "g", "h", "j", "k", "l", LV_SYMBOL_NEW_LINE, "\n",
"_", "-", "y", "x", "c", "v", "b", "n", "m", ".", ",", ":", "\n",
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
};
static const lv_btnmatrix_ctrl_t kb_de_lower_ctrl[] = {
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1),
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
};
static const char* kb_de_upper[] = {
"1#", "Q", "W", "E", "R", "T", "Z", "U", "I", "O", "P", LV_SYMBOL_BACKSPACE, "\n",
"abc", "A", "S", "D", "F", "G", "H", "J", "K", "L", LV_SYMBOL_NEW_LINE, "\n",
"_", "-", "Y", "X", "C", "V", "B", "N", "M", ".", ",", ":", "\n",
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
};
static const lv_btnmatrix_ctrl_t kb_de_upper_ctrl[] = {
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1),
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
};
/* Spanish keeps QWERTY but promotes n-tilde directly onto the alpha deck. */
static const char* kb_es_lower[] = {
"1#", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", LV_SYMBOL_BACKSPACE, "\n",
"ABC", "a", "s", "d", "f", "g", "h", "j", "k", "l", "ñ", LV_SYMBOL_NEW_LINE, "\n",
"_", "-", "z", "x", "c", "v", "b", "n", "m", ".", ",", ":", "\n",
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
};
static const lv_btnmatrix_ctrl_t kb_es_lower_ctrl[] = {
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1),
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
};
static const char* kb_es_upper[] = {
"1#", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", LV_SYMBOL_BACKSPACE, "\n",
"abc", "A", "S", "D", "F", "G", "H", "J", "K", "L", "Ñ", LV_SYMBOL_NEW_LINE, "\n",
"_", "-", "Z", "X", "C", "V", "B", "N", "M", ".", ",", ":", "\n",
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
};
static const lv_btnmatrix_ctrl_t kb_es_upper_ctrl[] = {
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1),
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
};
/* Italian gets the common accented vowels directly on the base layer. */
static const char* kb_it_lower[] = {
"1#", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", LV_SYMBOL_BACKSPACE, "\n",
"ABC", "a", "s", "d", "f", "g", "h", "j", "k", "l", "à", LV_SYMBOL_NEW_LINE, "\n",
"è", "ì", "z", "x", "c", "v", "b", "n", "m", ".", ",", ":", "\n",
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
};
static const lv_btnmatrix_ctrl_t kb_it_lower_ctrl[] = {
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1),
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
};
static const char* kb_it_upper[] = {
"1#", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", LV_SYMBOL_BACKSPACE, "\n",
"abc", "A", "S", "D", "F", "G", "H", "J", "K", "L", "À", LV_SYMBOL_NEW_LINE, "\n",
"È", "Ì", "Z", "X", "C", "V", "B", "N", "M", ".", ",", ":", "\n",
LV_SYMBOL_KEYBOARD, LV_SYMBOL_LEFT, " ", LV_SYMBOL_RIGHT, LV_SYMBOL_OK, ""
};
static const lv_btnmatrix_ctrl_t kb_it_upper_ctrl[] = {
LV_KEYBOARD_CTRL_BTN_FLAGS | 5, EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), EN_KB_BTN(4), LV_BTNMATRIX_CTRL_CHECKED | 7,
LV_KEYBOARD_CTRL_BTN_FLAGS | 6, EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), EN_KB_BTN(3), LV_BTNMATRIX_CTRL_CHECKED | 7,
EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1), LV_BTNMATRIX_CTRL_CHECKED | EN_KB_BTN(1),
LV_KEYBOARD_CTRL_BTN_FLAGS | 2, LV_BTNMATRIX_CTRL_CHECKED | 2, 6, LV_BTNMATRIX_CTRL_CHECKED | 2, LV_KEYBOARD_CTRL_BTN_FLAGS | 2
};
struct OsKeyboardLayout {
KeyboardLayoutId id;
const char* name;
@@ -252,6 +401,21 @@ static const OsKeyboardLayout k_os_layouts[] = {
{ KeyboardLayoutId::AR, "AR",
kb_ar_lower, kb_ar_lower_ctrl,
kb_ar_lower, kb_ar_lower_ctrl }, // unicameral: same map for both
{ KeyboardLayoutId::FR, "FR",
kb_fr_lower, kb_fr_lower_ctrl,
kb_fr_upper, kb_fr_upper_ctrl },
{ KeyboardLayoutId::NL, "NL",
kb_nl_lower, kb_nl_lower_ctrl,
kb_nl_upper, kb_nl_upper_ctrl },
{ KeyboardLayoutId::DE, "DE",
kb_de_lower, kb_de_lower_ctrl,
kb_de_upper, kb_de_upper_ctrl },
{ KeyboardLayoutId::ES, "ES",
kb_es_lower, kb_es_lower_ctrl,
kb_es_upper, kb_es_upper_ctrl },
{ KeyboardLayoutId::IT, "IT",
kb_it_lower, kb_it_lower_ctrl,
kb_it_upper, kb_it_upper_ctrl },
};
/* ================================================================
@@ -271,6 +435,11 @@ static const HwKeyboardLayout k_hw_layouts[] = {
{ KeyboardLayoutId::SR, "SR" },
{ KeyboardLayoutId::EL, "EL" },
{ KeyboardLayoutId::AR, "AR" },
{ KeyboardLayoutId::FR, "FR" },
{ KeyboardLayoutId::NL, "NL" },
{ KeyboardLayoutId::DE, "DE" },
{ KeyboardLayoutId::ES, "ES" },
{ KeyboardLayoutId::IT, "IT" },
};
/* Bulgarian phonetic mapping for T-Deck.
@@ -424,6 +593,83 @@ static const char* hw_ar_digits[10] = {
nullptr, "ج", "د", "ذ", "ز", "ط", "ظ", "و", nullptr, nullptr
};
/* French AZERTY mapping for the T-Deck's US-QWERTY physical keyboard.
* We only remap the alpha keys so punctuation/digits remain predictable on the
* compact hardware; accented letters are still available through accent popups. */
static const char* hw_fr_lower[26] = {
"q", "b", "c", "d", "e", "f", "g", "h", "i", "j",
"k", "l", "m", "n", "o", "p", "a", "r", "s", "t",
"u", "v", "z", "x", "y", "w"
};
static const char* hw_fr_upper[26] = {
"Q", "B", "C", "D", "E", "F", "G", "H", "I", "J",
"K", "L", "M", "N", "O", "P", "A", "R", "S", "T",
"U", "V", "Z", "X", "Y", "W"
};
static const char* hw_fr_digits[10] = { nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr };
static const char* hw_fr_digits_shift[10] = { nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr };
/* German QWERTZ on the T-Deck's US-QWERTY hardware: swap Y/Z and keep the rest
* predictable. Umlauts / eszett stay available through accent popups. */
static const char* hw_de_lower[26] = {
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
"u", "v", "w", "x", "z", "y"
};
static const char* hw_de_upper[26] = {
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
"U", "V", "W", "X", "Z", "Y"
};
static const char* hw_de_digits[10] = { nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr };
static const char* hw_de_digits_shift[10] = { nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr };
/* Dutch keeps the standard alpha matrix but exposes the common IJ digraph on a
* digit slot so compact hardware still gets a language-specific shortcut. */
static const char* hw_nl_lower[26] = {
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
"u", "v", "w", "x", "y", "z"
};
static const char* hw_nl_upper[26] = {
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
"U", "V", "W", "X", "Y", "Z"
};
static const char* hw_nl_digits[10] = { nullptr, "ij", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
static const char* hw_nl_digits_shift[10] = { nullptr, "IJ", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
/* Spanish stays close to US QWERTY; use the number row to expose n-tilde on
* compact hardware where the semicolon key is absent. */
static const char* hw_es_lower[26] = {
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
"u", "v", "w", "x", "y", "z"
};
static const char* hw_es_upper[26] = {
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
"U", "V", "W", "X", "Y", "Z"
};
static const char* hw_es_digits[10] = { nullptr, "ñ", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
static const char* hw_es_digits_shift[10] = { nullptr, "Ñ", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
/* Italian uses the number row to surface the common accented vowels directly. */
static const char* hw_it_lower[26] = {
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
"u", "v", "w", "x", "y", "z"
};
static const char* hw_it_upper[26] = {
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
"U", "V", "W", "X", "Y", "Z"
};
static const char* hw_it_digits[10] = { nullptr, "à", "è", "ì", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
static const char* hw_it_digits_shift[10] = { nullptr, "À", "È", "Ì", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr };
/* ================================================================
* Runtime state
* ================================================================ */
@@ -499,6 +745,11 @@ static const HwPhoneticMap k_hw_maps[KEYBOARD_LAYOUT_COUNT] = {
/* SR */ { hw_sr_lower, hw_sr_upper, hw_sr_digits, hw_sr_digits_shift },
/* EL */ { hw_el_lower, hw_el_upper, hw_el_digits, hw_el_digits_shift },
/* AR */ { hw_ar_lower, hw_ar_lower, hw_ar_digits, hw_ar_digits }, // unicameral
/* FR */ { hw_fr_lower, hw_fr_upper, hw_fr_digits, hw_fr_digits_shift },
/* NL */ { hw_nl_lower, hw_nl_upper, hw_nl_digits, hw_nl_digits_shift },
/* DE */ { hw_de_lower, hw_de_upper, hw_de_digits, hw_de_digits_shift },
/* ES */ { hw_es_lower, hw_es_upper, hw_es_digits, hw_es_digits_shift },
/* IT */ { hw_it_lower, hw_it_upper, hw_it_digits, hw_it_digits_shift },
};
const char* keyboardLayoutMapHwKey(KeyboardLayoutId id, int key, bool shifted) {
+5
View File
@@ -19,6 +19,11 @@ enum class KeyboardLayoutId : uint8_t {
SR = 4, // Serbian (phonetic) — best-effort, validate with a native speaker
EL = 5, // Greek (ΕΛΟΤ-style phonetic)
AR = 6, // Arabic (RTL, Arabic-101 positions) — EXPERIMENTAL, needs on-device validation
FR = 7, // French (AZERTY)
NL = 8, // Dutch (QWERTY + accent popups)
DE = 9, // German (QWERTZ)
ES = 10, // Spanish (QWERTY + accent popups)
IT = 11, // Italian (QWERTY + accent popups)
Count
};
+3515 -377
View File
File diff suppressed because it is too large. Load diff
+38
View File
@@ -113,6 +113,10 @@ private:
* unlock from this state, only another BOOT button press. False = the
* screen turned off from idle timeout, in which case touch wakes it. */
bool _manual_lock;
// Burn-in guard for the lit lock screen (#55): stamped when the lock screen goes from off->lit;
// the loop dims a hard-locked LIT panel a bounded time after this, independent of the screen-timeout
// setting and of held/ghost touches that keep re-revealing it. 0 = not currently lit-locked.
uint32_t _lock_lit_ms = 0;
NodePrefs* _node_prefs;
// GPS auto-location: once a fix is seen, keep the node location (node_lat/lon,
// used by the profile + adverts) synced to GPS and persist it occasionally
@@ -332,6 +336,35 @@ public:
void toggleBuzzer();
bool getGPSState();
void toggleGPS();
#if defined(HAS_EXPANSION_KIT)
// Heltec V4 Expansion Kit: snapshot of the locally-attached sensor rail
// (battery, BME280, GXHTV3/SHT4X) plus the GPS/buzzer module presence.
struct LocalEnvSnapshot {
bool query_ok = false;
bool have_batt = false;
float batt_v = 0.0f;
bool have_bme_temp = false;
bool have_bme_hum = false;
bool have_bme_pressure = false;
bool have_bme_alt = false;
float bme_temp_c = 0.0f;
float bme_hum_pct = 0.0f;
float bme_pressure_hpa = 0.0f;
int16_t bme_alt_m = 0;
bool have_gxhtv3_temp = false;
bool have_gxhtv3_hum = false;
float gxhtv3_temp_c = 0.0f;
float gxhtv3_hum_pct = 0.0f;
bool gps_present = false;
bool gps_enabled = false;
bool gps_fix = false;
int gps_sats = -1;
bool buzzer_available = false;
bool buzzer_quiet = true;
};
bool getLocalEnvSnapshot(LocalEnvSnapshot& out) const;
bool getLocalEnvSummary(char* buf, size_t cap) const;
#endif
/** True if the GPS currently reports a valid fix. */
bool getGpsFix();
/** Satellites currently in view, or -1 if unknown / no GPS hardware. */
@@ -353,6 +386,7 @@ public:
void setAdvertLocationPolicy(uint8_t policy);
void setPathHashMode(uint8_t mode);
void setExperimentalFlags(uint8_t multi_acks, uint8_t client_repeat, uint8_t rx_boosted);
void setTelemetryAllow(bool on); // answer mesh telemetry requests (battery+env; location stays separate)
/** Meshcomod CLI on device: `wifi on` / `wifi off`. */
bool setWifiRadio(bool on);
bool isTcpEnabled() const { return _serial && _serial->isTcpEnabled(); }
@@ -381,6 +415,9 @@ public:
void unlockScreen();
/** True while the panel backlight is off (idle-dimmed or manually locked). */
bool isScreenOff() const { return _screen_off; }
bool isManualLocked() const { return _manual_lock; } // hard screen lock engaged
void toggleScreenLock(); // Tanmatsu Vol- long-press: lock <-> unlock
void sleepScreen(); // soft screen sleep (backlight off, not locked)
/** True while hard-locked (manual lock engaged), whether lit or dark. */
bool isManualLock() const { return _manual_lock; }
bool sendAdvertNow(); // legacy: zero-hop
@@ -411,6 +448,7 @@ public:
int8_t snr_q4, int8_t rssi) override;
void appSentMsgToContact(const uint8_t* to_pub, const char* to_name, const char* text,
uint32_t ack_hash) override;
void appSentMsgToChannel(const char* channel_name, const char* text) override;
void notify(UIEventType t = UIEventType::none) override;
void logRxFrame(float snr, float rssi, const uint8_t* raw, int len) override;
void discoveredContact(const ContactInfo& contact, bool is_new, uint8_t path_len) override;
+20 -1
View File
@@ -509,6 +509,18 @@ static const uint8_t e_1f37a[] = {0,0,0,0,0,0,255,255,2,0,0,0,46,254,22,55,255,1
static const lv_img_dsc_t d_1f37a = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 16, 16 }, sizeof(e_1f37a), e_1f37a };
static const uint8_t e_1f37b[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,2,0,0,0,68,249,65,100,249,52,101,249,94,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,239,251,2,115,254,5,197,249,48,100,249,86,36,249,142,64,248,61,114,255,14,89,255,119,154,255,137,186,255,126,56,255,66,0,0,0,85,253,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,248,32,2,249,106,102,250,63,220,255,165,221,255,233,255,255,255,255,255,255,255,255,255,155,255,237,192,252,5,0,0,0,255,255,2,255,255,2,255,255,2,89,255,39,187,255,166,252,255,158,253,255,193,187,255,219,254,255,255,255,255,255,255,255,252,255,255,249,221,255,251,123,255,215,254,255,24,255,255,19,0,0,0,255,255,5,0,0,0,88,255,115,255,255,255,255,255,255,255,255,255,255,255,255,189,255,252,156,255,254,189,255,255,121,255,254,212,254,255,216,238,224,187,214,200,186,214,215,125,239,50,0,0,0,255,255,42,91,247,117,188,255,250,154,255,249,122,255,253,255,255,253,255,255,255,255,255,255,121,255,255,49,246,254,232,228,254,76,229,255,30,215,94,121,214,105,60,231,158,158,247,100,251,222,234,252,222,221,240,237,252,101,228,255,199,228,255,122,255,255,23,255,255,122,255,253,174,237,254,100,220,255,2,220,253,165,236,255,26,239,140,255,255,10,60,231,189,60,231,185,85,181,43,191,247,165,106,245,255,192,219,252,7,229,255,214,246,255,2,220,255,142,229,239,35,220,253,67,220,255,131,228,254,224,244,255,113,238,212,127,231,143,121,214,209,93,239,174,0,0,0,156,247,209,70,245,255,2,237,253,112,246,255,48,246,255,224,219,252,115,238,237,80,246,255,225,236,255,165,245,254,96,253,254,73,254,245,187,214,211,51,165,67,218,222,202,61,223,142,90,247,238,98,253,255,131,245,254,199,245,255,68,245,255,6,237,247,213,238,228,51,255,255,195,253,255,226,253,255,160,253,255,199,237,251,220,214,183,255,255,5,84,173,70,123,206,189,142,254,250,96,253,255,193,253,255,96,253,255,64,253,255,201,237,231,95,231,136,176,246,246,229,253,251,130,245,249,138,221,246,121,214,255,28,223,203,90,239,13,0,0,0,158,247,142,181,238,250,112,238,241,78,238,245,9,246,246,226,253,254,78,246,189,255,231,54,184,222,255,53,214,255,120,214,255,188,214,251,61,231,186,93,247,36,0,0,0,121,206,5,154,214,157,57,198,240,58,198,251,90,198,251,123,206,247,185,222,255,251,230,188,255,231,10,61,231,119,62,231,158,61,231,127,124,247,62,0,0,0,0,0,0,255,255,1,0,0,0,170,82,6,181,181,79,182,189,140,214,189,176,214,189,195,248,189,197,154,214,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,2,0,0,0};
static const lv_img_dsc_t d_1f37b = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 16, 16 }, sizeof(e_1f37b), e_1f37b };
static const uint8_t e_26bd[] = {0,0,0,0,0,1,0,0,0,0,0,0,77,99,65,154,214,167,28,231,228,60,231,253,219,222,253,179,148,228,73,66,172,227,24,72,0,0,0,0,0,0,239,123,2,0,0,0,0,0,1,0,0,0,44,99,10,8,58,155,48,132,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,207,123,255,53,165,255,247,189,169,146,148,19,0,0,0,239,123,2,170,82,3,170,42,6,138,74,190,8,58,255,207,115,253,158,247,252,255,255,254,255,255,255,255,255,255,56,198,254,81,140,253,255,255,253,255,255,255,219,222,211,211,156,18,239,123,2,0,0,0,138,74,139,41,66,255,207,115,249,20,165,255,174,115,255,211,156,255,52,165,255,45,99,255,41,66,255,171,82,255,251,222,255,255,255,249,255,255,255,154,214,171,0,0,0,41,66,44,41,58,250,81,140,252,223,255,254,255,255,255,255,255,255,243,156,255,167,49,255,73,66,255,106,74,255,8,58,255,44,99,255,190,247,255,255,255,252,158,247,255,44,99,69,20,157,147,239,123,255,255,255,252,255,255,255,223,255,255,255,255,255,56,198,255,40,58,255,106,74,255,105,66,255,106,74,255,41,58,255,109,107,255,52,165,252,118,173,254,41,66,171,56,198,212,146,148,254,255,255,253,255,255,255,255,255,255,255,255,255,89,206,255,8,58,255,138,74,255,106,74,255,106,74,255,8,58,255,211,156,255,215,189,254,207,123,255,41,66,227,89,206,237,113,140,255,255,255,254,255,255,255,255,255,255,255,255,255,211,156,255,41,66,255,8,58,255,41,66,255,40,58,255,77,99,255,255,255,255,255,255,255,125,239,255,106,74,251,251,222,236,16,132,255,154,206,254,255,255,255,255,255,255,81,140,255,146,148,255,60,231,255,117,173,255,175,115,255,105,66,255,219,214,255,255,255,255,255,255,255,190,247,255,44,99,254,56,198,211,236,90,255,8,58,253,240,123,255,16,124,255,20,165,255,255,255,255,255,255,255,255,255,255,255,255,255,178,148,255,121,206,255,255,255,255,255,255,254,158,247,255,203,82,237,240,123,153,138,74,255,106,74,252,199,49,255,45,99,255,255,255,255,255,255,255,255,255,255,223,255,255,255,255,255,215,189,255,52,173,255,255,255,255,255,255,253,243,156,254,170,82,191,41,66,59,106,74,255,105,66,252,105,66,255,203,82,255,158,247,255,255,255,255,223,255,255,223,255,255,255,255,255,186,214,255,174,115,255,186,214,255,150,181,252,44,99,255,121,206,95,0,0,0,73,66,172,138,74,255,73,66,249,199,49,255,150,181,255,255,255,255,255,255,255,255,255,255,28,231,255,77,99,255,40,58,255,40,58,250,203,82,255,182,181,203,255,255,6,239,123,2,73,74,21,106,66,213,203,82,255,240,123,253,81,132,252,16,132,254,182,181,255,56,198,255,203,82,254,8,58,253,106,74,252,106,74,255,236,90,237,154,214,40,0,0,0,239,123,2,0,0,0,170,82,21,150,181,172,251,222,255,125,239,255,24,198,255,145,148,255,12,91,255,41,66,255,138,74,255,138,74,255,106,74,199,232,57,45,0,0,0,239,123,2,0,0,0,239,123,2,0,0,0,0,0,0,24,198,73,24,198,168,122,214,224,219,222,250,24,198,254,171,82,235,105,66,190,106,74,101,134,49,5,0,0,0,255,255,2,0,0,0};
static const lv_img_dsc_t d_26bd = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 16, 16 }, sizeof(e_26bd), e_26bd };
static const uint8_t e_1f3c8[] = {0,0,0,0,0,0,0,0,0,0,0,0,160,170,3,0,0,0,204,252,5,228,161,85,103,162,173,83,205,231,190,247,252,153,230,254,42,163,237,1,121,191,164,129,97,0,0,0,0,0,0,0,0,0,0,248,1,0,120,2,0,0,0,4,162,78,196,145,205,196,137,255,214,221,255,149,205,255,140,163,255,27,239,255,255,255,255,111,196,255,163,153,255,196,137,117,0,0,0,0,0,1,224,123,2,0,0,0,5,162,143,164,145,255,70,146,255,219,230,253,190,255,253,92,247,254,229,129,255,228,153,255,246,221,254,255,255,251,144,204,254,34,129,217,0,248,1,160,170,3,0,0,0,37,162,164,98,145,255,10,155,252,27,247,253,93,247,255,218,238,255,38,138,255,164,137,255,196,153,255,163,153,255,149,213,254,255,255,255,43,155,232,170,170,3,0,0,0,38,162,141,66,145,255,108,163,249,218,238,255,60,247,255,251,238,255,6,138,255,131,137,255,228,153,255,229,153,255,196,153,255,163,153,254,120,230,255,173,155,232,0,0,0,70,170,66,98,145,255,42,155,251,246,221,254,222,255,255,55,230,255,196,129,255,164,137,255,228,161,255,228,153,255,228,153,255,228,153,255,163,153,254,70,162,255,229,121,221,255,255,2,196,153,201,103,154,254,116,213,252,255,255,255,84,213,255,6,138,255,131,129,255,228,161,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,253,164,145,255,34,97,194,66,153,66,66,145,255,210,188,254,255,255,255,55,230,255,168,154,255,66,121,255,5,162,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,252,164,129,255,67,97,139,14,188,155,5,162,255,104,146,252,116,205,255,140,171,255,1,121,255,5,162,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,255,196,145,254,67,105,253,66,105,55,255,255,217,60,247,255,75,163,253,192,120,255,98,129,255,5,154,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,252,131,129,255,67,105,187,0,0,0,120,230,242,255,255,255,255,255,255,51,213,255,196,153,255,196,153,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,255,228,153,254,131,129,251,67,105,254,99,113,51,0,0,0,164,145,253,238,171,255,157,247,255,255,255,255,214,213,255,196,153,255,196,153,255,228,153,255,228,153,255,228,153,255,228,153,254,131,129,250,99,113,255,67,105,118,0,0,0,160,82,3,164,137,253,33,121,255,42,179,255,60,239,255,255,255,255,214,221,255,196,153,255,196,153,255,228,153,254,196,153,251,131,129,255,99,113,255,67,105,137,0,0,0,160,82,3,0,0,0,131,129,244,228,145,253,130,153,253,233,170,254,60,239,254,255,255,254,181,213,253,228,153,253,196,145,255,131,121,255,67,105,245,67,113,107,0,0,0,160,82,3,0,0,0,0,0,0,99,121,167,228,161,255,5,162,255,131,153,255,42,179,255,157,247,255,251,238,255,136,146,255,34,105,240,99,105,161,99,105,40,0,0,0,0,80,3,0,0,0,0,0,0,0,0,0,227,137,16,228,153,139,228,153,210,228,145,238,99,137,240,201,146,220,165,113,178,225,96,111,131,113,36,0,0,0,0,0,0,160,82,3,0,0,0,0,0,0,0,0,0,0,0,0};
static const lv_img_dsc_t d_1f3c8 = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 16, 16 }, sizeof(e_1f3c8), e_1f3c8 };
static const uint8_t e_1f5ff[] = {0,0,0,0,0,0,239,123,2,0,0,0,56,206,50,154,222,169,24,206,218,150,189,232,182,197,213,215,197,154,86,181,43,0,0,0,239,123,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,1,57,214,40,89,214,237,89,214,255,247,205,255,215,197,255,215,197,255,56,214,255,211,172,224,170,122,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,117,189,156,52,181,255,206,131,248,141,123,254,48,148,254,178,164,254,52,181,250,80,148,255,170,98,177,235,90,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,107,194,7,74,254,203,98,252,207,131,255,8,74,255,7,74,255,170,98,255,12,107,252,203,106,255,170,98,133,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,1,8,74,172,12,107,255,215,197,252,56,214,255,235,106,255,235,98,255,170,90,255,73,82,253,203,98,252,170,98,211,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,85,181,159,24,206,255,56,214,251,48,148,255,174,131,255,24,206,255,215,197,255,12,107,254,170,98,255,170,98,223,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,149,189,42,247,197,245,89,214,255,52,181,254,202,98,255,174,131,255,215,197,255,85,181,255,235,106,254,170,98,255,170,98,226,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,117,189,129,121,222,255,215,197,252,178,164,255,52,181,255,142,123,255,243,172,255,52,181,255,138,90,254,170,98,255,170,98,231,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,150,189,177,207,131,254,105,90,252,203,98,255,16,140,255,76,115,255,207,139,255,48,148,255,138,90,254,170,98,255,170,98,231,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,215,197,196,85,181,255,48,148,253,142,123,255,44,115,255,77,123,255,48,148,255,109,123,255,138,90,254,170,98,254,170,98,235,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52,181,204,210,164,255,113,156,253,178,164,255,48,148,255,16,148,255,81,156,255,142,123,255,138,90,254,203,98,255,170,98,251,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52,181,209,142,123,255,203,98,253,44,115,255,16,148,255,48,148,255,81,148,255,142,123,254,138,90,254,170,98,232,170,98,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,215,197,207,52,181,255,207,139,253,81,156,255,215,197,255,243,172,255,48,148,255,142,131,251,203,98,255,170,98,144,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,182,197,218,215,197,254,85,181,253,117,189,254,182,197,254,52,181,253,48,148,253,142,123,248,170,98,252,170,98,171,170,82,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,52,181,255,247,205,255,247,205,255,182,197,255,182,197,255,85,181,255,113,156,255,109,123,255,203,106,255,170,98,157,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,148,127,146,156,223,146,156,242,113,156,241,113,156,229,48,148,204,239,139,181,203,98,170,170,98,129,138,106,22,0,0,1,0,0,0,0,0,0,0,0,0};
static const lv_img_dsc_t d_1f5ff = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 16, 16 }, sizeof(e_1f5ff), e_1f5ff };
static const uint8_t e_1f3f3[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,154,93,65,219,93,164,29,94,219,29,94,234,252,93,221,252,93,184,187,93,125,122,93,60,145,83,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,220,85,232,255,86,255,191,86,255,191,86,255,191,86,255,223,86,255,191,86,255,62,94,255,28,94,219,219,93,162,154,93,107,89,85,67,57,93,52,57,85,61,154,93,110,187,93,195,219,101,230,60,158,252,186,189,253,185,197,254,185,189,254,251,165,253,61,134,252,127,102,255,223,86,255,255,86,255,223,94,255,127,94,255,94,94,255,126,94,255,223,94,255,94,86,255,245,220,230,54,253,255,21,253,254,21,253,255,21,253,255,21,253,255,54,253,255,119,237,255,185,197,253,251,157,251,93,126,251,126,102,253,127,102,254,126,102,253,93,126,251,120,165,252,213,228,230,249,253,255,90,246,254,155,254,255,122,246,255,25,246,255,151,245,255,54,253,255,21,253,255,22,253,255,54,253,255,87,237,255,119,229,255,119,237,255,54,253,255,212,236,255,219,230,230,255,255,255,255,255,254,255,255,255,255,255,255,255,255,255,223,255,255,93,255,255,155,254,255,249,245,255,119,245,255,54,245,255,54,253,255,54,245,255,151,245,255,216,229,255,125,239,230,223,255,255,93,255,254,61,255,255,93,255,255,191,255,255,255,255,255,255,255,255,255,255,255,255,255,255,191,255,255,125,255,255,61,255,255,93,255,255,223,255,255,158,239,255,216,229,230,184,253,255,54,245,254,54,253,255,54,245,255,119,245,255,216,245,255,123,246,255,61,255,255,223,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,252,238,255,180,236,230,86,253,255,87,237,254,119,229,255,87,237,255,54,245,255,22,253,255,21,253,255,54,253,255,119,245,255,249,245,255,122,246,255,187,254,255,123,246,255,249,245,255,21,229,255,88,173,228,93,142,251,126,102,251,127,102,253,126,110,252,93,126,251,251,157,252,185,197,253,119,229,255,54,253,255,21,253,255,21,253,255,21,253,255,21,253,255,22,253,255,245,228,255,28,86,239,31,95,255,127,94,255,126,94,255,159,94,255,223,94,255,255,86,255,191,86,255,127,102,255,61,134,252,251,165,253,185,197,254,185,205,255,185,197,255,251,165,253,219,109,253,186,93,192,219,93,131,121,93,72,89,85,58,89,93,72,154,93,110,219,93,168,28,94,223,94,94,255,191,86,255,191,86,255,191,86,255,159,86,255,159,86,255,223,86,255,61,86,255,239,59,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,83,76,13,121,93,66,187,93,136,220,93,200,252,93,237,252,93,253,28,94,242,252,93,196,154,93,105};
static const lv_img_dsc_t d_1f3f3 = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 16, 16 }, sizeof(e_1f3f3), e_1f3f3 };
static const uint8_t e_1f1f1[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,19,1,255,20,1,255,20,1,255,20,1,255,20,1,255,20,1,255,20,1,255,20,1,255,19,1,255,19,1,255,20,1,255,20,1,255,20,1,255,20,1,255,20,1,255,20,1,255,20,1,255,179,0,255,179,0,255,179,0,255,179,0,255,179,0,255,179,0,255,179,0,255,212,0,255,212,0,255,179,0,255,179,0,255,179,0,255,179,0,255,179,0,255,179,0,255,179,0,255,220,181,255,220,181,255,220,181,255,220,181,255,220,181,255,220,181,255,220,181,255,118,132,255,118,132,255,220,181,255,220,181,255,220,181,255,220,181,255,220,181,255,220,181,255,220,181,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,19,165,255,19,165,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,223,255,255,223,255,255,223,255,255,223,255,255,223,255,255,223,247,255,255,255,255,138,82,255,105,74,255,255,255,255,223,247,255,223,255,255,223,255,255,223,255,255,223,255,255,223,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,118,181,255,65,16,255,65,16,255,86,181,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,248,182,255,248,182,255,248,182,255,248,182,255,248,182,255,248,182,255,114,133,255,13,84,255,45,84,255,114,133,255,248,182,255,248,182,255,248,182,255,248,182,255,248,182,255,248,182,255,103,4,255,103,4,255,103,4,255,103,4,255,103,4,255,103,4,255,200,4,255,232,4,255,232,4,255,200,4,255,103,4,255,103,4,255,103,4,255,103,4,255,103,4,255,103,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,136,4,255,136,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255,168,4,255};
static const lv_img_dsc_t d_1f1f1 = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 16, 16 }, sizeof(e_1f1f1), e_1f1f1 };
typedef struct { uint32_t cp; const lv_img_dsc_t* dsc; } EmojiGlyph;
static const EmojiGlyph kGlyphs[] = {
{ 0x200Du, &d_zero },
@@ -517,6 +529,8 @@ static const EmojiGlyph kGlyphs[] = {
{ 0x261Du, &d_261d },
{ 0x26A0u, &d_26a0 },
{ 0x26A1u, &d_26a1 },
{ 0x26A7u, &d_zero },
{ 0x26BDu, &d_26bd },
{ 0x2705u, &d_2705 },
{ 0x270Au, &d_270a },
{ 0x270Bu, &d_270b },
@@ -539,6 +553,8 @@ static const EmojiGlyph kGlyphs[] = {
{ 0x2B55u, &d_2b55 },
{ 0xFE0Eu, &d_zero },
{ 0xFE0Fu, &d_zero },
{ 0x1F1F1u, &d_1f1f1 },
{ 0x1F1F8u, &d_zero },
{ 0x1F308u, &d_1f308 },
{ 0x1F30Au, &d_1f30a },
{ 0x1F319u, &d_1f319 },
@@ -566,6 +582,8 @@ static const EmojiGlyph kGlyphs[] = {
{ 0x1F38Au, &d_1f38a },
{ 0x1F3AFu, &d_1f3af },
{ 0x1F3C6u, &d_1f3c6 },
{ 0x1F3C8u, &d_1f3c8 },
{ 0x1F3F3u, &d_1f3f3 },
{ 0x1F40Cu, &d_1f40c },
{ 0x1F414u, &d_1f414 },
{ 0x1F417u, &d_1f417 },
@@ -651,6 +669,7 @@ static const EmojiGlyph kGlyphs[] = {
{ 0x1F595u, &d_1f595 },
{ 0x1F596u, &d_1f596 },
{ 0x1F5A4u, &d_1f5a4 },
{ 0x1F5FFu, &d_1f5ff },
{ 0x1F600u, &d_1f600 },
{ 0x1F601u, &d_1f601 },
{ 0x1F602u, &d_1f602 },
@@ -767,7 +786,7 @@ static const EmojiGlyph kGlyphs[] = {
{ 0x1F9D0u, &d_1f9d0 },
{ 0x1F9E1u, &d_1f9e1 },
};
const uint16_t kEmojiGlyphCount = 252;
const uint16_t kEmojiGlyphCount = 257;
const lv_img_dsc_t* emojiGlyphLookup(uint32_t cp) {
int lo = 0, hi = (int)(sizeof(kGlyphs)/sizeof(kGlyphs[0])) - 1;
+1 -1
View File
@@ -1,5 +1,5 @@
// AUTO-GENERATED by scripts/build/gen-emoji-font.py — do not edit.
// 252 Noto colour emoji baked to RGB565+alpha at 16px.
// 257 Noto colour emoji baked to RGB565+alpha at 16px.
#pragma once
#include "lvgl.h"
#ifdef __cplusplus
+7
View File
@@ -0,0 +1,7 @@
// AUTO-GENERATED by scripts/build/gen-qr-icon.py — do not edit.
// 18x18 recolour-able QR glyph for the Chats-header Share button.
#pragma once
#include "lvgl.h"
static const uint8_t qr_icon_map[] = {255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255};
static const lv_img_dsc_t qr_icon_dsc = { { LV_IMG_CF_TRUE_COLOR_ALPHA, 0, 0, 18, 18 }, sizeof(qr_icon_map), qr_icon_map };
+3
View File
@@ -44,6 +44,9 @@ set(WADA_DEFS
MAX_CONTACTS=500 MAX_GROUP_CHANNELS=40 # 2000 was a ~400KB internal-DRAM strand; BT's SDIO RX buffers (MALLOC_CAP_INTERNAL|DMA) need that headroom
ENABLE_PRIVATE_KEY_IMPORT=1 ENABLE_PRIVATE_KEY_EXPORT=1
DISPLAY_CLASS=TanmatsuDisplay
# microSD lives on the P4's SDMMC *slot 0* (IOMUX pins CLK43/CMD44/D0-3 39-42); slot 1 is the
# C6 radio (esp-hosted SDIO), so these make Arduino SD_MMC target slot 0 only — no radio conflict.
BOARD_HAS_SDMMC=1 BOARD_SDMMC_SLOT=0
# the core's ESP32Board.h reads these raw pins directly; Tanmatsu uses badge-bsp, so -1 (none):
P_LORA_DIO_1=-1 P_LORA_TX_LED=-1 P_LORA_TX_NEOPIXEL_LED=-1
PIN_BOARD_SCL=-1 PIN_BOARD_SDA=-1 PIN_VBAT_READ=-1)
+34 -3
View File
@@ -8,6 +8,8 @@
#include <Arduino.h>
#include <Mesh.h>
#include "MyMesh.h"
#include <new> // placement-new for the PSRAM-resident the_mesh
#include "esp_heap_caps.h" // heap_caps_malloc(MALLOC_CAP_SPIRAM)
#include "UITask.h"
#include "target.h" // board, radio_driver, rtc_clock, display, sensors, radio_init()
#include <SPIFFS.h>
@@ -27,6 +29,8 @@ extern "C" {
#include "bsp/device.h"
#include "bsp/input.h"
#include "bsp/display.h"
#include "bsp/tanmatsu.h" // bsp_tanmatsu_coprocessor_get_handle (C6 radio power)
#include "tanmatsu_coprocessor.h" // tanmatsu_coprocessor_radio_disable / _enable_application
#include "esp_hosted.h" // C6 radio co-processor link (WiFi + LoRa ride esp-hosted)
#include "esp_netif.h" // lwIP/tcpip bring-up (else WiFi calls assert "Invalid mbox")
#include "esp_event.h"
@@ -54,7 +58,17 @@ MultiTransportCompanionInterface serial_interface;
StdRNG fast_rng;
SimpleMeshTables tables;
UITask ui_task(&board, &serial_interface);
MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store, &ui_task);
// the_mesh (~42 KB, dominated by the MAX_CONTACTS array) lives in PSRAM instead of the
// scarce internal DRAM — the P4 has 32 MB PSRAM. Placement-new into a heap_caps_malloc
// block + a reference, exactly like src/main.cpp on the S3 boards: the ctor still runs
// HERE at static-init (PSRAM is up first), so only the address moves. Internal-RAM
// fallback if PSRAM is somehow absent. MyMesh.h declares `extern MyMesh& the_mesh`.
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();
// ---------------------------------------------------------------------------
// Boot splash — a tiny on-screen console so launching the app shows progress
@@ -304,6 +318,19 @@ static void wadameshSetup() {
esp_netif_init();
esp_event_loop_create_default();
bootLog("C6 radio coproc");
// Power-cycle the C6 through the CH32 so the app ALWAYS gets a fresh C6. Without this,
// re-launching the app (especially right after a flash) onto a C6 still in its previous
// state wedges esp-hosted ("Not able to connect with ESP-Hosted slave") into a P4 reboot
// loop that previously only a manual power cycle cleared — this does the same in software.
{
tanmatsu_coprocessor_handle_t cph = nullptr;
if (bsp_tanmatsu_coprocessor_get_handle(&cph) == ESP_OK && cph) {
tanmatsu_coprocessor_radio_disable(cph);
delay(150);
tanmatsu_coprocessor_radio_enable_application(cph); // boot the C6 into its esp-hosted slave fw
delay(600); // let the C6 come up before we attach
}
}
hostedConnectC6(); // identify the C6 slave so WiFi + LoRa work (must precede radio_init)
bootLog("Radio link");
if (!radio_init()) { bootLog("radio init FAILED"); Serial.println("[BOOT] radio_init FAILED"); }
@@ -437,6 +464,11 @@ extern "C" void app_main(void) {
}
if (WiFi.status() == WL_CONNECTED) {
if (!modem_sleep_set) { WiFi.setSleep(true); modem_sleep_set = true; }
// Start the TCP (+ WebSocket) companion server now the STA is associated. Deferred to here on
// purpose: starting WiFiServer before association asserts in lwIP (the old reason this was
// stubbed out). startTcpServer() is idempotent — it no-ops once the server is up — so calling
// it on every connected tick is safe.
serial_interface.startTcpServer(true);
if (!sntp_kicked) {
char tz[48]; touchPrefsBuildLocalTz(tz, sizeof tz);
configTzTime(tz, "pool.ntp.org", "time.google.com");
@@ -447,8 +479,7 @@ extern "C" void app_main(void) {
}
} else if (sntp_kicked && !sntp_pushed) { sntp_kicked = false; }
}
// (No TCP/WS companion server on the Tanmatsu — the companion app connects over USB serial.
// Starting WiFiServer here asserted in lwIP when the STA wasn't associated yet.)
serial_interface.tickWebSocketHandshake(); // accept WS clients/handshakes (no-op until the WS server is up)
the_mesh.loop(); // mesh + companion servers
vTaskDelay(1);
+6
View File
@@ -21,6 +21,12 @@ CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE=y
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_ENABLED=y
# Persist BLE bonds (LTK/IRK) to NVS so a paired phone survives a device reboot. Default is OFF, which
# keeps bonds in RAM only — after every power-cycle the device has no key, so the phone's saved pairing
# is rejected as "invalid" and the user must delete + re-pair. The nvs partition (partition_tables/16M.csv)
# already backs Wi-Fi creds, so there's room for the (max 3) bonds.
CONFIG_BT_NIMBLE_NVS_PERSIST=y
# AsyncTCP (pulled by the core's ESP32Board HTTP-OTA) references CONFIG_ESP_TASK_WDT_TIMEOUT_S,
# which IDF 5.5 only defines when the task watchdog is initialised. Enable it (5 s timeout).
CONFIG_ESP_TASK_WDT_INIT=y
+12
View File
@@ -54,6 +54,18 @@ int TanmatsuLoraRadio::recvRaw(uint8_t* bytes, int sz) {
// one-shot is enough. It also gates pushConfig from here on (the remote link blocks under RX load).
_rx_polling = true;
lora_set_mode(&_h, LORA_PROTOCOL_MODE_RX);
} else if (_cfg_dirty) {
// A runtime setParams()/setTxPower()/setRxBoostedGainMode() (the wadamesh radio
// settings save, from the UI or the companion app) changed the PHY config. Apply it
// HERE, on the RX-poll thread, so the C6 remote link is never touched cross-thread:
// leave RX to STANDBY (so the link isn't under active-RX load and lora_set_config
// can't block), push the new config, then re-arm continuous RX. This is what makes a
// live frequency / SF / BW / CR / power change actually reach the modem (it used to
// need a reboot) AND lets wadamesh's settings override whatever the Tanmatsu OS set.
_cfg_dirty = false;
lora_set_mode(&_h, LORA_PROTOCOL_MODE_STANDBY_RC);
lora_set_config(&_h, &_cfg);
lora_set_mode(&_h, LORA_PROTOCOL_MODE_RX);
}
lora_protocol_lora_packet_t pkt;
// Non-blocking poll of the RX queue (timeout 0).
+14 -5
View File
@@ -56,16 +56,25 @@ public:
uint32_t getPacketsSent() const { return _n_sent; }
private:
// Push _cfg to the modem — but ONLY while not yet polling RX. The remote link's
// esp_hosted_send_custom_data() blocks indefinitely under continuous-RX load, so a
// live reconfig from the UI thread would freeze the app. Boot bring-up (before the
// RX loop starts) pushes fine; runtime changes update _cfg and apply on next init.
void pushConfig() { if (_started && !_rx_polling) lora_set_config(&_h, &_cfg); }
// Push _cfg to the modem. The remote link's esp_hosted_send_custom_data() can block
// under continuous-RX load, so a live lora_set_config() from the UI/companion thread
// (mid-RX) risked freezing the app — which is why this used to no-op once RX started,
// making a frequency/SF/BW change only take effect on the next reboot ("receives
// nothing on the new frequency", and wadamesh's settings never overrode the Tanmatsu
// OS's). Now: boot bring-up pushes directly; a runtime change marks _cfg dirty and
// recvRaw() applies it on the RX-poll thread, between polls (standby → push → re-arm),
// so wadamesh's settings actually reach the modem and win.
void pushConfig() {
if (!_started) return;
if (_rx_polling) { _cfg_dirty = true; return; } // defer to recvRaw() (RX-poll thread)
lora_set_config(&_h, &_cfg);
}
lora_handle_t _h = {0};
lora_protocol_config_params_t _cfg = {0};
bool _started = false;
bool _rx_polling = false; // set once recvRaw() starts draining — see pushConfig()
bool _cfg_dirty = false; // runtime config change pending; applied by recvRaw() between polls
bool _sending = false;
bool _rx_boost = false;
float _last_rssi = 0.0f;