Compare commits

...
205 Commits
Author SHA1 Message Date
Kaj SchittecatandClaude Opus 5.5 1eac81b81f touch: beta_98 - release notes, language store v28
Repeater and room server admin, SenseCAP Indicator D1L, CrowPanel 10.1 touch,
OTA watchdog fix (#689), backup names (#688), P4 C6 recovery (#685), the web
client, sync cursor reuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:50:23 +02:00
Kaj SchittecatandClaude Opus 5.5 f0dc398cf2 companion sync: recycle the stalest cursor when all 8 are taken
Every browser running the web client now keeps its own sync cursor; with all
slots taken a new client was never synced at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:47:47 +02:00
Kaj SchittecatandClaude Opus 5.5 5350db0110 deploy+site: the Wi-Fi copy is wifi.app.wadamesh.com
Two levels deep: proxied (origin IP stays hidden) but outside Universal
SSL, so https fails and Chrome stays on http.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:22:45 +02:00
Kaj SchittecatandClaude Opus 5.5 8a24c42969 site: drop the unverified Insecure-content tip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:59:38 +02:00
Kaj SchittecatandClaude Opus 5.5 7d8a094ecf site: the Wi-Fi version lives on wifi.wadamesh.com
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:59:29 +02:00
Kaj SchittecatandClaude Opus 5.5 6657014035 deploy: serve the web client's Wi-Fi copy on wifi.wadamesh.com
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:59:18 +02:00
Kaj Schittecat 8954fe06df Merge repeater-admin: repeater and room server admin app
# Conflicts:
#	deploy/apps/lang/bg.lang
#	deploy/apps/lang/cs.lang
#	deploy/apps/lang/de.lang
#	deploy/apps/lang/el.lang
#	deploy/apps/lang/es.lang
#	deploy/apps/lang/fr.lang
#	deploy/apps/lang/hu.lang
#	deploy/apps/lang/it.lang
#	deploy/apps/lang/nl.lang
#	deploy/apps/lang/pt-br.lang
#	deploy/apps/lang/ro.lang
#	deploy/apps/lang/ru.lang
#	deploy/apps/lang/sr.lang
#	deploy/apps/lang/uk.lang
#	src/ui-touch/i18n_builtin.h
2026-10-18 15:34:38 +02:00
Kaj SchittecatandClaude Opus 5.5 7ee059b803 i18n: regenerate the V4 table for the repeater admin strings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:17:08 +02:00
Kaj SchittecatandClaude Opus 5.5 2a4e48bc50 touch: repeater and room server admin app
The contact sheet's Admin button (repeaters, and now room servers) opens a
full admin app instead of a password box and a raw console. Login as admin
or guest (remembered password logs in at once), then an overview with the
node's status (GET_STATUS), firmware and owner info (GET_OWNER_INFO),
neighbours (GET_NEIGHBOURS, paged, newest or strongest), telemetry, the
access list with role changes (GET_ACCESS_LIST + setperm), settings pages
(identity and location, radio, repeating and limits, adverts, passwords and
access, regions) and actions (adverts, clock sync, discover neighbours,
reset counters, reboot, firmware update). The old CLI console stays as
Advanced > Console.

Every CLI line carries the "wX|" correlation prefix MeshCore echoes back, so
a reply always lands on the field that asked; one request is in flight at a
time, reads retry once, writes never, and a write is followed by a re-read.
Radio changes go through a confirm that shows old and new values, with a
10 minute tempradio trial before a saved change, and a reboot offer after.

MyMesh gains uiSendBinaryRequest (UiReqKind::Binary -> onBinaryReply) and
uiLastLoginInfo (the LOGIN_OK ACL byte and firmware level). The repeater
ping toast read uptime from the airtime field (offset 16, not 20); fixed.
Parsing lives in RepeaterAdminProto.h with a host test. Translations for all
14 languages. -DDOC_CAPTURE -DDOC_AUDIT_FOCUS -DDOC_ADMIN_AUDIT captures the
app against a node in range as a guest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:17:08 +02:00
Kaj Schittecat 10b541cbbd Merge crowpanel-touch: CrowPanel 10.1 touch (GPIO45 SD power pin), tiles on storage, C6 version, quiet SD probe 2026-10-18 15:12:15 +02:00
Kaj Schittecat e566823d0f Merge p4-ui-bits: P4 always-external antenna, slim landscape composer, Glass tiles switch 2026-10-18 15:12:14 +02:00
Kaj Schittecat 33426ffaa4 Merge p4-at-recovery: the AT C6 resets and rejoins when it stops answering (#685) 2026-10-18 15:12:14 +02:00
Kaj SchittecatandClaude Opus 5.5 d2ec668535 site: the web client (app.wadamesh.com), its page, and a third hero button
- webclient.html: what the web client is, how to connect (USB, Bluetooth,
  Wi-Fi), where it works (desktop, Android, iPhone), the https / http split
  for Wi-Fi, where data lives, what only a device can do, and a FAQ, with
  screenshots of the real client.
- index: "Use the web client" next to Install and Join the community, a Web
  client button on the side rail and a resource card; sitemap.
- deploy/nginx/app.wadamesh.com.conf: the vhost (COOP/COEP/CORP, versioned
  bundles cached, same-origin /tiles/ and /fw/apps/ handed to the tiles and
  firmware vhosts over loopback). scripts/deploy-webclient.sh publishes
  web/dist (and web/dist-nothreads to nt/).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:09:22 +02:00
Kaj SchittecatandClaude Opus 5.5 d20e3702bb crowpanel_p4_101: fix touch (SD_MMC tore down I2C0 on GPIO45), quieter boot
Touch: the generic esp32p4 Arduino variant defines BOARD_SDMMC_POWER_PIN 45
(the Function EV board's SD power switch). SD_MMC.begin() did pinMode(45,
OUTPUT) and SD_MMC.end() cleared it, so the peripheral manager detached I2C0
(i2cDeinit) that ESP32Board::begin had started on SDA45/SCL46. Every Wire
call after the boot SD probe failed with "bus is not initialized". build.sh
now guards that block behind WADA_SDMMC_NO_POWER_PIN (defined for this board,
whose card has no power switch), failing the build if upstream drifts.

GT911 init now goes: LDO4 check -> bus begin/re-begin with a logged result
and the pin owner on failure -> RST/INT strap per the datasheet (0x5D, then
0x14, then RST only) -> full scan logged -> address pick -> product id,
firmware, config version, resolution and touch count logged. Three attempts,
700 ms apart, each actually on the bus.

Tiles: no free flash for a "tiles" partition without moving user data, so
CAP_TILES_ON_STORAGE routes the cache to microSD or the internal 'storage'
LittleFS, now with a 30% (min 2 MB) free-space floor for user data. The
"tiles" mount is only tried when the partition exists (no more partition
not found / format failed errors), and logs name the board.

SD: one quiet 400 kHz identification probe at boot; an empty slot logs "no
card or not responding" once and skips the 3-step ladder.

C6: the INIT-event firmware version is kept (build.sh patch) and logged
plainly against the host version; an older C6 gets 60 s Wi-Fi retries.

Also: no setCpuFrequencyMhz on the P4 (fixed 360 MHz), no SPIFFS purge walk
where SPIFFS is not mounted, and SdNvsPrefs asks nvs_open() before a
read-only Preferences open so absent namespaces no longer log NOT_FOUND.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:06:21 +02:00
Kaj SchittecatandClaude Opus 5.5 737b51aaa0 touch: regenerate the V4 language table for the new strings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:06:00 +02:00
Kaj SchittecatandClaude Opus 5.5 f71a42ef0e touch: P4 always-external antenna, slim landscape composer, Glass tiles switch
- T-Display P4 (CAP_ANTENNA_SELECT): Radio settings "Always use the external
  antenna (MMCX)". Boot selects the external socket only with this opt-in,
  which asks first with a damage warning. Own NVS key ant_ext (never the
  reserved cfg byte p4_antenna), left out of the settings backup. Turning it
  off goes back to the on-board antenna at once. The control panel gets an
  Antenna tile (filled + "MMCX" on external); a tap asks for the external
  socket through the same warning, hold opens Radio settings.
- Large screens in landscape (P4 landscape, Tanmatsu, CrowPanel 5.0/10.1):
  the empty composer text box is one line tall and grows from there; the row
  stays as tall as its buttons and a shorter box sits centred on them. P4
  portrait two-row layout unchanged.
- Settings > Theme > Background: "Glass tiles" (default on). Off draws Home
  widgets, cards, the control panel and its tiles solid. Pending + Apply.
- 11 new strings translated in all 14 languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:06:00 +02:00
Kaj SchittecatandClaude Opus 5.5 ce620f2671 tdisplay_p4: reset and rejoin a wedged ESP-AT C6 at runtime (#685)
On units whose C6 runs LilyGo's factory ESP-AT image, Wi-Fi died after
about an hour: scans came back empty, Connect did nothing, and only a
reboot helped. The AT worker brought the SDIO link up once at boot and
never again, so a C6 that wedged mid-session left every command timing
out for the rest of the session.

Link health: AT commands now report OK / ERROR / TIMEOUT / BUSY. A
timeout or busy reply asks for a plain "AT" ping; a ping that times out,
or three command timeouts in a row with no answer between them, is a
stall. ERROR replies are answers and never count. Connects to dead hosts
(CIPSTART) and missing CIPSEND prompts only ask for a ping. The decision
logic lives in c6_link_health.h with a host test.

Recovery: on a stall the worker takes the link down at once (queued
requests fail fast, links read closed so the TCP/WS companion servers
prune them), then runs sdio_bring_down + the XL9535 C6 reset +
c6at_begin, the same path as boot. Resets back off 30 s, 60 s, 120 s,
240 s, then every 5 min; 10 min of a healthy link resets the ladder.
After a successful reset it rejoins the last network and the kept
listener want-flag re-arms AT+CIPSERVER, so port 5000 comes back without
app-side changes. The Wi-Fi status reads "reconnecting" meanwhile, and
main.cpp no longer treats the reset as "radio off".

Join/disconnect: one latest-wins slot instead of the 6-deep queue, so
the user's Connect can no longer be dropped behind 10 s retry pairs and
a stale CWQAP can no longer undo a fresh join. c6at_join_busy() gates
main.cpp's retry while a join is pending or running.

Bring-up now also sends AT+SLEEP=0 and sets AT+CWRECONNCFG=5,0 when the
AT build supports it (query first; ERROR on old images is ignored).

The esp-hosted build is unchanged: everything is in the c6_at component
or behind !TDP4_C6_HOSTED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:05:54 +02:00
Kaj SchittecatandClaude Opus 5.5 17c3d6392f site: the P4 installer names the blue screen as the sign to pick Never connects
On a P4 whose C6 still runs the factory ESP-AT firmware the esp-hosted image
can stop on a teal/blue screen right after the logo (Discord), not only fail
to connect; both P4 cards' help text now names that symptom too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:51:03 +02:00
Kaj SchittecatandClaude Opus 5.5 66a0c30945 release: the SenseCAP Indicator D1L ships in betas and the web flasher
The #682 port is merged; add its env to release.sh, a flasher manifest, a
card under Seeed Studio on wadamesh.com (experimental, community port, no
device updates) and the README. The site card points at the beta feed, so
deploy the site after the next beta publishes. Board count 16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:50:26 +02:00
Kaj SchittecatandClaude Opus 5.5 d1524fd45b touch: backup names probe by opening the file (Tanmatsu FFat)
#688 picks the first free meshcore-backup-nodate-N with fs->exists(). On the
Tanmatsu FFat's exists() reports files that are there as missing (and stat
lies on the P4), so without a card every export would pick nodate-1 and
overwrite the previous one. Probe with open() instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:49:39 +02:00
Kaj Schittecat 75074e9d2a Merge PR #688: backups get one meshcore-backup-<date> name, no overwrite without a clock (#686) 2026-10-18 14:43:17 +02:00
Kaj Schittecat 2656327d2a Merge PR #682: SenseCAP Indicator D1L touch target (ST7701S, FT6336, SX1262 behind a TCA9535) 2026-10-18 14:41:21 +02:00
Kaj SchittecatandClaude Opus 5.5 35f462ecc8 touch: OTA update no longer trips the task watchdog while it waits (#689)
otaWorkerRun streamed the image with Update.writeStream(), which reads
through Stream::timedRead: that spins without yielding until HTTPClient's
20 s timeout. The OTA runs on the core-0 tile worker, so a slow start starved
IDLE0 and the 5 s task watchdog rebooted the device: "stuck at 0%, then a
restart" on a T-Deck Plus (#689). The image is now copied by hand through an
internal 4 KB buffer, waiting with vTaskDelay and yielding after every block,
giving up after 15 s without data (the same loop as the SD download).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:38:11 +02:00
Michael A. CojocariandClaude Opus 5.5 c67d07e252 Backups: one meshcore-backup-<date> name, no overwrite without a clock (#686)
With the clock unset, Export backup named the file after uptime seconds
(meshcore-backup-86.json), which read as a broken date and repeated after
every reboot, so a later export could overwrite an earlier backup.

Every timestamped backup is now meshcore-backup-YYYYMMDD-HHMMSS.json (was
meshcore-YYYYMMDD-HHMMSS.json), so the date always ends the name. Without a
set clock it is meshcore-backup-nodate-N.json; the Backups page picks the
first N not already on the card or in internal storage. The web download
uses the same pattern without the storage probe. Older names still list
and restore (the filters match meshcore-*.json).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MjAKa5hXgyBykCLqnwcjEy
2026-10-17 19:35:48 -04:00
Confituurke d37d1798e7 docs(sensecap-indicator): port notes and device table entry
PORT.md records what a reviewer or tester needs and the code does not
say in one place: the pin and expander maps, the boot order and why the
display comes before the radio, the /INT dispatch and DIO1 edge rules,
the TCXO strap and retry, the display timings, pixel clock and draw
band, the touch mapping, the flash layout and image size, what the port
leaves out (RP2040 peripherals, maps, deep sleep, OTA artifacts) and
what has been exercised on a D1L.

DEVICES.md lists the D1L as an experimental development-build target
with no battery and the untested D1Pro, and drops it from the requested
boards (#14).

Signed-off-by: Confituurke <9595028+Confituurke@users.noreply.github.com>
2026-10-17 22:39:32 +02:00
Confituurke 0aaafba96c feat(sensecap-indicator): hide Power off, nothing can wake the board
The Indicator's only user button is GPIO38, which is not an RTC GPIO on
the ESP32-S3 (those are 0..21), so it cannot wake the chip from deep
sleep, and the touch interrupt is behind the I2C expander. A software
power-off that only unplugging USB can undo reads as a freeze (#310,
#406), which is why the Wio Tracker L2 and the T-Display P4 hide it.

The board joins them in the Power-off gate, and its power menu card is
sized for the three remaining rows.

Signed-off-by: Confituurke <9595028+Confituurke@users.noreply.github.com>
2026-10-17 22:39:30 +02:00
Confituurke 8acb68415d feat(sensecap-indicator): SX1262 behind the TCA9535 via a RadioLib HAL
The SX1262's NSS, RESET, BUSY and DIO1 sit on the TCA9535 (P0.0..P0.3)
while SCK/MISO/MOSI are real GPIOs. IndicatorRadioHal derives from
ArduinoHal, keeps hardware FSPI at 4 MHz and maps the virtual pins
0x40..0x43 onto expander transfers; every other pin goes straight to
the Arduino pin and interrupt functions, as ArduinoHal does. The
meshcomod core is used as is: CustomSX1262 and its wrapper run
unchanged on top of the HAL.

DIO1 has no GPIO edge. The expander's open-drain /INT on GPIO42 has an
IRAM ISR that only notifies a dispatch task, because the read needs
I2C and the bus mutex. That task reads both input bytes under the bus
lock: the TCA9535 releases /INT only when the port that changed is
read, and several port-1 inputs float, so a port-0-only read could
leave GPIO42 low for good. Every port-0 byte feeds a small edge
detector (Dio1Edge) through the expander's port-0 observer, which
calls the callback RadioLib attached to DIO1 once per rise. That
callback is the core's setFlag, so TX-done and RX-done reach the core
exactly as they would from a DIO1 interrupt.

A BUSY poll is a port-0 read as well, and it clears /INT. Sampling DIO1
in those reads means a rise that lands while RadioLib waits on BUSY
fires the callback from that read instead of being lost; the dispatch
task's next read sees the level already high and does not fire again.
The task also re-reads every 50 ms, so a rise whose /INT was missed
still fires the callback, at most 50 ms late. MESH_RADIO_DIO1_POLLED is
not set: polling the IRQ register would add an SPI transfer, and with
it expander writes for NSS, on every pass of the receive path.

Floating port-1 inputs can keep /INT toggling, each toggle an I2C read
at high priority. The task counts ISR wakes and timeouts and logs them
for each of the first six 10 s windows, then only above 500 wakes/s
and at most once a minute, so the idle wake rate is visible at
bring-up without flooding the console later.

TCXO or crystal comes from the radio strap read at boot: the env's
SX126X_DIO3_TCXO_VOLTAGE calls indicatorTcxoVoltage(), which returns
2.4 V for the TCXO radio and 0 for the crystal one. radio_init() starts
the radio with the strap's setting and, if that fails, with the other
one, because std_init only falls back from TCXO to 0 V and a TCXO board
whose strap misread as crystal would never start otherwise. main.cpp's
one retry runs both again. The log says which voltage was requested and
which one the radio came up with.

The board reports GPIO42 (indicator::kExpanderIntGpio, shared with the
HAL) as its IRQ line and never light-sleeps: the inherited ESP32Board
versions would treat the virtual DIO1 pin as a GPIO, and /INT is active
low while the light-sleep wake source is high-level.

test_indicator_dio1 covers the virtual-pin routing and the
once-per-rise rule across dispatch and BUSY reads; run-host-tests.sh
gets its include path.

Signed-off-by: Confituurke <9595028+Confituurke@users.noreply.github.com>
2026-10-17 22:39:29 +02:00
Confituurke 3ccd27c80a feat(sensecap-indicator): FT6336 touch on the shared expander bus
The FT6336 sits at 0x48 on the same SDA39/SCL40 bus as the TCA9535,
and its reset is expander pin P0.7, so the driver takes
IndicatorIo::BusLock for every register write + read and pulses reset
through the expander. It implements the heltecV4CapTouch* interface
the touch UI already polls, with the same tap and swipe thresholds as
the Wio Tracker L2, so UITask needs no new touch path.

The controller reports both axes mirrored against the panel, so points
are mapped as 479 - raw and clamped; the raw point is kept for the
diagnostic overlay.

UITask calls begin on every loop pass until it succeeds. Pulsing reset
and waiting 300 ms on each call would starve loopTask when the
controller does not answer, so reset runs on the first probe only,
later probes run at most every 2 s, and the failure is logged once.
The chip and vendor ids are logged, not checked, so a compatible
controller with another id still works.

Signed-off-by: Confituurke <9595028+Confituurke@users.noreply.github.com>
2026-10-17 22:39:27 +02:00
Confituurke c2d50b5430 feat(sensecap-indicator): ST7701S RGB panel on LovyanGFX
IndicatorDisplay drives the 480x480 ST7701S through LovyanGFX's Bus_RGB
and Panel_ST7701 on the stock core; the CrowPanel 5.0 drives its RGB
panel through the same Bus_RGB. The panel's CS and RESET sit on the TCA9535, so LovyanGFX
gets pin_cs/pin_rst = -1 and begin() drives them through IndicatorIo:
reset pulse, CS low, lcd.init(), CS high. The bus lock is held from CS
low to CS high because the panel's 3-wire init shares GPIO41/48 with
the radio SPI, and no other task may move the radio's NSS meanwhile.

The second init list is Meshtastic device-ui's Indicator list (GPL-3.0,
credited in NOTICE): MADCTL 0x10 and SDIR 0x04 in BK0 flip both axes,
and the closing 0xFF,0xFF terminator keeps the colours from coming up
inverted.

The pixel clock is INDICATOR_PCLK_HZ, 12 MHz by default. The RGB bus
reads its frame buffer out of PSRAM continuously and this core has no
bounce buffers, so PSRAM bandwidth under Wi-Fi and flash traffic sets
the limit, not the panel; 12 MHz leaves margin for that, and the value
is a build flag.

The backlight is Light_PWM on GPIO45, LEDC channel 7; brightness 0
writes duty 0, so it goes dark. begin() returns false with the
backlight off when lcd.init() or any expander write in the sequence
fails, since the panel may then have missed its reset or init. UITask
keeps driving the backlight through the global display even when
begin() failed, so setBrightness() and turnOn() do nothing until the
panel has come up; otherwise the first brightness or wake request
would light a blank panel.

UITask: a 480 px draw band in internal DMA RAM (with LVGL told its full
size), because the RGB panel scans its frame buffer out of PSRAM all
the time and an internal band makes the flush a single PSRAM write. The
resolution is 480x480 with no UI rotation, and the board shares the
Wio L2's brightness, screen-sleep and no-keyboard-rotate branches.

Signed-off-by: Confituurke <9595028+Confituurke@users.noreply.github.com>
2026-10-17 22:39:25 +02:00
Confituurke d8200c35b3 feat(sensecap-indicator): board skeleton, TCA9535 expander and build target
Add the Seeed SenseCAP Indicator D1L as a new touch target. This step
builds and boots to a blank screen; the panel, touch and radio drivers
follow in their own commits so each can be reviewed against the
hardware it drives.

The SX1262's NSS, RESET, BUSY and DIO1, the LCD CS/reset and the touch
reset all sit on a TCA9535 at 0x20 on the SDA39/SCL40 bus, not on ESP32
GPIOs, so everything after this commit depends on the expander. The env
gives the radio lines virtual pin numbers 0x40..0x43 for a radio HAL to
map onto expander bits; until that HAL exists the radio is a plain
Module that links but cannot drive the chip.

The expander driver is header-only so a host test can cover it:
- its output and config shadows are seeded from the chip, because a
  warm reset or the previous firmware may have left pins driven and
  writing the power-on defaults back would glitch the radio, panel and
  RP2040 resets;
- a failed write rolls both shadows back to the last state written in
  full, and the next write brings the chip back in line with them;
- polarity inversion is cleared when a previous firmware left it set,
  since it would invert BUSY, DIO1 and the strap; it is written only
  when it is not already 0;
- every read of input port 0 is reported to an observer, so a DIO1 edge
  is not lost when another reader clears /INT.
A five-sample majority of the P1.3 strap selects the TCXO (2.4 V) or
crystal radio at run time, so one noisy read cannot pick the wrong
oscillator.

The expander, the touch driver and the display init serialise on one
recursive mutex over the shared Wire bus (IndicatorIo::BusLock). It is a function-local static, so the compiler
guards its creation when two tasks reach the first lock together.
IndicatorIo::begin() logs the registers it inherited and names the
step that failed, since a dead expander takes the panel and radio down
with it and is otherwise silent.

The board has 8 MB of flash. The partition table has one 4.5 MB
factory app slot, a 3.375 MB SPIFFS (as on the 16 MB boards) and a
64 KB coredump, with no map tile partition. The factory slot is larger
than the 0x3E0000 OTA slots of the 16 MB boards, so an image that fits
those fits here. CAP_OTA is off: with one app slot there is no in-place
update. CAP_BATTERY is 0: no battery is fitted.

Montserrat 18/20/24 for the UI-size presets are the generated faces; the
board joins their gate in ui_font_18/20/24.c, gen-touch-fonts.sh,
hwFitFont and luaHostFigureFont. LVGL is built at -O2 with the Arabic and newlib-memcpy
settings the other envs use.

run-host-tests.sh builds every test/test_*.cpp, so the new expander
test gets its include path there in the same commit.
build-upload-monitor.sh gets --sensecap-indicator.

Signed-off-by: Confituurke <9595028+Confituurke@users.noreply.github.com>
2026-10-17 22:39:16 +02:00
Kaj SchittecatandClaude Opus 5.5 5833e00d25 touch: beta_97 release notes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:18:07 +02:00
Kaj SchittecatandClaude Opus 5.5 18279edac9 release: the CrowPanel Advanced 10.1 (P4) ships in betas and the web flasher
- p4-release.sh builds and verifies the three revisions (V1.2, V1.1, V1.0)
  from crowpanel_p4_101/ in the same loop as the T-Display P4 images, with
  the same OTA-name and tag checks.
- Self-update is on (CAP_OTA 1): each revision updates from its own
  wadamesh-crowpanel-p4-101-v1x.bin, so a V1.0 never takes a V1.2 image.
- gen-flasher-meta writes a manifest per revision; wadamesh.com gets a
  CrowPanel Advanced 10.1 card with a V1.2 / V1.1 / V1.0 picker, and the
  standalone flasher one card per revision. Both point at the beta feed,
  so nothing shows until the next beta publishes the images.
- README, DEVICES.md and the site's board count (15).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:02:37 +02:00
Kaj Schittecat cff8bd4eea Merge crowpanel-p4-101: experimental Elecrow CrowPanel Advanced 10.1" ESP32-P4 port 2026-10-17 20:56:50 +02:00
Kaj SchittecatandClaude Opus 5.5 328434cbda touch: repeater status and telemetry requests go direct first (#683)
The Request button (and Ping) always reset the path, flooded a blank guest
LOGIN, and sent the STATUS/TELEMETRY REQ from inside the LOGIN-OK callback,
while the repeater's flooded login reply was still being repeated around us.
Bench against a repeater next to a T-Deck, 8 tries each: login-first got the
login answer 7 times but telemetry back only 2 times; a direct request like
the MeshCore app's got 8 of 8, about 1 s each (#683 reports under 5% from the
device, over 90% from the app on the same T-Deck).

Now the REQ goes out directly first on the stored path (flood when there is
none). Only when no reply comes within 6 s (12 s when it flooded) does the UI
fall back to the guest login, and after LOGIN-OK the REQ waits 2 s instead of
firing from the callback. Same bench: 7 of 8 answered (one through the
fallback, 17 s), and the login route alone went from 2 of 8 to 6 of 8. A reply
cancels the pending fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:54:39 +02:00
Kaj SchittecatandClaude Opus 5.5 e37258018a crowpanel_p4_101: experimental Elecrow CrowPanel Advanced 10.1" ESP32-P4 port
Standalone ESP-IDF target (crowpanel_p4_101/) modelled on tdisplay_p4/, sharing its
ESP-IDF, components and esp-hosted 2.12.3 backend. Untested on hardware.

- variants/crowpanel_p4_101: EK79007 MIPI-DSI 1024x600 display (Elecrow timings,
  LEDC backlight on GPIO31), GT911 touch on I2C0 45/46 (RST40/INT42), SX1262 on the
  J9/J11 header with a real DIO1 IRQ, LDO3/LDO4 bring-up, C6_EN reset callback on
  GPIO32, full-system-reset restart shim.
- Board revision is a build option: WADA_CP_REV=1.0|1.1|1.2 (default 1.2). V1.2 moves
  LoRa DIO1/NRST from GPIO53/54 to 27/28; V1.0 has the C6 SDIO data lines reversed
  (run 1-bit like Elecrow's V1.0 lesson). hostedSetPins() overrides Arduino's
  generic esp32p4 SDIO pin defaults, which only match V1.0.
- LVGL at native 1024x600 with the large-screen layouts; fixed landscape.
- Shared src/: one CAP_* block in device_caps.h plus HAS_CROWPANEL_P4_101 at the P4
  platform sites (storage law, SD_MMC, hosted BLE, no RTC_CNTL, no MQTT, fonts) and
  the CrowPanel no-unlock-control sites. navOpenChatPanel/navGoToMainTab get
  fallbacks for boards without CAP_KEYPAD_NAV.
- build.sh merge writes a 0x0 merged image per revision.

T-Display P4 still builds from the same tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:22:13 +02:00
Kaj SchittecatandClaude Opus 5.5 ed896b3404 touch: history never drops records at the segment cap; host test for the scheduler
fcb392a merged two neighbouring segments at the 24-file cap and, when no pair
fitted in one file, retired the oldest segment. A host simulation showed that
fallback does fire for heavy users (many quiet chats next to a few busy ones):
up to 626 old messages dropped over 60 reboots on 3 of 8 seeds. Now 24 is a
soft cap: at 24 pending compactions run first, then the best merge, and only
when no pair fits may a new segment open, up to a hard cap of 48. Below 41
files some adjacent pair of the 5000 ring records always fits, so the hard
cap never stops a save and nothing is dropped; files only pass 24 in that
heavy case, so older builds still read every file for almost everyone.

test/history_segments/run.sh lifts the scheduler out of UITask.cpp verbatim
and runs it against a simulated card: mixed, many-quiet, one-busy and
reboot-heavy workloads plus power cuts between a merged write and the
absorbed file's removal. 60k messages per run, every live record must
survive every reboot with no duplicates. beta_95's code loses ~48k of 60k in
the same run; this passes every mode (max 29 files).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:59:29 +02:00
Kaj SchittecatandClaude Opus 5.5 0351cf4410 apps: Store installs no longer leave a cut-off app behind
luaStoreDownloadWorker wrote each downloaded file to the card with one
f.write() straight from its PSRAM buffer, and an SD write from PSRAM can come
back short. Blackjack (48 KB, the first downloaded app past ~32 KB) landed cut
off near 32 KB: the install reported a network error, and the half .lua stayed
behind as an installed app that failed with "syntax error near <eof>"
(Discord). Files now go through the internal bounce-buffer writer into a .tmp,
are size-checked on the card and only then replace the real name, so a failed
install leaves nothing and a failed update keeps the old version.

wada.fs.write and the Home widget snapshot wrote from PSRAM the same way (Lua
strings live in the app's PSRAM heap); both go through a 2 KB internal buffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:32:41 +02:00
Kaj SchittecatandClaude Opus 5.5 94baf4deac touch: beta_96 - release notes, language store v27, Blackjack on the App Store page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:22:25 +02:00
Kaj SchittecatandClaude Opus 5.5 d900bb83aa fonts: bidi and zero-width marks stay invisible with the '*' fallback
#679 ends every font chain in a '*' placeholder. LVGL itself only gives
<0x20, F8FF and 200C zero width, so LRM/RLM (common in Arabic and Hebrew
messages), ZWSP, ZWJ, word joiner, the embeddings, isolates and the BOM would
each have drawn a '*'. They now resolve to an empty glyph with no advance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:22:25 +02:00
Kaj Schittecat e43c06b09a Merge PR #681: Blackjack 1.0 in the Store 2026-10-17 15:17:16 +02:00
Kaj Schittecat 39cbbea6b7 Merge PR #679: one '*' placeholder for missing glyphs, hide skin tones (#673) 2026-10-17 15:17:16 +02:00
Kaj Schittecat 6a17f2a836 Merge PR #676: lock screen PIN, the keyboard's Sym/Alt layer stays live (#675) 2026-10-17 15:17:16 +02:00
Kaj Schittecat d89185367d Merge PR #630: Touch UI design calls (P4 one-row bar, chat clock, Device page, Bluetooth at setup)
# Conflicts:
#	src/ui-touch/UITask.cpp
2026-10-17 15:17:16 +02:00
Kaj Schittecat 1e72667b97 Merge PR #680: Web page Up/Down scroll the text on keypad boards (#677) 2026-10-17 15:16:56 +02:00
Kaj SchittecatandClaude Opus 5.5 aa5de751a5 touch: chats list ticks, M9 header layout code, contact popup titles, lock wallpaper veil
- #670: the chats list kept one tick after a DM was delivered. Its change
  signature never included the delivery state, so the rebuild was skipped;
  a delivery now bumps a generation it mixes in (and marks threads dirty).
- #672: on the M9 the keyboard layout code (EN, BG) sat on top of the GPS
  glyph in the chat header. It now has its own slot left of the clock, the
  GPS glyph moves left of it and the title leaves room for both.
- #668: with Large Text a long node name wrapped in the contact popup and its
  second line ran under the buttons. The title gets a fixed one- or two-line
  box (two when the name needs it) and the card uses most of a 320 px screen
  (up to 300 wide). The scroll check also counts the nickname line now.
- #652: a custom lock wallpaper was veiled with the theme background at
  150/255, milky on Day and cloudy on Night. It gets a lighter black veil and
  light text whatever the theme, like a phone lock screen.
- [HIST] boot line: segments and records loaded, for field reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:15:24 +02:00
Kaj SchittecatandClaude Opus 5.5 9eda9b0f26 apps: Store lists up to 40 apps, canvases fit the big screens, button:bg
- The catalog parse stopped at 16 rows and the catalog has 16, so a 17th app
  (Blackjack, #681) was silently never listed; installed apps had the same
  cap. Both are 40 now, and the catalog buffer grows from 8 KB to 24 KB so
  the JSON is not cut off. PSRAM only.
- wada.ui.canvas raised a Lua error above 480 px, which killed apps that size
  a canvas from on_open(w, h) on the T-Display P4 (body ~575 tall), Tanmatsu
  and CrowPanel 5 (800 wide) (Discord). Sides are now clamped to the screen's
  long side instead.
- button:bg(fill [, border]) colours a button (or boxes a label), so apps no
  longer fake coloured buttons from panels that keys cannot focus (Discord).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:15:18 +02:00
Kaj SchittecatandClaude Opus 5.5 5198813f99 usb files: Lua apps can be dropped into the apps folder
/meshcomod/apps on the card and /apps internally now take top-level .lua and
.json files, so an app can be side-loaded without pulling the card (Discord).
Everything else there stays read-only: the folder itself, subfolders (<id>.d
data), app save files (.sav, .wgt), hidden names and 8.3 aliases. A change
from the page invalidates the cached app listing, so the drawer and the Store
pick the new app up the next time they open. Host tests cover the policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:54:47 +02:00
Kaj SchittecatandClaude Opus 5.5 fcb392afd0 touch: chat history kept saving at the 24-segment cap; open chats under a locked screen stay unread
History: at the segment table cap a new segment waits for the oldest to age
out of the RAM ring, but after a reboot the ring only holds what was loaded
(per-chat caps thin segments without emptying them), so nothing was ever
evicted and every new message stayed RAM-only until the next reboot lost it
(Discord, "messages are going away after Oct 13th"). segBuildJob now merges
the adjacent pair with the fewest live records into the older file (the
absorbed file is unlinked after the merged one lands; the loader's
monotonic-seq filter covers a crash in between), and retires the oldest
segment when no pair fits. A flush with records still pending no longer
reports "everything durable": it logs [HIST] flush stuck and retries.

Read state: refreshChatDetail marked the open chat read on every refresh, so
messages arriving under a dark or locked screen were cleared at once and the
lock screen's unread cards stayed empty (Discord). Only mark read while the
chat is really in view; wake, unlock and the glance closing re-run the check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:54:42 +02:00
Kaj SchittecatandClaude Opus 5.5 8daf59bdd7 doc: DOC_APPS_AUDIT capture tour for every Store app
Launches each catalog app (kAuditStore, generated by gen-radio-audit-apps.py
into the gitignored doc_radio_apps.h) and captures store_<id> for the
website's App Store page. Music gets two bench tracks and a play tap first.
Capture builds only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:54:31 +02:00
Kaj SchittecatandClaude Opus 5.5 2986354f3c site: Join the community (Discord) button next to Install
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:38:17 +02:00
Kaj SchittecatandClaude Opus 5.5 8109bbf43a site: App Store page listing every Store app, with the SDK and how to submit
apps.html is generated from deploy/apps/apps.json (the catalog the devices
read) plus the site-only write-ups in deploy/site/apps-info.json, by
scripts/build/gen-apps-page.py, which deploy-site.sh now runs on every
deploy. Linked from the home page (Apps button + resource card), the SDK
guide and the sitemap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:17:59 +02:00
halflive-743 bc32153c69 apps: add Blackjack 1.0 to the Store catalog 2026-10-16 22:06:13 -06:00
Michael A. CojocariandCopilot 7f29bccf76 docs: M9 Web page Up/Down reading (#677)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-16 21:02:59 -04:00
Michael A. CojocariandCopilot 3470c308d9 Web page: Up/Down scroll the text on keypad boards (#677)
Links were the page body's only focus targets, so spatial nav jumped from
a link to the next one however far down, or straight to the toolbar, and
the text in between could not be read on the M9 (or with a d-pad).

Up/Down on the Web page now move to the next link already on screen, else
scroll the body a page (handing focus to a link coming on screen when the
focused one scrolls off); only at the end of the page does focus leave for
the address row or toolbar. navMoveDir reports when the page took the press
so the M9 and Attaky "focus did not move" fallbacks don't scroll twice.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-16 20:23:34 -04:00
Michael A. CojocariandCopilot 77f6a04543 fix(ui): one '*' placeholder for missing glyphs, hide skin tones (#673)
Missing characters drew as '*' in labels that went through
copyUtf8ReplacingMissingGlyphs() and as LVGL's empty box everywhere else.
Every text chain (g_font_12/14/16 incl. the Tanmatsu accent slots, the
Pager map font, the glance body) now ends in a font that draws the base
Montserrat '*' for any codepoint nothing else has. It is flagged
is_placeholder, which LVGL still draws, so Constellation's name filter
keeps dropping undrawable characters.

Skin-tone modifiers U+1F3FB-1F3FF have no baked image; map them to the
invisible zero glyph (as U+FE0F is), so a toned emoji shows its base.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-16 19:52:47 -04:00
Michael A. CojocariandClaude Opus 5.5 9aa590f0c1 fix(touch): the keyboard's Sym/Alt layer stays live while a PIN is typed on the lock screen (#675)
Locked, the T-Deck loop switched the keyboard modifiers off, and the PIN
screen opens over the lock screen, still locked, so a PIN was typed with
Sym off. Sym+mic ('0') sent nothing, and Sym+W..O sent the letter under
the digit, which a PIN accepts, so the PIN silently came out wrong.
Setting the PIN in Settings worked because the device isn't locked there.

Now the modifiers stay on while the device is locked, lit and has a PIN
(the first key typed on the lock screen opens the PIN screen and counts).
The Pager-style keyboards (Pager, T-Deck Pro, Max) had the same rule for
a tapped Alt, cleared every tick while locked; the same exception
applies there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Us6CjkLFga9GkFJmmr3rrq
2026-10-15 14:55:28 -04:00
Michael A. CojocariandCopilot ea4c791270 i18n: regenerate the V4 table for the Device page strings
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:52 -04:00
Michael A. CojocariandCopilot 896c8e7030 Settings: the group is 'Display & lock', so 'Hardware' only names the Device page's card
docs.html follows: the group's name, Theme under it, and Run setup again
and Reboot on the Device page rather than General.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:25 -04:00
Michael A. CojocariandCopilot b9ff27de8c site: Settings > About is now Settings > Device
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:25 -04:00
Michael A. CojocariandCopilot e83c483a0f i18n: rows for the Settings > Device and keyboard protocol strings
Hardware, the setup wizard Bluetooth hint, Radio:/Keyboard: and the four
keyboard protocol states in all 14 languages, taken from #631 so they
ship with the code that uses them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:25 -04:00
Michael A. CojocariandCopilot a913b1331e Settings: one 'Device' page; keyboard protocol switch on Keyboard page
The settings group that held Display/Theme/Lock is now 'Hardware', so
'Device' only names the page that was About. Run setup again and Reboot
move from General to an untitled card on that page. The older keyboard
protocol switch is also on the Keyboard page instead of a note pointing
elsewhere.

Refs #416

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:14 -04:00
Michael A. CojocariandCopilot ec5e08038b P4 Bluetooth: the status-bar glyph shows when Bluetooth runs; setup applies only a flipped switch
- The round one-row bar hid the Bluetooth glyph outright, but the esp-hosted
  P4 build does run Bluetooth on the C6. The glyph now follows
  hasBleCapability() && isBleEnabled() as on every other board (so never on
  the ESP-AT build), with a slot in statusBarPackRound between Wi-Fi and GPS.
- The one-row repack signature includes the Bluetooth and GPS glyphs, so
  either appearing re-packs the row.
- Setup's Bluetooth switch shows Bluetooth as it is when step 3 opens, and
  Finish applies it only if the user flipped it. A switch left alone no
  longer tries to start a Bluetooth that could not start at boot, which
  showed the P4 its 'not enough memory' alert on first boot (#612).
- tdisplay_p4/main/main.cpp: drop the stale 'keep BLE gated OFF' comment.

Refs #416

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:14 -04:00
Michael A. CojocariandClaude Opus 5.5 6f2783c8c9 P4 landscape Settings: two real columns, Device row full width
The wide landing wrapped groups two to a row, so each row was as tall as
its taller group: Device (one row) sat beside Mesh with a gap under it,
Connections beside the Device group, and System alone at the bottom.

In two-column mode the untitled group holding the Device row now spans
both columns at the top, and the remaining groups stack in two column
containers, split in order at the point that makes the taller column
shortest. Value-slot width is tracked per row since the spanning row is
wider.

Unchanged.
(from 7d6875a)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XERzMfBu6rw4behJzs9Hmw
2026-10-15 14:15:14 -04:00
Michael A. CojocariandCopilot 43c80a1b0f fix(touch): chat header keeps the clock and battery at the bar's normal height
The taller chat bar centred them on its full height, so opening a chat
dropped them below where they sit on every other screen. The round P4 packs
them on the one-row bar's line; the double-bar boards lift them by half the
extra height (the Pager's bar does not grow, so it is unchanged).

Unchanged.
(from 5f9f5ee)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:14 -04:00
Michael A. CojocariandCopilot d4a8a09310 P4: one-row status bar
The round-cornered T-Display P4 drops its two-row bar for one row: the
title on the left, then the clock beside the icons on the right (battery,
signal, Wi-Fi, GPS, the DND moon), every glyph's ink on one bottom line.
No Bluetooth glyph (wadamesh does not drive the P4's Bluetooth) and no
battery %. The stacked chat header only shows while a chat is open.

Split out of 4844a9b (Touch UI follow-ups 4) and rebased onto beta_92,
whose GPS glyph joins the packed row.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-15 14:15:14 -04:00
Michael A. CojocariandClaude Opus 5.5 3e8bf7c51d fix(touch): first-boot setup keeps Bluetooth on and shows a switch for it (#416)
Since beb85bd a fresh install forced Bluetooth off and persisted it when
the setup wizard opened, and nothing on screen said so: the device was
invisible to the MeshCore app until the user found the control-center
toggle. A format + clean install hits exactly that path, which fits the
T-Deck Plus report (stock MeshCore, always advertising, paired fine).

Setup no longer forces Bluetooth off; it stays as it booted (on by
default, as with stock MeshCore). Step 3 "Wi-Fi & Bluetooth" gets a
Bluetooth switch, on for a first boot and showing the live state on
"Run setup again". Finish applies only a change, via the same
enableBle()/disableBle() path as the control center, and shows the usual
failure text if the heap guard refuses a cold start. Skip leaves
Bluetooth on.

The step's paragraph is shorter, so the switch row still fits above the
buttons on 240 px-tall panels. The new string is translated in all 14
languages; i18n_builtin.h regenerated.

(cherry picked from commit 700355a23c465faa2df3b2ec5d46f06dbb8d7873)

Translations are in the i18n PR.
(from 3e2b8b5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zE15egJdCZ9nRBZmg1YUq
2026-10-15 14:15:14 -04:00
Michael A. CojocariandClaude Opus 5.5 b4c6c20900 feat(tanmatsu): report the LoRa chip, so Device names a radio on every build (#596)
Every PlatformIO touch build and both T-Display P4 radio builds define
RADIO_CLASS, which the Device page already reads. The Tanmatsu has none:
its SX1262/SX1268 sits behind the ESP32-C6 and is reached through
TanmatsuLoraRadio. That bridge now asks the coprocessor once at init
(lora_get_status, before RX polling starts) and exposes chipName(), and
deviceRadioName() uses it, falling back to "SX126x" if the C6 did not
answer.

(cherry picked from commit 94056e7eb7baf136d0f17ffa04e7e48cbe9ac712)

Unchanged.
(from 53c1cd9)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zE15egJdCZ9nRBZmg1YUq
2026-10-15 14:15:14 -04:00
Michael A. CojocariandClaude Opus 5.5 07c92b2af8 feat(touch): Device shows the radio; info pages scroll by keypad, close with X (#596)
- The Device page lists the radio this build drives ("Radio: SX1262"),
  from RADIO_CLASS with the "Custom" prefix dropped. The Device card's
  second line shows it too on boards without the T-Deck keyboard, which
  is what tells the T-LoRa Pager's SX1262 and LR1121 builds apart.
- System Information and Memory detail close with a compact X instead
  of a "Close" button.
- Their text now scrolls from the keyboard: the X is the only focus
  stop, so Up/Down and the Pager's wheel found nothing to move to and
  the text was unreachable off-touch. A shared navInfoModalScroll()
  scrolls the body instead, from navMoveDir (trackball / d-pad / arrow
  keys) and the Pager encoder; the M9's own special case now uses it.
  Focus stays on the X at either edge.

"Radio:" translated in all 14 languages; i18n_builtin.h regenerated.

(cherry picked from commit 4b99579b4e58aa78d165204502c14263559c563e)

Translations are in the i18n PR.
(from e56c6ea)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zE15egJdCZ9nRBZmg1YUq
2026-10-15 14:15:14 -04:00
Michael A. CojocariandClaude Opus 5.5 671213771b feat(touch): Device section at the top of Settings, About merged in (#596)
Settings opens with a Device card that names the hardware on a second
line ("LilyGo T-Deck · newer keyboard"), so a wrongly detected or set
variant is visible before anyone opens a page. It replaces About, which
moves from the bottom of the list; its content is unchanged and now sits
under a new hardware block:

- Model, and on the T-Deck the keyboard the protocol probe settled on,
  or "set manually" when the override is on (new
  tdeckKeyboardDetectedMode() getter).
- The T-Deck "Older keyboard protocol" switch, moved here from the
  Keyboard page, which keeps a one-line pointer to it. Flipping it
  updates the card and the page in place.

The update-available dot moves to the Device card, and the "Diagnostics
are on the About page" hint now points at Device.

New strings are translated in all 14 languages; the moved switch's label
and note were English-only in 12 and are now translated too. The old
About rows stay for firmware that still asks for them. i18n_builtin.h
regenerated. The site docs now say Settings > Device.

Not included: a T-Deck Pro touchscreen override (V1.0 and V1.1 are
separate builds, so it cannot change at runtime; the card shows which
build is installed) and an inaccurate-RTC flag (new clock behaviour,
better as its own ticket).

Fixes #596

(cherry picked from commit 726905ea607dea496f5e74e90e7b13068552007e)

Rebased onto beta_92's card-based Settings: the hardware readout and the switch are a Hardware card on the Device page. Its translations are in the i18n PR.
(from ddcff4e)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zE15egJdCZ9nRBZmg1YUq
2026-10-15 14:15:14 -04:00
Kaj SchittecatandClaude Opus 5.5 b85eef6fec touch: beta_95, MeshCore 1.18 via meshcomod v1.18.0.1
About (Build) and the boot log name the meshcomod release the core is:
"on meshcomod v1.18.0.1". release.sh reads it from the pinned core's own
examples/companion_radio/MyMesh.h, keeps the in-tree defaults in step and checks
every staged image carries it. Language store v26 (this build's new texts),
release notes for beta_95.

Bench (T-Deck, 1.18): device and self info, 404-contact sync, the companion CLI
(ver, board, get name/radio/tx/freq, unknown command), 4 packets received and a
zero-hop advert sent in 90 s, correct device time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:07:34 +02:00
Kaj SchittecatandClaude Opus 5.5 ef0b27f1e0 core: MeshCore 1.18 (core-v1.18.0 = meshcomod 8aba438f), V4 language table deflated
Pins move from core-v1.17.4 to core-v1.18.0, cut from the main meshcomod
companion-v1.18.0.1 was released from. The companion changes are ported from
meshcomod's rebase into wadamesh's own MyMesh:

- onCLICommandRecv: a contact's remote CLI command runs when the user granted
  that contact remote CLI, and lands in the chat as a CLI message otherwise.
- CMD_RUN_CLI_COMMAND (66) / RESP_CODE_CLI_REPLY (29) and the local companion CLI
  (name, pin, Wi-Fi, radio, af/dutycycle, rxgain, tx, rxdelay, path.hash.mode,
  multi.acks, board, ver), also reachable from the serial CLI. Settings without a
  prefs field answer "unsupported". NodePrefs layout unchanged.
- TXT_TYPE_CLI_COMMAND is accepted for sending; sendCommandData (and our queue-
  preserving wrapper) carry the text type.
- Channel data is delivered to the app (onChannelDataRecv), unsolicited responses
  (pushed telemetry) reach it as PUSH_CODE_BINARY_RESPONSE, and raw-data sends
  decode multi-byte-hash paths.

Clock: 1.18 seeds an unsynced ESP32 clock at 1 Mar 2026, so ClockFloorRTC's
MIN_VALID_EPOCH follows it (a set equal to the seed must stay invalid).
Pager LR1121: the AGC reset re-applies the boosted-gain setting (new core arg).
IDF builds exclude the new CommonRadioPrefs.cpp (needs <target.h>/radio_driver).

The 2 MB Heltec V4 went to 100.0% with the CLI. Its built-in language table is now
one raw-deflated blob per language (895 KB -> 312 KB), inflated into PSRAM by the
S3 ROM on first use (~10 ms, ~75 KB for the active language): 91.7% flash. Checked
on a T-Deck forced onto the table (nl, hu, el) and by the post-link key check.

Builds: all 15 S3 touch envs, Tanmatsu (3,237,024 B), T-Display P4 SX1262 + LR2021.
T-Deck boots and runs on 1.18.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:50:32 +02:00
Kaj SchittecatandClaude Opus 5.5 d3a9cf1453 touch: radio tools redesign, Music app, Store update badge, triage fixes
Spectrum: stat tiles (peak, floor, our channel, busy), plot and waterfall drawn
at screen size (the lv_chart and zoomed waterfall were the page's render cost),
dark-to-yellow waterfall, peak hold, our-channel marker, Band/Channel and
Live/1 min/10 min chips. RF Monitor 1.4 (tiles, filled chart, typed feed, packet
detail card; the old type table did not match MeshCore payload types) and
Airtime 1.6 (big figure, tiles, duty line, rx/tx split).

Lua SDK: ui.panel, bold labels and a size-24 figure font, colors.warn/border,
app.on_audio (fires with the screen off), read-only wada.sd.list and sd: audio on
the SD_MMC boards (P4, Tanmatsu, Wio L2), caps panel/audio_events/epaper, a
"music" icon and an "audio" Store requirement. button:set() no longer writes
label fields into the button object (heap corruption, every earlier firmware).
The SDK page's input table described events the firmware never sent.

Music 1.0 (Store app): plays MP3/WAV from the card's Music folder, shuffle,
repeat, keeps going through the list with the screen off. Verified on the
T-Display P4 end to end.

Store: the update check also fetches the app catalog and badges the Store tile
with the number of installed apps that have a newer version. Baked Lua apps are
raw-deflated and inflated by the S3 ROM (14.6 KB back on the V4); the V4 language
table keeps only texts of apps it can run.

Fixes: the SD-card notice no longer fires when the card mounts fine (#667); the
Control Center clock keeps time while open (#666); M9 keyboard brightness (#664);
the battery widget's number no longer sits on a ring too small for it (Discord);
M9 shortcuts doc follows #669's composer order. New texts in all 14 languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:07:11 +02:00
Kaj Schittecat 3b0af92044 Merge pull request #665 from Ender436: T-Deck Max audio (chimes, custom sounds and Lua audio through the ES8311) 2026-10-15 15:28:35 +02:00
Kaj Schittecat 833fac8943 Merge pull request #669 from oumike: the composer puts the text box next to Quick Reply, Emoji (and the M9's #) beside Send 2026-10-15 15:17:45 +02:00
Michael A. CojocariandClaude Opus 5.5 d3202fe7fe feat(touch): the composer puts the text box next to Quick Reply, with Emoji (and the M9's #) beside Send
Non-touch M9: Quick Reply | text | Emoji | # | Send. Touch boards: Quick
Reply | text | Emoji | Send. Emoji and # align off Send's slot so they
follow the text box's new width on a rotation; the P4's landscape row
matches. The M9's arrow keys walk the new order: RIGHT from the end of
the text goes to Emoji, then #, then Send; LEFT walks back # → Emoji →
text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Us6CjkLFga9GkFJmmr3rrq
2026-10-15 09:06:21 -04:00
Ender436 3166e4a654 t-deck max audio fix
Notification chimes, custom WAV notification sounds and Lua wada.audio
playback through the ES8311 codec + NS4150B amp, mirroring LilyGo's
examples/ES8311/playWAV bring-up. I2S DOUT is GPIO40, not the header
name's 17 (measured in GlomarGadaffi/tdeck-max-phone#34); the XL9555
amp enable (line 6) is bracketed around playback and the shared-speaker
mux (line 10) held to the ES8311. Reuses the pager's Es8311Codec driver
and enables CAP_SOUND/CAP_SOUND_FILES/CAP_AUDIO_STREAM for the Max.

Signed-off-by: Ender436 <68835715+Ender436@users.noreply.github.com>
2026-10-14 11:37:08 -06:00
Kaj SchittecatandClaude Opus 5.5 75ec6759dd test: the M9 keyboard test follows #628's one-read-per-poll driver and checks its 70 ms repeat filter
#662's test queued three keys and read them all after one poll; #628 (for #625,
keys typing twice) reads one key per poll and drops the same key again within
70 ms. The test now polls per key and also checks that a repeat inside 70 ms is
dropped and one after it is a new press. Host tests: 18 pass, 2 skips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:34:12 +02:00
Kaj SchittecatandClaude Opus 5.5 179577ef10 beta_94 notes: CrowPanel 5.0, hashtag links, #628's fixes, the Radio widget line
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:33:33 +02:00
Kaj SchittecatandClaude Opus 5.5 0692775ba5 Merge follow-ups: the confirm X stays on touch boards (#628), link bubbles skip a full-screen layout (#580)
#628 dropped the close X from actions-only confirms on every CAP_KEYPAD_NAV board,
which includes the V4, the T-Deck and the touch-only boards that take a Bluetooth
keyboard. Now only a board without touch drops it; touch boards keep main's X,
skipped by keypad focus.

#580's chatCreateLinkLabel called lv_obj_update_layout on the message label to read
its height, laying out the whole screen for every link bubble as it scrolled in.
The caller already measured the wrapped text; it passes that height in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:17:57 +02:00
Kaj Schittecat 507511fab6 Merge PR #628 (oumike): Touch UI follow-ups 5a: quick fixes (#603, #605, #614, #617, #620, #625, #627, M9, Tanmatsu, Wio)
# Conflicts:
#	src/ui-touch/UITask.cpp
2026-10-14 10:17:19 +02:00
Kaj Schittecat ea4913f117 Merge PR #580 (TerryTel93): feat: add support for hashtag channels links with prefill and open fu… 2026-10-14 10:16:49 +02:00
Kaj Schittecat 806ecb0f71 Merge PR #655 (oumike): Add the Elecrow CrowPanel Advance 5.0 (#651) 2026-10-14 10:16:48 +02:00
Kaj SchittecatandClaude Opus 5.5 9f633d9d6d touch(home): no bottom bar on the Radio widget; a widget's bar never sits on its text
The Radio widget drew its airtime as a 5 px bar pinned to the bottom edge, which
on the usual widget height landed on the detail line (SF / BW) and looked like a
stray purple line through the text. Gone from Radio; and hwCardBuild now leaves
the bar out wherever it would not fit under the text (the Lua app widget's bar).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:15:55 +02:00
Michael A. CojocariandCopilot 31747c5477 Chat composer: name the text box's chrome height once
chatComposerChromeH() is the row padding plus the top row, used for the
text box height, the auto-grow and the one-line radius. A board-specific
inset (the P4's bottom lift in #611) then changes one helper instead of
four expressions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 09:37:44 -04:00
Michael A. CojocariandCopilot 3b58f58433 Review fixes for #628
- M9 shortcuts guide: the function row is Back, Ctrl and Triangle (no Mic).
- The X-last action lists, the bubble-only chat cursor and the lifted
  message-menu X are keypad behaviour, so they follow CAP_KEYPAD_NAV
  (Pager, Tanmatsu, T-Deck, ...) rather than HAS_THINKNODE_M9.
- Actions-only confirms (#603) drop the X on keypad boards only; touch-only
  boards keep it.
- M9 map buttons: focus keeps the accent fill but no local outline, so the
  shared keypad cursor is not outranked.
- M9 chat header: compare the bar's style height, not its stale coords.
- Contacts bulk delete ends with onThreadsChanged() so web clients refetch.
- The contacts selection buffer (~12 KB at MAX_CONTACTS) is allocated when
  select mode opens and freed when it closes, not at boot.
- Boot time sync: disconnect before the saved-network scan, so a first
  attempt still mid-handshake cannot fail it with ESP_ERR_WIFI_STATE.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 09:31:48 -04:00
Michael A. CojocariandClaude Opus 5.5 a5996faa41 M9: the chat header's clock, battery and GPS centre vertically
They sat on the title's line. They now centre on the header's two lines,
level with the back arrow and the avatar, so the whole header is centred
vertically. The step in from the rounded corner stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
2026-10-13 09:31:48 -04:00
Michael A. CojocariandClaude Opus 5.5 32c01f0682 M9: one key press enters one character (#625)
The keyboard controller latches a single key. When one was pending,
m9KeyboardPoll read the register up to three times back to back, a
"drain" meant to catch a second key, but with one slot a second read
can only return nothing or the same key again if the controller has not
cleared it yet. Keys came out twice, often and on any key.

- One read per poll, as the Launcher and Meshtastic M9 ports do.
- The same key reported again within 70 ms of the last accepted one is
  dropped as the same press. That covers a stale latch read on the next
  poll and the matrix bouncing on a slow, deliberate press, which the
  report found worse. Nobody presses one key twice that fast on purpose.
  The guard runs from the last accepted key, so bounces cannot stretch it.

Fixes #625

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
2026-10-13 09:31:48 -04:00
Michael A. CojocariandClaude Opus 5.5 7f025ffbf5 M9: the conversation header fits its text and lines up (#627)
The M9's UI sizes grow the fonts but not the geometry, so the header's
title and subtitle, centred in a bar of fixed height, clipped the
subtitle ("3 nodes heard today"). The back arrow and avatar sat low, the
clock and battery sat mid-bar while the GPS glyph kept the top row, and
the message list started several px below the header, leaving a strip of
bare background.

- m9ChatHeadH() gives the header's height from its fonts: the two lines
  from 3 px down, 2 px under them. The bar takes that height in a
  conversation, and makeChatDetail starts the list's content at the same
  line. The list is virtualised and places its rows against that padding
  once, so it is set when the list is built.
- The back arrow and avatar centre on the two lines.
- The clock, battery and GPS glyph share the title's line, with the GPS
  just left of the clock. The group steps in from the panel's rounded
  corner, which clipped the battery, and the title stays short of it.
- Leaving the conversation puts the bar's height, the battery and the
  GPS glyph back.

Other boards are unchanged.

Fixes #627

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
2026-10-13 09:31:38 -04:00
Michael A. CojocariandClaude Opus 5.5 14589f2c6e Status bar: the smoked-glass shade is not shifted with the icons
In a tall bar, updateGlobalStatusBar shifts each child of the bar up half
a row to put the system icons on the top row. It skipped the full-bar
backdrops (fade, dim) but not the shade drawn over a background picture,
so the shade sat half a row too high. The bottom of the bar showed the
picture unshaded, a strip over the top of the content below it. Measured
on the M9's conversation header: bar y0-38, shade y-11-27.

The shade is skipped like the other backdrops. All boards with a
background picture and a tall bar (conversations, settings pages).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
2026-10-13 09:31:38 -04:00
Michael A. CojocariandCopilot b247fa3ace Wio Tracker L2: Clock settings and the cold-boot time sync
The Wio Tracker L2 has no battery-backed clock, so it gets the same
opt-in saved-Wi-Fi time sync at power-on as the T-Deck
(CAP_BOOT_TIME_SYNC / BootTimeSync).

Split out of 4844a9b (Touch UI follow-ups 4).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 09:31:38 -04:00
Michael A. CojocariandCopilot 9d7da2354d M9 chat: # and emoji move left of the message input
The composer reads QR | # | emoji | text | Send. With the caret at the
start of the input, LEFT walks to emoji, then #; RIGHT walks back.

Split out of 4844a9b (Touch UI follow-ups 4).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 09:31:38 -04:00
Michael A. CojocariandClaude Opus 5.5 10e0f84b35 Channel region scope on phone-app channel sends too (#614)
CMD_SEND_CHANNEL_TXT_MSG called sendGroupMessage directly, so a post from
the phone app on a scoped channel went out on the default scope. It now
pushes the channel's scope for the send and pops it afterwards, as the
on-device composer, web remote, terminal and Lua sends do. Touch builds
only, where per-channel scopes are stored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
2026-10-13 09:31:38 -04:00
Michael A. CojocariandClaude Opus 5.5 94a7c13ef5 M9: name the 0x88 key by its triangle; it no longer opens chat options
The key is printed with a triangle, not a microphone, so the docs and the
code now call it the triangle key (M9_KEY_TRIANGLE). In an open chat it
used to open the thread's options; keypad navigation reaches the
conversation header for that now (#623), so the key keeps only its other
job, the focused item's hold action. M9_PORT.md no longer calls it
unbound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
2026-10-13 09:31:38 -04:00
Michael A. Cojocari b63b4f3904 M9 and Tanmatsu feedback from #609
- M9: double-press Home opens the launcher settings (what Home opens, icon size)
- M9: "Mark all as read" is the first item in the chats menu
- M9: MIC key opens the chat options in a chat, else the focused item's hold action
- M9: stronger focus on the map overlay buttons
- M9: the at-a-glance card shows even when the message-flash wake lit the screen first
- Tanmatsu: screensaver preview ignores input for its first 1.2 s so it no longer ends at once

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit f479382c9b12070cffae530e3b4789d689c441c5)
2026-10-13 09:31:38 -04:00
Michael A. Cojocari 1700366f3f M9: white tab bar while the map loads, double-press Home for Settings, bubble-only chat cursor
- Map: the transparent tab bar shows white icons from opening the Map tab until
  its first tile render finishes, then returns to its map colours.
- Home: a second press within 450 ms opens Settings. The keyboard controller
  sends no key-up or long-press code for Home, so a hold can't be detected.
- Chat: the keypad cursor surrounds the message bubble only, not the info
  line under it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 9523475232aa1a73e2c7c3395888407dfd4020c8)
2026-10-13 09:31:38 -04:00
Michael A. Cojocari 7f4d3fd0b6 M9: action menus focus first row (X last); lift message-menu X clear of buttons
The ⋯ / thread action list creates its close X after the rows on the M9, so
keypad focus opens on the first action and reaches the X last. The message
action menu's X moves up into the card padding so the first button row no
longer clips its focus cursor. Other boards unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 8fdb23ce5f5dbf58514304ab3be7a5dd04c4cdd8)
2026-10-13 09:31:37 -04:00
Michael A. Cojocari 2964ff02e3 fix(time): cold-boot Wi-Fi time sync gets past a slow first join (#620)
The first attempt on the active network brings the radio up from cold and
searches every channel for the AP, inside the same 4 s as every other
attempt. A T-Deck that joins in about 7 s by hand never made it, and the
scan retry then skipped that network as already tried, so a home with one
saved network always ended "no association".

The first attempt now gets 8 s. The scan retry keeps the active network
and joins each candidate on the channel and BSSID the scan found it on,
which skips the all-channel search. The whole session is capped at 16 s
(was 12 s) so the retry and SNTP still fit.

Fixes #620

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
(cherry picked from commit 477e0163ccf64d65706737a9558f1ef9ab439539)
2026-10-13 09:31:37 -04:00
Michael A. Cojocari 9bbe0a468b Chat: compact rows fit emoji; composer keeps the newest message in view and its text inside the box (#617)
- Compact rows that carry a colour emoji get its overhang above the text
  line as extra top padding, measured into the row height so the
  virtualizer agrees (chatCompactEmojiLift).
- When the composer grows while the list is at the bottom, the list is
  scrolled back to the newest message instead of being covered.
- The composer's corner radius stays at its one-line size, so a four-line
  box no longer curves through the start and end of its lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkSqsocfpbhCD2AtpskvQo
(cherry picked from commit 867db9917e682ac24f2542ecd77224fcf27f232a)
2026-10-13 09:31:37 -04:00
Michael A. Cojocari 2463bc238b Channel region scope on web remote, terminal and Lua sends (#614)
The chat composer tagged a channel post with the channel's own region scope,
but termDoSend (web remote and terminal) and luaHostMeshSendChannel called
sendGroupMessage directly, so those posts went out on the default scope.
All three now go through sendGroupMessageScoped, which pushes the channel's
scope for the send and pops it afterwards.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit fa1ec9868fdee31922ee57a5ed0a940522bb86c1)
2026-10-13 09:31:37 -04:00
Michael A. Cojocari ae473ee86d Contacts: A-Z sorts by name only; Select all covers every contact (#605)
- Name (A-Z) no longer sinks "?" (never heard) contacts below the heard
  ones; that rule now applies to the Last heard / Last message modes
  only. Favourites with and without a last-heard sort together by name.
- Select all selected at most 128 contacts, in storage order, while the
  list shows the first 128 in sorted order. Past 128 contacts it picked
  mostly rows that were not on screen, so nothing visible was ticked and
  a delete seemed to do nothing. The selection is now sized to
  MAX_CONTACTS and Select all takes every non-favourite that passes the
  filter.
- Bulk delete removes without saving (uiRemoveContactNoSave) and writes
  /contacts3 once at the end, keeps only the public keys for the run,
  and matches the selection by binary search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkSqsocfpbhCD2AtpskvQo
(cherry picked from commit f30ca794039ff11358903938d70e402019908776)
2026-10-13 09:31:37 -04:00
Michael A. Cojocari 477b7d5981 fix(touch): backup Import dialog opens on Import, loops Import/Cancel, no X (#603)
On the T-LoRa Pager the "Import this backup?" confirm started on the
close X, then stopped on the message text before reaching the buttons:
keypad navigation follows the card's tree order (X, message, Cancel,
Import), and confirmBackupImport() used none of showConfirm()'s options
for that.

- confirmBackupImport() now passes actions_only_nav (the message box is
  not a focus stop) and focus_confirm (focus opens on Import). With two
  stops, the group's wrap loops Import <-> Cancel in both directions.
- showConfirm() no longer creates the X at all for actions_only_nav
  dialogs, rather than only skipping it in navigation. Cancel and the
  Back key already dismiss them. The Purge-discovered dialog uses the
  same flag and loses its X too, which keeps the two consistent.

Fixes #603

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016zE15egJdCZ9nRBZmg1YUq
(cherry picked from commit 1df7b93ef91d5ca3790271e3d216409988cabf1c)
(cherry picked from commit 29a842b4a2de8bb2ff8a849b14d57955d17b2729)
2026-10-13 09:31:37 -04:00
Michael A. CojocariandCopilot 1826e0f74c DEVICES.md: the CrowPanel 5 recovers from long flash erases (full install, OTA)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 09:09:54 -04:00
Michael A. CojocariandCopilot 320e5cb93b CrowPanel 5: zero chat_back's shift in the chat header; docs say what was tested
chatHeaderApply now resets chat_back's translate_y, which updateGlobalStatusBar
skips in a chat, so a chat opened from a settings or app page no longer keeps
the half-bar upward shift. DEVICES.md and the site describe the hardware
testing, and the site counts 14 boards.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-13 09:01:59 -04:00
Michael A. Cojocari b0960cdcf1 Merge remote-tracking branch 'upstream/main' into crowpanel-50
# Conflicts:
#	scripts/build/gen-touch-fonts.sh
#	src/ui-touch/ui_font_18.c
#	src/ui-touch/ui_font_20.c
#	src/ui-touch/ui_font_24.c
2026-10-13 08:59:08 -04:00
Terry Carr 9fc158499b fix: conditionally include bubbleUrlTapCb for TDECK_PRO only 2026-10-13 12:02:39 +01:00
Terry Carr 7e539c3373 Merge branch 'ALLFATHER-BV:main' into hash-channels-links 2026-10-13 11:38:23 +01:00
Terry Carr 5ac174b443 fix: refactor chat link choices management to use dynamic allocation 2026-10-13 11:20:21 +01:00
Kaj SchittecatandClaude Opus 5.5 6b6253c242 storage: the SD-fallback notice opens above the app drawer
It was shown from setupWizardMaybeOpen(), before the drawer (the default Home)
was built on the same top layer, so the drawer covered it. It now opens after
the drawer, in place of the crash-report prompt for that boot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:17:30 +02:00
Terry Carr 7d283c6ae7 fix: adjust label long mode and width in chat link picker 2026-10-13 11:11:16 +01:00
Kaj SchittecatandClaude Opus 5.5 88dc3894e4 i18n: regenerate the V4 table for the new strings
The pre-build hook picked up the track, SD and Reader strings (8 translated keys x 14).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:03:56 +02:00
Kaj SchittecatandClaude Opus 5.5 a99a582bd4 i18n + notes: the track, SD and Reader strings; beta_94 notes for the three features
Short strings in all 14 languages; the long SD-fallback and track-stopped texts
as English placeholders until translated. v25 snapshot kept equal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:49:29 +02:00
Kaj SchittecatandClaude Opus 5.5 bec56ba814 reader: page through big text files, open card documents from Files, and a tool to fill a card
Asked for on Discord (prepper use: books and documentation offline on the card).

- The Reader opens a .txt or .md file on the card as a 16 KB window at an
  offset carried in the URL (sd:/x.txt?o=49152), cut back to a line (or a whole
  UTF-8 character), with Next page / Previous page / Back to the start links and
  the position in percent. A book of any size pages through; Back works as usual.
- Files opens HTML on the card in the Reader (its links work there, so a folder of
  pages browses offline) and text or markdown too big for the 8 KB editor; small
  text files keep the editor.
- scripts/sdpack.py (standard library only): an EPUB becomes /library/<name>/ with
  one page per chapter (long chapters split under 30 KB, linked in order) and a
  contents page; .txt/.md are copied as they are; /home.htm lists everything, so
  the Reader, which opens sd:/home.htm, starts on the library. Tested with a
  generated EPUB.
ZIM (Wikipedia) files are not read on the device: their clusters are zstd/xz
blobs of 1-2 MB and the files run to tens of GB on FAT32; converting on a
computer keeps the device side small.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:48:26 +02:00
Kaj SchittecatandClaude Opus 5.5 d0b1f58024 storage: a failed SD card no longer costs the node its key; the boot says what happened
Reported on Discord: a T-Deck that kept its data on a card that died booted
into first-time setup with a NEW private key, and the next good card was then
given the new key, so the old one was gone for good.

- DataStore keeps a copy of the main identity in NVS (namespace mc_id, 96 B):
  every save writes it, a load with no identity file falls back to it and puts
  it back into the active store, and a successful load seeds it when missing. A
  different copy is never replaced silently; only an explicit save changes it.
  Verified on a T-Deck: one boot with the card seeded mc_id.
- When the SD store was chosen but the card did not mount, the boot is flagged
  (g_sd_fallback_boot) and, instead of the setup wizard (whose done-mark is on
  the card too), a dialog says the card could not be read, that the key is kept
  (or, on a device that never had a copy, that it started with a new one while
  the card keeps the old), and offers Try the card again.
The retry ladder at lower SPI clocks already existed. Not yet done: copying the
data off a card that mounts but refuses writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:44:58 +02:00
Kaj SchittecatandClaude Opus 5.5 fd8a163421 touch(map): record a GPS track to a GPX file (Map options > Record track)
Asked for on Discord. A switch in the map options records where you go into
/meshcomod/tracks/<date-time>.gpx on the SD card, a standard GPX 1.1 track that
other mapping software opens, with elevation and time per point.

- Runs from the UI loop beside the GPS saver, so it keeps recording with the
  screen off; the GPS stays awake while it records (gpsWantedLive).
- A point once we have moved 10 m (GPS jitter stays out of the distance), or
  every five minutes after at least 3 m. Climb counts steps of 3 m or more.
- The file is complete after every point: the closing tags are rewritten behind
  each new one (r+ and seek), so a flat battery still leaves a file that opens.
- The map draws this session's track as a red line (one lv_line, thinned to 256
  points) and the status corner shows the distance and climb while recording.
- Three failed writes in a row stop it with a message; no card, no start.
Boards with an SD card and GPS (CAP_SD or the Pager). The app route was ruled out:
Lua apps stop ticking with the screen off and their files cap at 32 KB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:39:24 +02:00
Kaj SchittecatandClaude Opus 5.5 459be25e34 beta_94 notes: this round; MECP gets the bell icon
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:20:12 +02:00
Kaj SchittecatandClaude Opus 5.5 083d422a2d i18n: the V4 bakes the live translations, trimmed to keys the firmware can show
beta_93 pinned the Heltec V4 to the beta_92 .lang files because the full table
no longer fit. lang_reach.py now works out which keys any source can look up:
a .lang key is kept when it equals a C string literal anywhere in the sources
(every #if branch, adjacent literals joined, LV_SYMBOL_*/UI_ICON_* and other
string macros expanded, escapes decoded as GCC does), or that literal with TR()'s
glyph prefix stripped, or a string in any Lua app. 1818 of 1925 keys survive; the
107 dropped look unused (old dropdown strings, texts the source has reworded).

The trimmed live table is 15.5 KB smaller than the pinned one (V4 4,025,365 B
used, 37.9 KB free) and carries every translation added since beta_92.

Guards: the generator refuses to trim when an input is missing (lv_symbol_def.h,
UITask.cpp, or under 80% kept) and leaves the header alone; after linking the V4,
--check-objects reads the strings the compiler emitted and fails the build if a
translated key among them was dropped (proved by dropping a live one). The
pre-build hook regenerates on source changes too, and the header is only
rewritten when its content changes. The pinned copy is gone (git history).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:20:12 +02:00
Kaj SchittecatandClaude Opus 5.5 6ff2cdedd8 i18n: rows for the Bluetooth failure messages
"The Bluetooth chip is not responding." and "Bluetooth could not start.
Restart the device and try again." in all 14 languages, v25 snapshot kept equal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:20:12 +02:00
Kaj SchittecatandClaude Opus 5.5 cd70c91465 p4: Bluetooth starts from the toggles again; failures say what went wrong (#612)
Since beta_88 (b52c71f) the P4 stored its BLE name and PIN only when BLE was on
at boot and the C6 came up for it. A unit that booted with BLE off (or once had
it off, since that is saved) then failed every enable at once, and the UI read
every refusal as "Not enough free memory for Bluetooth", Wi-Fi off or not.

- main.cpp stores the parameters on every boot; storing them does not touch the C6.
- enableBle() brings the C6's BLE controller up on a runtime cold start
  (hostedInitBLE) before NimBLE, or NimBLE would start against no controller.
- The transport records why a cold start did not happen (no parameters, the
  memory guard, the radio) and the toast says so; only the guard is about memory.

Not run on a P4 yet: the P4 build has both strings in it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:20:12 +02:00
Kaj SchittecatandClaude Opus 5.5 821f9b2c5a apps: a Store app can ship its own files
An app's manifest can list files beside its code as one quoted string,
"files":"beep.wav,track.mp3" (the manifest reader takes strings only). The
Store installs each into /apps/<id>.d/, the folder wada.fs and
wada.audio.play("beep.wav") already read, so an app can carry its sounds. On
the T-Display P4, whose Lua SD API is off, that was the only way to get a sound
there (#lua-app-dev). Files stream to storage through a 4 KB internal buffer
(SD writes from PSRAM come back short), the server must send a length, a
short read deletes the partial file and fails the install. Up to 8 files of
4 MB, names as wada.audio's. No app uses it yet, so existing installs are
unchanged. SDK page documents it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:20:12 +02:00
Kaj SchittecatandClaude Opus 5.5 420ce5298d touch(cc): T-Deck Pro and Max control panel tiles stay visible on e-paper (#659)
Every Settings-group hue thresholds to white on the 1-bit panel, so an off tile
was white on white and its icon vanished, and an on tile was a bare icon. On
HAS_TDECK_PRO the tile is drawn in ink only: an outlined square with a black
icon when off, solid black with a white icon when on (the fix oumike proposed
in the issue). Not yet seen on a Pro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:20:02 +02:00
Terry Carr 7517b02f9c fix: revert back to center button 2026-10-13 10:14:22 +01:00
Kaj SchittecatandClaude Opus 5.5 0394fefb22 apps: MECP 1.0, write and read Mesh Emergency Communication Protocol messages
Asked for on Discord. MECP (github.com/xiang-dev-1/MECP) codes an emergency
or everyday message as MECP/<severity>/<codes> [extras], so every reader with
a code table sees it in their own language. Compose: severity, codes from the
12 categories, head count and GPS position, a preview of the exact text, then
a channel post (MAYDAY asks first). Read: MECP lines heard while the app is
open are listed in words, a MAYDAY pops a toast, and Decode takes typed text.
English code table from MECP's languages/en.json (CC BY 4.0, credited); the
protocol is CC0. Needs sdk_ext; seed false, so not in the V4 image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:56:36 +02:00
Kaj SchittecatandClaude Opus 5.5 6504135c16 touch(home): the Advert widget is laid out like the app tiles (#637)
The solid accent button from the first fix stood out on Home. It now uses the
App shortcut tile's layout: a tinted chip in the app drawer's colour for
Advertise, the name under it in the same 12 px font and position as Apps and
Discover, and flood/nearby under that only where the tile is tall enough. The
box keeps the default widget style (glass or card), so it no longer turns into
the see-through gradient that hid its text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:49:45 +02:00
Kaj SchittecatandClaude Opus 5.5 a7a286b4e7 beta_94 prep: release notes, guide entries, language store v25
Notes for beta_94; the guide mentions the map's Favorites only and the
Fahrenheit option; the 14 languages move to v25 with their snapshot
(deploy/apps/lang/25/), for deploy-apps.sh at the slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:58:49 +02:00
Kaj SchittecatandClaude Opus 5.5 6c6fb32d8d i18n: rows for the beta_94 strings
Temperature in Fahrenheit, Up to 15 characters and Favorites only in all 14
languages; the full-Home toast as an English placeholder like the other Home
widget strings. The V4 keeps its pinned beta_92 table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:24:59 +02:00
Kaj SchittecatandClaude Opus 5.5 3b47e089b3 touch: temperatures in Fahrenheit, as an option
"Temperature in Fahrenheit" next to "Distance in miles" (Settings > Display >
Status bar & units), asked for on Discord. Only what is shown converts: the
sensor text, Sensors widget and page, remote sensor widget, telemetry replies
and chart (its axis, shared with humidity, already spans -20..100). Stored
readings, logs, telemetry sent out and the Lua SDK stay in degrees C. Saved as
a one-byte blob (temp_f), so no settings-schema bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:24:59 +02:00
Kaj SchittecatandClaude Opus 5.5 0b409b1f3c touch(admin): the login password field says it takes up to 15 characters
MeshCore carries at most 15: the core's sendLogin() cuts there and stock
servers store 15, on the phone app too. A longer password is used as its first
15 characters, which the empty field never said (Discord).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:24:59 +02:00
Kaj SchittecatandClaude Opus 5.5 57d1840df6 touch(home): the radar fits a two-cell slot; a full Home says how to make room
The radar was 2x2 only, and the default Glance layout fills all twelve cells
of the T-Deck grid, so adding it (or anything) only flashed "No room for it
on Home" (Discord). The radar now also comes in the two-across size (it draws
into the shorter side), and when no cell at all is free the toast says to hold
a widget to remove one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:24:59 +02:00
Kaj SchittecatandClaude Opus 5.5 46279eef92 touch(chats): a text symbol picked as a chat icon is drawn (#647)
The icon picker's last rows are text symbols (a star, a heart, a currency
sign). They were saved and announced as "Chat icon set", but the avatar only
draws colour-emoji images, so the plain avatar stayed. Now a pick with no
colour glyph is drawn as text in the colour the initials had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:24:59 +02:00
CyrioandClaude Opus 5.5 6ee9424ece i18n: wording nits from review (nl, de, hu)
jcyrio's follow-up to #632 (ea2315e), pushed after the merge so it never
reached main. Only the .lang rows; i18n_builtin.h is generated and the V4
builds from its pinned copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:18:50 +02:00
Kaj SchittecatandClaude Opus 5.5 1130ed62e3 Merge follow-ups: Pager shortcut table says Nothing, built-in language table back to the V4 pin
#624's new Special keys row ended in an em dash; the other rows say Nothing.
#635 edited the generated i18n_builtin.h; regenerated from the pinned
beta_92 .lang copy the V4 builds from, so the V4 table is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:18:37 +02:00
Kaj Schittecat 133df4ca7f Merge PR #635 (Confituurke): feat(ui): pick your advert position on the map 2026-10-13 09:18:20 +02:00
Kaj Schittecat 01182d1be1 Merge PR #624 (jcyrio): Pager: Fn+V pastes the clipboard (no touch path to the edit menu) 2026-10-13 09:18:18 +02:00
Kaj Schittecat 4e73bcc34a Merge PR #645 (oumike): T-Deck Pro / Max: Normal, Large and Huge text sizes, as on the T-Deck (#639) 2026-10-13 09:18:17 +02:00
Kaj Schittecat f817646932 Merge PR #658 (oumike): Day mode: readable figures on the count badges (#657) 2026-10-13 09:18:15 +02:00
Kaj Schittecat 449b8abb67 Merge PR #649 (oumike): Contacts: show the chat icon, picked or the first emoji in the name (#646) 2026-10-13 09:18:14 +02:00
Kaj Schittecat 18882e975a Merge PR #650 (oumike): web app: add Reply to channel message long-press (#642) 2026-10-13 09:18:13 +02:00
Kaj Schittecat 016983e4b9 Merge PR #656 (oumike): Keyboard: readable labels on the control keys 2026-10-13 09:18:12 +02:00
Kaj SchittecatandClaude Opus 5.5 2d5e1b25ad touch(home): the Advert widget is a solid button again (#637)
hwAdvertBuild recoloured only bg_color. On glass (the aurora default) the box
is a translucent gradient into the panel colour, and the widget's dark
ON_GLOW text vanished on it. Gradient off, opacity full.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:18:05 +02:00
Kaj SchittecatandClaude Opus 5.5 793660f3fd touch(map): "Favorites only" in the map options
A switch under "Direct (0-hop) only" that leaves only favourite contacts on
the map (markers and link lines), asked for on Discord. Saved as a one-byte
blob (map_fav), so no settings-schema bump. Both filters now go through one
mapHidesContact() check, and the favourites list is read once per render
instead of a blob read per contact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:18:05 +02:00
Kaj SchittecatandClaude Opus 5.5 c8dc7f486e touch(home): the radar widget's tap opens the screensaver the way Settings' preview does
hwRadarTap called ssaverStart() directly, which never marked the screen off,
so loop() tore the saver down again on its next pass: a tap only flickered it
and dipped the backlight to 40% for a frame. It now sets the same preview
request as Settings, which starts it and marks the screen off, so it stays
until a touch ends it. ssaverStart() also refuses a second start, so the
dimmed 40% can never be saved as the user's brightness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:18:05 +02:00
Pratik Patel 8dbb349fc6 thinknode_m9: pick GPS enable polarity from the board revision
The GPS enable pin (GPIO11) is active-LOW on the original ThinkNode-M9 but
active-HIGH on the newer revision (keyboard controller at 0x6D, e.g. Elecrow's
Meshtastic SKU LMM15409D2). Driven LOW on those units the GPS never gets a
fix: on one unit, same spot, 0 satellites with EN active-LOW vs 15 with
active-HIGH, and 34 after 20 minutes outdoors with the GPS toggled off and on
again from the UI. Meshtastic's ELECROW-ThinkNode-M9 variant makes the same
split, keyed on the same keyboard address (m9Version > 1 ? HIGH : LOW), and
Elecrow engineers have confirmed the split.

WadaNmeaLocationProvider: keep the enable level in a member that defaults to
the compile-time GPS_EN_ACTIVE, add setEnActive() to override it at runtime
(re-driving the pin so a running GPS stays on), and an optional
setPostEnableHook() run at the end of begin(). Every other board is unaffected.

thinknode_m9: m9KeyboardAddr() and a one-shot found-callback on the keyboard
driver; target.cpp uses it, when the controller is at 0x6D, to call
gps.setEnActive(true) and to register a GPIO10 wake pulse (50 ms, HIGH 5 ms,
LOW, 50 ms) after enabling, as Elecrow engineers confirmed. Any other address
keeps the existing default (LOW, no pulse), so the 0x6C path is unchanged; it
is not tested here, no 0x6C unit was available.

Depends on the keyboard 0x6D probe change.

Signed-off-by: Pratik Patel <Pratik@MakerNova.io>
2026-10-13 08:22:55 +02:00
Kaj Schittecat 9cd10dfe30 Merge PR #662 (Bfindlay): detect the ThinkNode M9 V1.1 keyboard at 0x6D (#644) 2026-10-13 08:22:30 +02:00
bfindlay 7025f79b30 no-mistakes(lint): Guard host-test runner against failed directory changes 2026-10-13 15:00:33 +11:00
bfindlay 04d8a606c5 no-mistakes(document): Consolidate M9 keyboard address documentation 2026-10-13 14:59:30 +11:00
bfindlay ad13d42a12 Fix M9 keyboard detection at the V1.1 I2C address
Signed-off-by: bfindlay <bfindlay@acm.org>
2026-10-13 14:43:28 +11:00
Michael A. CojocariandCopilot d94273bde3 Badges: paper-white with a black ring on the T-Deck Pro (#657)
The Day glow fill thresholds to solid black on e-paper, and the 11 px
anti-aliased white figures mostly fall under the one-bit cut, so every
count badge read as a black blob. On the Pro the tab-bar/launcher pills,
the Chats badge and the Discovered-nodes badge now draw white with a 1 px
black ring and black figures, like the Pro's avatars and buttons.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 19:39:51 -04:00
Michael A. CojocariandCopilot 0cba7ce037 Badges: figures in the glow's ink, readable in Day (#657)
The unread and Discovered count badges hard-coded Night's dark ink
(0x00201C) on the glow fill. Day's glow is the accent darkened, so the
figures read at 1.6-2.4:1 on every preset accent. Use COLOR_ON_GLOW,
which the theme already sets per mode and look (white in Day, the
look's own ink for Neon and Pastel). Under MORE COLORS! the chat-row
pill takes the row's hue, so its figures pick their ink by that hue's
luma, as Taste the rainbow's drifting badge already does.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 18:22:33 -04:00
Michael A. CojocariandCopilot 11dd04c6b7 Keyboard: theme text colour on the control keys
The control keys (1#, ABC, backspace, enter, the arrows, the tick) are
CHECKED keys. The stock theme's CHECKED key style carries its light-mode
text colour, near black, and a CHECKED style outranks the plain key text
colour, so their labels were dark on every theme. Set the theme text
colour on our CHECKED key style as well.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 11:10:23 -04:00
Michael A. CojocariandCopilot 0a7a3714f3 CrowPanel 5: pause the LCD clock during flash writes by default
Tested on hardware: while sending messages, the sideways jump during flash
writes is now a brief freeze or flicker, with no crash and no panel self-test.
The crowpanel_50 env now sets CROWPANEL50_FLASH_PAUSE and wraps
esp_flash_write and esp_flash_erase_region. DEVICES.md notes the behaviour.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-12 09:07:54 -04:00
Michael A. CojocariandCopilot a8a0758088 CrowPanel 5: experimental LCD clock pause around flash writes
Flash writes hold the bus shared with PSRAM, so the RGB panel's DMA underruns
and the picture jumps sideways for a frame (confirmed with a flash-write trace
while sending messages). This hook stops the LCD clock for the length of each
flash write or erase. It's off by default and not yet tested on hardware.
Also notes that 12 MHz pclk sends the panel into its self-test pattern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 20:12:41 -04:00
Michael A. CojocariandCopilot e33eef43dc CrowPanel 5: edge fix, Wire touch, composer icons; status bar and keyboard redraw fixes
- Panel: clock edge pclk_idle_high=1 (fixes gradient striping), tunable pclk/hporch macros.
- Touch: read the GT911 through Wire with a background poll (fixes repeated-start crash).
- Chat composer: buttons sized to the input, icon labels in ui_icons_20.
- Status bar: chat header uses guarded setters and skips redundant moves/tints, so an
  open chat no longer repaints the bar every 250 ms.
- Keyboard: set/clear PRESSED in a preprocess handler with invalidation off, so a key
  press repaints only that key instead of the whole keyboard.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 20:00:37 -04:00
Terry Carr 6c99a085a4 fix: enhance hex color validation in ChatHashtag and update tests 2026-10-11 23:00:01 +01:00
Terry Carr 412324631b fix: update conditional compilation for keypad navigation and adjust event callback formatting 2026-10-11 22:16:11 +01:00
Terry Carr 9c8ac06d30 fix: remove redundant translations for "Select a link or location" in multiple language files 2026-10-11 21:48:15 +01:00
Terry Carr ace2023026 feat: add script for chat link highlighting and testing 2026-10-11 20:40:54 +01:00
Terry Carr cc4f0f318a fix: link highlighting and drawing for chat messages 2026-10-11 20:31:02 +01:00
Michael A. CojocariandCopilot 3ce6b065e4 Add Elecrow CrowPanel Advance 5.0 build target
New env crowpanel_50_companion_radio_touch for the 800x480 ST7262 RGB panel,
GT911 touch and an SX1262 in the wireless slot. Board quirks follow Camillia-MT:
14 MHz pixel clock, internal-RAM draw band, PCA9557 (V1.0) / MCU (V1.1+)
backlight controller detection with GT911 reset at 0x5D or 0x14, NSS on GPIO8
(V1.2+), no SD/GPS/battery. Large-screen layouts and UI-size fonts; browser
file transfer to the internal tiles partition. Added to the upload helper,
release script, flasher manifests, site, DEVICES.md, README and NOTICE.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 15:07:32 -04:00
Terry Carr 7c8c366b56 fix: remove redundant translations for "Channel" and "Select a link or location" 2026-10-11 19:14:06 +01:00
Terry Carr ed4f530bca feat: add RTL support for chat link text rendering 2026-10-11 19:09:09 +01:00
Michael A. CojocariandCopilot c05153ef85 web app: add Reply to the channel message long-press sheet
Long-pressing someone else's message in a channel chat in the remote web
app now offers Reply (above Copy and Info). It prefills the composer with
the device's mention syntax '@[sender] ', keeps any draft, clamps to the
160-char limit and focuses the input. DMs and our own messages are
unchanged.

Fixes #642

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 13:46:37 -04:00
Michael A. CojocariandCopilot b0a6f59ea3 Contacts: show the chat icon, picked or the first emoji in the name (#646)
The Contacts list now gives each contact the same icon as its chat: the
chat icon picked for it (keyed on the advertised name), else the first
emoji in its shown name, as #640 does for the chat list. Picking or
resetting a chat icon marks the Contacts list for a rebuild.

Fixes #646

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-11 13:00:53 -04:00
Terry Carr 8cc4a5aacf Merge branch 'ALLFATHER-BV:main' into hash-channels-links 2026-10-11 12:05:52 +01:00
Michael A. CojocariandCopilot 67680763ed T-Deck Pro / Max: Normal, Large and Huge text sizes, as on the T-Deck (#639)
Enable CAP_UI_SIZE and the font-only text-size presets (montserrat
12/14/16, 16/18/20, 18/20/24) for HAS_TDECK_PRO boards, compile the
18/20/24 px UI fonts for them, and size the status bar to match.
Docs list the T-Deck Pro and Max under Text size.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-10 21:44:37 -04:00
Terry Carr ae4ed9e44c fix: refactor chat link handling 2026-10-11 01:21:26 +01:00
Terry Carr 80b433bdd6 feat: enhance message action menu with link handling for URLs, hashtags, and coordinates 2026-10-11 00:37:05 +01:00
Confituurke 15ca3f8902 feat(ui): pick your advert position on the map
Typing a latitude and longitude by hand is the only way to set a
position on a node without GPS, and few people know their coordinates to
six decimals. The map already knows where its centre is, so let it do
the work.

Settings > Profile gets a "Pick on the map" row under the lat/lon fields
in the Advert location card. It opens the Map tab centred on the current
position, with a crosshair pinned over the map centre and a Cancel /
Save position bar. Pan and zoom as usual, then "Save position" saves the
centre as the advert position. Leaving the tab cancels it, and so does
Back on the M9, the pager and a Bluetooth keyboard. Auto-follow pauses
while picking so it cannot pull the centre away.

With GPS on and a fix, the GPS sets the position on every loop, so a
picked one would not stay: the row is hidden then, and a fix that
arrives while picking is reported instead of saved. A centre panned past
the antimeridian is wrapped to -180..180 before it is saved.

A node without GPS usually has no map view yet either, so it starts from
the first contact that has a position; only with none at all does it ask
for a rough location first. A typed but unsaved lat/lon is saved before
the jump. The bar sits above the zoom slider, so zooming still works
while picking, and left of the right-edge map buttons, so it never
covers them on a short screen.

The four new strings are translated in every deploy/apps/lang/*.lang
file and i18n_builtin.h is regenerated, so the i18n audit stays clean
for them.

Signed-off-by: Confituurke <9595028+Confituurke@users.noreply.github.com>
2026-10-10 23:40:51 +02:00
Kaj SchittecatandClaude Opus 5.5 2dfe3727cd docs: beta_93 in the guide and the repo docs
Guide (wadamesh.com/docs.html): contact nicknames, the redesigned control
panel with the Battery saver tile, keyboard tooltips and the amber cursor,
automatic chat icons, the default lock wallpaper, the GPS serial speed and
battery notes, the V4 built-in apps and the Lua send budget, and a new
T-LoRa Pager section (hotkeys, navigation memory, Power off confirm, power
saving). Three new screenshots, GPS position and IP masked.

Repo docs: the Pager and M9 key guides describe tooltips, per-tab memory
and Power off, and README, TLORA_PAGER_SHORTCUTS and THINKNODE_M9_SHORTCUTS
no longer use em dashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 23:38:01 +02:00
CyrioandClaude Fable 5.1 5a3c1be63d Pager: paste chord on Fn+Enter instead of Fn+V
Review follow-up (B0N3-GD): Fn is the Pager's only symbol key, so a
held-Fn letter chord takes that letter's symbol away -- Fn+V cost the
question mark with Fn held. Fn+Enter types nothing today (Enter has no
entry in the symbol map), so the chord moves there and costs nothing.

- PagerKeyboardState: V_POS -> ENTER_POS (row 1, col 9); the chord is
  still resolved by matrix position before the symbol map, only while
  Fn is physically held, and cleared by discardAlt().
- PagerKeyboard.h/.cpp, UITask.cpp: renamed to *AltEnterChord*; the
  paste path itself (txtPasteInto, hex-key extraction, toasts, drain
  sites) is unchanged.
- test_pager_keyboard_state: chord on Enter fires once and swallows the
  press; a plain Enter still returns '\r'; tap-Fn-then-Enter is not a
  chord and types nothing, as before; Fn held + V types '?' again.
- TLORA_PAGER_SHORTCUTS.md: both rows now say Fn held + Enter, and the
  em dash is gone from the prose.

Rebuilt and flashed on a Pager (RAM 37.8 %, Flash 90.0 %, unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VKtRiAcddFBAUDogBWmZk4
2026-10-10 13:57:18 -07:00
Terry Carr c5042b71d3 fix: improve hashtag span validation and enhance test coverage for edge cases 2026-10-10 21:56:21 +01:00
Kaj SchittecatandClaude Opus 5.5 d68f276b06 touch: beta_93, contact nicknames, new control panel, tooltips, Pager shortcuts, speed and battery
Contact nicknames: TouchPrefsStore nick blob (48 x 38 B), display-only lookups
(contactShownName, threadShownName, senderShownName), Contact options >
Nickname on the shared text prompt (an empty OK clears it).
Control panel restyled in the launcher's tile language, with a Battery saver
tile; keyboard tooltips (navSetTip), an amber cursor and touch long-press
tooltips (the T-Deck touch read hid them on every poll).
T-LoRa Pager: M/C/A/S on Home, H/S tap and hold, Backspace = Back, N = jump to
new, held Enter = long press, per-tab focus memory, Power off confirm; LVGL
-O2 and IRAM, INT-gated keyboard, idle yield, status-bar setter guards, instant
tabs; PagerGps (u-blox software standby for rests, GPS rail cut when off),
panel sleep, amp off at boot, power-off parks the radio (NSS held), GPS, panel
and keyboard light.
Fixes: the DM duplicate check keys on the attempt and a path reset clears it,
the direct timeout masks the packed path byte, web remote hops (#636);
changelog over http (#638); Lua SDK send budgets and duplicate guard (#606);
GPS speed note (#613, CAP_GPS_BAUD_AUTO); emoji chat icon (#640); default
wallpaper (#626).
Heltec V4: Tetris, 2048 and Breakout unseeded and the built-in Lua stripped of
comments (identical bytecode), 27 KB freed; the baked language table is pinned
to beta_92's files (scripts/build/lang-pin-beta92) so it still fits.
Translations: 39 new keys in 14 languages, language store v24.
Site: theme, dark mode, aurora and guide refresh, SDK send limits, nicknames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:08:16 +02:00
Kaj c66f70f852 Merge PR #632 (jcyrio): translate the 18 beta_90/91 strings missing from every .lang
i18n: translate the 18 beta_90/91 strings missing from every .lang
2026-10-10 21:56:48 +02:00
Kaj 45c977ff11 Merge PR #633 (oumike): LR1110 old-firmware patch applies to RadioLib 7.8.1
build(m9): the LR1110 old-firmware patch applies to RadioLib 7.8.1
2026-10-10 21:46:19 +02:00
Terry Carr 4ed3fdb52c fixed formatting 2026-10-10 17:34:47 +01:00
Terry Carr e6490b5f7b fixed formatting 2026-10-10 17:33:56 +01:00
Terry Carr dd9e47b2e0 Add translations for "Select a link or location" in multiple languages
- Updated the UI to include functionality for selecting links or locations in chat messages.
- Refactored chat link handling to improve user experience when interacting with links in chat bubbles.
2026-10-10 17:19:19 +01:00
Terry Carr bf8c2255c0 fix: handle trailing hyphens and underscores in hashtag spans 2026-10-10 16:14:04 +01:00
Terry Carr 6cbc9892cb test: enhance hashtag span tests for better coverage and edge cases 2026-10-09 20:53:53 +01:00
Terry Carr 8c31d759b5 fix: simplify hashtag picker layout and improve button styling 2026-10-09 20:53:46 +01:00
Terry Carr 301246f67b feat: add "Select a hashtag channel" option in multiple languages 2026-10-09 20:36:51 +01:00
Terry Carr e9b8811dff fix: update alert message for missing channel in hashtag chat 2026-10-09 20:15:26 +01:00
Terry Carr 68771f7860 Removed already delete comment 2026-10-09 19:49:30 +01:00
Michael A. CojocariandClaude Opus 5.5 9c0cccd2fb build(m9): the LR1110 old-firmware patch applies to RadioLib 7.8.1 too
lib_deps takes RadioLib ^7.6.0, and a fresh fetch now brings 7.8.1. Its
LR11x0::config() wraps driveDiosInSleepMode in its own skip for LR1110
firmware below 0x0306, so the call sits one level deeper and the patch
script no longer found it. It failed closed, stopping every M9 build at
its pre-script.

The script now knows both shapes. The patch still matters on 7.8.1:
firmware 0x0306 and 0x0307 get past RadioLib's own skip, send the
command, answer CMD_PERR and would abort radio init with -706.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yt7fNCKmafm9LtRjyuUmS
2026-10-09 11:26:32 -04:00
Terry Carr 6df1476922 fix: enhance bubble link navigation to support URLs and hashtags 2026-10-09 15:55:48 +01:00
CyrioandClaude Fable 5.1 9b891ff501 i18n: translate the 18 beta_90/91 strings missing from every .lang
scripts/build/i18n-audit.py reported the same 18 keys missing in all
14 language files -- the background picker (Background, Background
colour, Still/Moving aurora, Image, None, Choose an image, Choose the
colour, Set as background, the JPEG hints and the restart notice), the
GPS battery saver (Save battery, Check position every, the two
descriptions, "GPS: resting, next check in %lum%02lus") and the
"remote view open" touch-sleep block reason.

Each file gets the 18 rows in its alphabetical slot, translated rather
than left as English placeholders, reusing the terms the files already
use for the older wallpaper strings (Hintergrund, fond d'écran, sfondo,
achtergrond, háttérkép, pozadí, papel de parede/fundo, fundal, фон,
позадина, φόντο ...) and for the battery saver. "%lum%02lus" is kept
verbatim in every language. src/ui-touch/i18n_builtin.h regenerated
with scripts/build/gen-lang-builtin.py; the audit now passes for all
14 languages. "# ver: 23" left alone, since that is bumped at release.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VKtRiAcddFBAUDogBWmZk4
2026-10-09 07:05:06 -07:00
Terry Carr 3de61004e8 fix: handle extended characters in hashtag detection 2026-10-09 14:33:50 +01:00
Terry Carr 10b3674c32 fix: add support for multiple hashtags in chat bubbles 2026-10-09 14:18:15 +01:00
Terry Carr efe71684c4 Fix: clickable flag and navigation skip 2026-10-09 13:49:45 +01:00
CyrioandClaude Fable 5.1 2f811a7a6b i18n: add "Clipboard empty" to every .lang (Fn+V toast)
Review follow-up: the new TR("Clipboard empty") toast had no row in
deploy/apps/lang/*.lang, so i18n-audit.py flagged it in all 14
languages. Added in each file's alphabetical slot (after "Client
repeat") and regenerated src/ui-touch/i18n_builtin.h with
scripts/build/gen-lang-builtin.py. The audit no longer lists the key.
"# ver: 23" is left alone, since that is bumped at release.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VKtRiAcddFBAUDogBWmZk4
2026-10-09 05:36:36 -07:00
Terry Carr 357a9092d4 Merge branch 'ALLFATHER-BV:main' into hash-channels-links 2026-10-09 13:22:22 +01:00
Kaj SchittecatandClaude Opus 5.5 33ae4c61bd tanmatsu: drop the Wi-Fi OTA path so the app fits the launcher's load window
The launcher's AppFS bootloader maps only the first 50 x 64 KB of an app
while it checks and starts it (appfsBlMmap in badgeteam/appfs 2.1.0).
beta_91 (3,329,568 B) and the first beta_92 build (3,331,120 B) were past
it, so neither would start on a Tanmatsu.

On the Tanmatsu the launcher updates the app (CAP_OTA 0) and the only OTA
slots are the launcher's own 2 MB ones, so the core's "ota start" upload
page and URL install could never work there. DISABLE_WIFI_OTA stubs them
("ERR: OTA not supported in this build"), and with them go ElegantOTA,
the async web server and AsyncTCP: the app drops to 3,200,864 B, 75,936 B
under the window.

tanmatsu-appstore-publish.sh now refuses a binary over the window, and
its PR text no longer has an em dash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:40:02 +02:00
Kaj SchittecatandClaude Opus 5.5 9d0301b80a touch: beta_92, faster screens on most boards, Arabic fixes, GitHub fixes
Frame rate: full-width PSRAM draw band (6 bands a frame instead of 14 on the
T-Deck), the still aurora drawn once and copied, LVGL at -O2 with its blend and
label loops in IRAM (scripts/build/perf_lvgl.py, 1 KB more loop stack), circle
and gradient caches, newlib memcpy. Rolled out to the M9, Attaky, RAK, Wio,
CrowPanel and V4-R8; the Pager gets the band only, the V4 the caches only.
Frame benchmark tour (DOC_PERF_BENCH) for measuring it.

Fixes: hop counts read from the packed path length (#621); Arabic shaping
rows patched in LVGL (patch_lvgl_arabic.py), Arabic 101 shift layer, kaf,
symbols, text sent as standard letters (#582); info popup colour runs that
never wrap (#602); keypad nav reaches the chat header (#623); the browser
opens the changelog (#622, gen-changelog.py at every site deploy); the
Theme background colour picker no longer edits the accent; flasher entries
for P4 units with factory ESP-AT Wi-Fi chips (#612, #619).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:53:33 +02:00
CyrioandClaude Fable 5.1 e3baa774f5 Pager: Fn+V pastes the clipboard (no touch path to the edit menu)
On the T-LoRa Pager there was no way to paste: the in-RAM clipboard is
only consumed by the Cut/Copy/Paste edit menu, which opens solely from a
touch long-press on a textarea, and the Pager's encoder never reaches
LVGL's press events. Copy from a message bubble worked; nothing could
paste the result -- the 64-hex contact key and 32-hex channel secret
fields being the painful cases.

Physical Fn(Alt) held + V now pastes into the field being edited:

- PagerKeyboardState: V_POS; a held-Alt V press is resolved by matrix
  position before the symbol map, swallowed, and reported through
  alt_v_chord_pending_/consumeAltVChord() (same shape as Alt+Backspace
  and the Pro's Alt+B). discardAlt() clears it. !HAS_TDECK_PRO only.
- PagerKeyboard.h/.cpp: pagerKeyboardConsumeAltVChord().
- UITask: the Paste cell's logic is factored into txtPasteInto(ta)
  (selection replaced, pasteTextFor hex-key extraction kept) and reused
  by updatePagerAltVChord(), which uses the same "actually editing" test
  as the Alt+Shift Caps chord, hides any open edit menu/accent popup,
  and toasts "Pasted" / "Clipboard empty". Drained at the remote-mirror
  and screen-off discard sites like the other chords.
- TLORA_PAGER_SHORTCUTS.md: documented.
- test_pager_keyboard_state: chord fires once and swallows V;
  tap-Fn-then-V still returns '?'; discardAlt() drops a pending chord.

Trade-off: Fn+V is '?' on this layout, so only a HELD Fn chords. The
one-shot (tap Fn, then V) and locked (double-tap Fn) symbol layers still
type '?', the same compromise the Pro's physical Alt+B makes with '!'.

Tested on tlora_pager_sx1262_companion_radio_touch: Copy from a bubble
then Fn+V in the composer pastes; Fn+V in a key field pastes just the
key out of a copied message; empty clipboard toasts; tap-Fn-then-V
still types '?'.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VKtRiAcddFBAUDogBWmZk4
2026-10-09 02:34:07 -07:00
Terry Carr b2ecf51c38 Merge branch 'ALLFATHER-BV:main' into hash-channels-links 2026-10-09 00:30:37 +01:00
Kaj SchittecatandClaude Opus 5.5 c230820945 touch: beta_91, no picture background on e-paper (the T-Deck Pro link)
The image viewer's Set as background called into the Theme page, which the
e-paper builds do not compile. CAP_BG_IMAGE and CAP_BG_MOVING are 0 there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:24:45 +02:00
Kaj SchittecatandClaude Opus 5.5 218bc6ed3e touch: beta_91, Home becomes widgets, backgrounds, GPS in the bar
Home is a grid of arrangeable widgets (24 kinds, presets, a layout per
orientation, Lua app widgets via wada.widget); Unread, Chat and Mentions
get a large size. Theme > Background puts an aurora (still by default, or
moving), a colour, a picture or nothing behind every screen, with glass
cards and a smoked-glass top bar. A GPS icon in the bar, a GPS battery
saver (u-blox GNSS stop/start where there is no power switch), and a
battery saver that no longer waits for Bluetooth and Wi-Fi to be off.
Also: navigation bar position, every settings page on the redesign's
cards, a faster app drawer, slower screensaver drift for burn-in, text
wrapping at spaces only, the V4-R8 Store Apps tab, and SDK Test marked
extended-SDK only (keeps the 2 MB V4 at 99.8%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:14:04 +02:00
Terry Carr 6f26a93149 fix: target hashtag taps to link text 2026-10-08 00:03:09 +01:00
Terry Carr 3d95076b3e refactor: remove unused hashtag functions and streamline hashtag handling in chat bubbles 2026-10-08 00:03:09 +01:00
Terry Carr cfa98462e6 fix: improve hashtag detection by excluding mentions and URL fragments 2026-10-08 00:03:09 +01:00
Terry Carr 3bc2acc07a feat: add support for hashtag channels links with prefill and open functionality 2026-10-08 00:03:09 +01:00
Kaj SchittecatandClaude Opus 5.5 39f342c67e site: user guide for beta_90, colour and the Theme page
A Colours & theme section: the Theme page (preview, Apply and restart on the
box and in the bar under the title), MORE COLORS! in Regular, Neon and Pastel,
Taste the rainbow, chat icons and colourful bubbles, with a gallery of the
looks. A Theme card in Settings pages; the look rows left the Display card.
Chats describes the per-chat colours and the Initials option.

The main screenshots (Home, Chats, conversation, Contacts, Settings, Map) are
retaken in the new default look. The map is fully loaded this time (the beta_89
shot was taken mid-download), with its coordinates pixelated as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:44:21 +02:00
Kaj SchittecatandClaude Opus 5.5 f1a7775d0c touch: beta_90, colour comes back
Testers said the colour was what set wadamesh apart, and the beta_89 redesign
took most of it out. This puts it back as choices on one new page.

Settings > Theme (CAT_THEME, Device group): a live preview drawn from a copy of
the palette (themeTokensFor), Night/Day cards, High contrast, the accent swatches
inline with Custom, MORE COLORS! with Regular/Neon/Pastel, Taste the rainbow, and
the chat looks. Mode, accent and look are pending until one Apply and restart on
the preview box; scrolled past it, a slim bar under the title carries the button
with six dots in the picked look. Chat icons, bubbles and compact apply live. The
look rows left Display; the control panel's theme chip long-press opens Theme.

The colour engine: ThemeTokens + lookAdjust over the mode and accent at boot
(applyBootTheme), section hues for the launcher, Settings, tab bar and status
icons, per-name colours (FNV hue) for names, glyphs, initials balls and bubble
tints, Neon lit edges and gradient primaries, Pastel tints, and a deeper Day ink
for names and glyphs on light lists. MORE COLORS! defaults on (Regular); HC and
e-paper keep their own colours. New prefs are standalone u8 keys (chat_icons,
col_more, col_style, col_rb), no schema bump.

Taste the rainbow: per-item hues round the wheel, a 10 Hz hue drift on the tab
marker, chats count and focus ring (rests when off, locked or screensaver), press
dip, an arrival flash on new bubbles (draw-only props, the virtual list never
sees it), rainbow lock mark, boot mark and Constellation nodes (CstTheme
node_sat/node_val), and a dot burst on enabling it.

Chat icons: Glyphs or Initials through one avatarApplyLook; AVATAR_SQUARE_FLAG
moved off LV_OBJ_FLAG_USER_1, which is the nav's NAV_SKIP_FLAG.

Size: wadamesh_mark_rgb.h was a static array included by main.cpp and
UITask.cpp, so every S3 image carried the 30 KB boot mark twice. One definition
now (UITask.cpp; the P4 never compiles main.cpp). The V4 goes from overflowing to
~30 KB free with all of the above.

Capture tour: wakes and ends the screensaver before shooting, DOC_LOOK forces a
look, DOC_LOOK_AUDIT walks the looks and the Theme page, DOC_MAP_SHOT waits for
Wi-Fi and for every tile before shooting the map.

25 new strings as English placeholders in every .lang file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:34:08 +02:00
Kaj SchittecatandClaude Opus 5.5 6fb66d6872 site: user guide for the redesigned interface (beta_89)
New sections for the lock screen and PIN, the Constellation screensaver,
new-message cards, the control panel, text size and the T-Deck trackball;
Home, Chats, Contacts and Settings rewritten for the new screens; Settings
pages regrouped as on the device; every screenshot recaptured on a T-Deck
running beta_89 (location, Wi-Fi name and pairing code masked).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:29:59 +02:00
Kaj SchittecatandClaude Opus 5.5 9b720401f8 release: beta_89 notes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:44:50 +02:00
Kaj SchittecatandClaude Opus 5.5 662665e6de touch: beta_89, the UI overhaul
Every screen redesigned to the Oct 2026 mockups, on one design system:
palette, type roles (generated Montserrat faces with Lucide line icons on the
LV_SYMBOL codepoints), a theme apply-cb that styles every LVGL widget, button
kinds, list rows, sheets as action lists, toasts and key-glyph avatars.

Rebuilt: status bar, Chats (filters, + and menu), conversation (route header,
hop meta, "Heard by N repeaters", delivery states, pill composer), lock screen
(mark, clock, 24 h activity, unread cards), Constellation screensaver, Home
launcher and launcher sheet, Status page, grouped Settings home, animated boot
splash, and the message glance as a notification card.

New: optional PIN lock (salted SHA-256, keyboard / touch pad / number pad per
board, backoff that survives reboot), screensaver on battery with its own
duration, T-Deck text sizes, trackball hold to switch Scroll and Cursor (Scroll
default, prefs v69/v70), hold Settings tab for the control panel. M9 bottom
notices removed. Every screen measured for the Large and Huge text sizes.

Fix: adverts over 2/3-byte path hashes never reached the recently heard table
(packed path_len compared as a byte count); ADVERT_PATH replies now carry the
whole multi-byte path.

English placeholders for the 204 untranslated keys in every .lang file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:26:41 +02:00
435 changed files with 251044 additions and 25592 deletions

No files matched your search

+3
View File
@@ -46,3 +46,6 @@ CLAUDE.md
# device's AppFS partition and is both large and device-specific.
tanmatsu/tools/appfs-src/
tanmatsu/tools/dev_appfs.bin
# Generated by scripts/build/gen-changelog.py at every site deploy
deploy/site/changelog.html
src/ui-touch/doc_radio_apps.h
+51 -2
View File
@@ -15,10 +15,13 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
| Tanmatsu | ESP32-P4 + ESP32-C6, SX1262 | 4" 800x480, 69-key keyboard (no touch) | Tanmatsu app store on the device (runs under the badge.team launcher, not web-flashable) | Store tracks the test channel | Fully supported; LoRa + Wi-Fi + Bluetooth simultaneously, standalone and companion in one |
| Elecrow ThinkNode M9 | ESP32-S3, LR1110 | 2.4" 240x320 (no touch), I2C QWERTY + d-pad | Web flasher | Beta (new in beta_38) | Hardware-complete community port by ded (#138): GPS, microSD, buzzer, lock screen, d-pad navigation |
| Elecrow CrowPanel Advance 3.5 | ESP32-S3, external SX1262 | 3.5" ILI9488 480x320 touch (GT911) | Web flasher (beta) | Beta, hardware validation pending | Requires an SX1262 module in the LoRa expansion slot; not for other CrowPanel models |
| Elecrow CrowPanel Advance 5.0 | ESP32-S3, external SX1262 | 5" ST7262 RGB 800x480 touch (GT911) | Web flasher (beta) | Beta, hardware-tested (display, touch, radio) | V1.1+ boards; SX1262 in the wireless slot with the function DIP switch on the wireless module; no SD, GPS or battery gauge |
| Seeed SenseCAP Indicator D1L | ESP32-S3, SX1262 | 4" 480x480 touch (FT6336) | Development build only | Experimental | Hardware-tested (display, touch, radio); USB-powered, no battery; the RP2040's buzzer and microSD are not used |
| RAK WisMesh Tap V2 (RAK3312) | ESP32-S3, SX1262 | Touch display (LovyanGFX, 30+ fps) | Web flasher | Beta (new in beta_38) | Early community port by Ethac.chen (#136); core mesh, chat and map working |
| LilyGo T-Lora Pager | ESP32-S3, LR1121 or SX1262 | 2.33" IPS LCD, 480x222 (no touch), QWERTY + rotary encoder | Web flasher (pick the build matching your radio) | Beta (new in beta_45) | Fully supported; keyboard-first navigation, microSD, map tile packs |
| Heltec V4-R8 + Expansion Kit V2 | ESP32-S3 (8 MB octal PSRAM), SX1262 | 2.4" touch (CHSC6x) | Web flasher | Beta (new in beta_45) | As the V4 plus microSD and the Expansion Kit sensors; buzzer supported |
| LilyGo T-Display P4 | ESP32-P4 + ESP32-C6, SX1262 or LR2021 | AMOLED or TFT-LCD, touch | Web flasher | Beta (new in beta_45) | Four panel/radio builds; AMOLED + SX1262 hardware-tested, the other combinations need validation |
| Elecrow CrowPanel Advanced 10.1 | ESP32-P4 + ESP32-C6, external SX1262 | 10.1" EK79007 MIPI-DSI 1024x600 touch (GT911) | Web flasher (beta) | Experimental (new in beta_97), not yet tested on hardware | One image per board revision (V1.0, V1.1, V1.2); SX1262 in the wireless slot; no battery reading, sound or GPS |
| Attaky Mesh Series | ESP32-S3, SX1262 | Touch display + front D-pad | Web flasher | Beta (new in beta_47) | Community port by attakygit (#158/#169); detachable keyboard supported; the front D-pad + SELECT navigate the UI |
## Feature notes per board
@@ -98,7 +101,9 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
mode manually and retry, and please report it.
- **Seeed Wio Tracker L2**: pre-release touch target under active bring-up. Builds are for
development and hardware validation only; no public release artifact is
promised until the port is verified.
promised until the port is verified. It has no battery-backed clock, so the
same optional saved-Wi-Fi cold-boot time sync offered on T-Deck is available
in Clock settings.
- **CrowPanel Advance 3.5**: `crowpanel_35_companion_radio_touch` is a
16 MB flash / 8 MB OPI PSRAM build for the ILI9488/GT911 panel and an SX1262
in the expansion slot. It starts upright in 480x320 landscape; Display
@@ -124,6 +129,51 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
CrowPanel. Like Wio Tracker L2, it is included in the all-target builds and local beta release
scripts, not the two-board stable CI workflow. Select it in the upload
helper with `scripts/build-upload-monitor.sh --crowpanel-35`.
- **CrowPanel Advance 5.0**: `crowpanel_50_companion_radio_touch` is a
16 MB flash / 8 MB OPI PSRAM build for the 800x480 ST7262 RGB panel, the
GT911 touch controller and an SX1262 in the wireless-module slot (J8/J9). The
board quirks follow Camillia-MT's port:
- The panel is driven over the 16-bit RGB bus from a PSRAM framebuffer at a
14 MHz pixel clock (Elecrow's demo uses 21 MHz); the slower clock leaves
PSRAM bandwidth for WiFi and LVGL so the picture does not drift or tear.
LVGL's draw band stays in internal RAM for the same reason.
- Flash writes (saving chats and settings) take the bus that the panel's DMA
shares with PSRAM, which made the picture jump sideways. The firmware stops
the LCD clock for each flash write or erase, so the screen briefly freezes
or flickers instead.
- Backlight and touch reset sit behind an I2C controller on GPIO15/16 that
changed between revisions: a PCA9557 at 0x18 on V1.0, a board MCU at 0x30
from V1.1. The driver detects which one answers, resets the GT911 through
it and accepts the touch controller at either 0x5D or 0x14. Below full
brightness the MCU dims by PWM, which can whine; 100% is steady.
- The function DIP switch on the back routes GPIO 2/4/5/6/19/20 to the
wireless slot, the microSD slot or the speaker/microphone. Set it to the
wireless module (S1=0, S0=1), or the radio is not found (the serial log
says so). The microSD slot is therefore unusable; browser Transfer uses the
internal tiles partition instead, as on the Attaky.
- The radio's chip select is GPIO8 on V1.2/V1.3 boards (current production)
and GPIO0 on V1.0. The build targets GPIO8; a V1.0 board is detected at boot
and logged, and needs `P_LORA_NSS=0`.
- No GPS (no free GPIO pair for a UART; 17/18 are panel data), no battery
voltage sensing and no free user button, so hard lock is disabled and the
screen wakes by touch.
It starts in native 800x480 landscape; Display settings can switch to
480x800 portrait. It uses the large-screen layouts and UI-size presets of the
Tanmatsu. Serial uses the board's CH340 USB-to-UART bridge. Tested on a physical
unit: a steady picture with smooth gradients, responsive typing with key
highlighting, the chat header and composer, and sending and receiving messages
over the radio (flash writes show as a brief freeze or flicker), and a fresh
full install and an on-device update, whose long flash erases the panel
recovers from. Select it in
the upload helper with `scripts/build-upload-monitor.sh --crowpanel-50`.
- **SenseCAP Indicator D1L**: `sensecap_indicator_companion_radio_touch` drives
the 480x480 ST7701S RGB panel, the FT6336 touch controller and the SX1262,
whose control lines sit on a TCA9535 I/O expander. It is USB-powered with no
battery, so the battery readouts and Power off are hidden. The RP2040
peripherals (buzzer, microSD, Grove, D1Pro sensors) are not used, and without
storage there are no map tiles. The D1Pro shares the ESP32 side and is expected
to run the same image, but is untested. See
[variants/sensecap_indicator/PORT.md](variants/sensecap_indicator/PORT.md).
## Requested boards
@@ -132,7 +182,6 @@ Open hardware requests, roughly in demand order. Ports are welcome, see
PRs.
- LilyGo T-Deck Pro hardware validation / T-Deck Max variants: [#62](https://github.com/ALLFATHER-BV/wadamesh/issues/62)
- SenseCAP Indicator D1L: [#14](https://github.com/ALLFATHER-BV/wadamesh/issues/14)
## Channels
+3
View File
@@ -53,6 +53,9 @@ We do NOT bind LVGL wholesale. We bind a stable, versioned surface we own
trackball, gps), `version()`, `toast(msg)`, `tr(s)` (the firmware's own
translation table, so an app is not stuck in English).
- `wada.timer` — `every(ms)` drives `on_tick` cadence (min 33 ms).
- `wada.widget` (extended SDK) — `set{title, value, line, bar, spark}` publishes the app's
Home widget (at most once a second), `clear()` empties it. The last one is kept after the
app closes and written to `<root>/apps/<id>.wgt` at teardown; nothing runs headless.
Sandbox env (no `io`, `os`, `require`, `dofile`, `load` of new chunks;
whitelisted stdlib: `math`, `string`, `table`, `pairs/ipairs/select/pcall/
+17 -1
View File
@@ -88,7 +88,16 @@ driver, variants/tdisplay_p4/P4Audio.cpp)
Espressif's public es8311.c/es8311_reg.h; P4Audio.cpp additionally
re-bases the esp-bsp init flow onto Arduino Wire.
LovyanGFX (RAK WisMesh Tap V2 display backend)
ST7701S panel init list (SenseCAP Indicator display driver,
variants/sensecap_indicator/IndicatorDisplay.cpp)
Copyright (c) Meshtastic
GPL-3.0 License — https://github.com/meshtastic/device-ui
The panel's second init command list (MADCTL vertical flip, SDIR
horizontal flip) is taken from Meshtastic device-ui's
include/graphics/LGFX/LGFX_INDICATOR.h (Panel_Indicator::getInitCommands).
The rest of the driver is original code on LovyanGFX's Panel_ST7701.
LovyanGFX (display backend: RAK WisMesh Tap V2, SenseCAP Indicator and others)
Copyright (c) lovyan03 — FreeBSD License — https://github.com/lovyan03/LovyanGFX
Camillia-MT (CrowPanel Advance 3.5 software-SPI microSD backend)
@@ -97,5 +106,12 @@ Camillia-MT (CrowPanel Advance 3.5 software-SPI microSD backend)
Adapted from src/sd_soft_spi.cpp; integrated with WadaMesh's filesystem
API and concurrent storage consumers.
Camillia-MT (CrowPanel Advance 5.0 display, backlight and touch bring-up)
Copyright (c) 2025-2026 Michael A. Cojocari and contributors
GNU General Public License v3.0 or later
The RGB panel configuration, revision detection, backlight controller and
GT911 reset in variants/crowpanel_50/CrowPanel50Display.cpp are adapted
from src/hal/crowpanel_50_display.h and src/hal/hw_crowpanel_50.h.
This list is not exhaustive and is expanded as dependencies are vendored or
added.
+20 -16
View File
@@ -11,7 +11,7 @@ Touch-UI [MeshCore](https://github.com/meshcore-dev/MeshCore) companion-radio
firmware for the **LilyGo T-Deck / T-Deck Plus**, **Heltec V4 + TFT** and other boards
(ESP32-S3).
An LVGL touch UI — map, chat, contacts, channels, settings — split out of
An LVGL touch UI (map, chat, contacts, channels, settings) split out of
[meshcomod](https://github.com/ALLFATHER-BV/meshcomod). The app depends on a
MeshCore fork via PlatformIO `lib_deps`.
@@ -20,22 +20,25 @@ MeshCore fork via PlatformIO `lib_deps`.
See **[DEVICES.md](DEVICES.md)** for the full support matrix, install paths and
per-board status.
- LilyGo T-Deck / T-Deck Plus — env `LilyGo_TDeck_companion_radio_touch` (stable)
- Heltec V4 + TFT + CHSC6x touch — env `heltec_v4_tft_companion_radio_usb_tcp_touch` (stable)
- Tanmatsu (ESP32-P4) — built from `tanmatsu/` (ESP-IDF), ships via the Tanmatsu app store — [sideload guide](TANMATSU_SIDELOAD.md) for running your own build
- Elecrow ThinkNode M9 — env `ThinkNode_M9_companion_radio_touch` (beta) — [keyboard & d-pad guide](THINKNODE_M9_SHORTCUTS.md)
- RAK WisMesh Tap V2 (RAK3312) — env `rak_tap_v2_companion_radio_touch` (beta)
- LilyGo T-Lora Pager — envs `tlora_pager_lr1121_companion_radio_touch` / `tlora_pager_sx1262_companion_radio_touch` (beta) — [keyboard shortcuts](TLORA_PAGER_SHORTCUTS.md)
- Heltec V4-R8 + Expansion Kit V2 — env `heltec_v4_r8_tft_companion_radio_usb_tcp_touch` (beta)
- LilyGo T-Display P4 — built from `tdisplay_p4/` (ESP-IDF); AMOLED + SX1262 by default, with `WADA_P4_LCD=1` for TFT-LCD and `WADA_P4_LR2021=1` for LR2021 (beta)
- Attaky Mesh Series — env `attaky_mesh_series_companion_radio_touch` (beta)
- Elecrow CrowPanel Advance 3.5 — env `crowpanel_35_companion_radio_touch` (beta, hardware validation pending; requires an SX1262 in the expansion slot)
- LilyGo T-Deck / T-Deck Plus: env `LilyGo_TDeck_companion_radio_touch` (stable)
- Heltec V4 + TFT + CHSC6x touch: env `heltec_v4_tft_companion_radio_usb_tcp_touch` (stable)
- Tanmatsu (ESP32-P4): built from `tanmatsu/` (ESP-IDF), ships via the Tanmatsu app store; [sideload guide](TANMATSU_SIDELOAD.md) for running your own build
- Elecrow ThinkNode M9: env `ThinkNode_M9_companion_radio_touch` (beta); [keyboard & d-pad guide](THINKNODE_M9_SHORTCUTS.md)
- RAK WisMesh Tap V2 (RAK3312): env `rak_tap_v2_companion_radio_touch` (beta)
- LilyGo T-Lora Pager: envs `tlora_pager_lr1121_companion_radio_touch` / `tlora_pager_sx1262_companion_radio_touch` (beta); [keyboard shortcuts](TLORA_PAGER_SHORTCUTS.md)
- Heltec V4-R8 + Expansion Kit V2: env `heltec_v4_r8_tft_companion_radio_usb_tcp_touch` (beta)
- LilyGo T-Display P4: built from `tdisplay_p4/` (ESP-IDF); AMOLED + SX1262 by default, with `WADA_P4_LCD=1` for TFT-LCD and `WADA_P4_LR2021=1` for LR2021 (beta)
- Seeed SenseCAP Indicator D1L: env `sensecap_indicator_companion_radio_touch` (experimental, community port, PR #682)
- Elecrow CrowPanel Advanced 10.1 (ESP32-P4): built from `crowpanel_p4_101/` (ESP-IDF), one image per board revision with `WADA_CP_REV=1.0|1.1|1.2` (experimental, not yet tested on hardware; requires an SX1262 in the wireless slot)
- Attaky Mesh Series: env `attaky_mesh_series_companion_radio_touch` (beta)
- Elecrow CrowPanel Advance 3.5: env `crowpanel_35_companion_radio_touch` (beta, hardware validation pending; requires an SX1262 in the expansion slot)
- Elecrow CrowPanel Advance 5.0: env `crowpanel_50_companion_radio_touch` (beta, hardware validation pending; requires an SX1262 in the wireless slot and the DIP switch set to it)
## Architecture
This repo holds only the **app**: the `companion_radio` glue, the `ui-touch`
LVGL UI, each board's glue/variants, and `platformio.ini`. The **MeshCore
core is not vendored here** — it's pulled as a library via `lib_deps` from the
core is not vendored here**: it's pulled as a library via `lib_deps` from the
[`ALLFATHER-BV/meshcomod`](https://github.com/ALLFATHER-BV/meshcomod) monorepo
(the same repo as the non-touch firmware), pinned by a lean source-only `core-*`
git tag. The touch-app files this repo owns (TouchPrefsStore, WifiRuntimeStore,
@@ -100,28 +103,29 @@ automatically:
pio run -e heltec_v4_tft_companion_radio_usb_tcp_touch # Heltec V4 TFT
pio run -e LilyGo_TDeck_companion_radio_touch # LilyGo T-Deck
pio run -e crowpanel_35_companion_radio_touch # CrowPanel Advance 3.5
pio run -e crowpanel_50_companion_radio_touch # CrowPanel Advance 5.0
# or just `pio run` to build the configured default environments
```
Flash with the NVS-preserving 4-component chain (bootloader / partitions /
boot_app0 / firmware at `0x0 / 0x8000 / 0xe000 / 0x10000`) so saved Wi-Fi
credentials survive — not a merged image, which 0xFF-pads and wipes NVS.
credentials survive, not a merged image, which 0xFF-pads and wipes NVS.
## Contributing
Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). One topic per
Contributions are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md). One topic per
PR; inbound contributions are accepted under the project's GPL-3.0 license.
## License
**GPL-3.0-or-later** — see [LICENSE](LICENSE). wadamesh is copyleft: anyone who
**GPL-3.0-or-later**: see [LICENSE](LICENSE). wadamesh is copyleft: anyone who
distributes a build or a fork must also make their source available under the GPL.
This keeps the UI open and concentrates community effort instead of fragmenting it
into closed forks.
wadamesh incorporates and depends on
[MeshCore](https://github.com/meshcore-dev/MeshCore) (MIT, © Scott Powell /
rippleradios.com) and other third-party components — see [NOTICE](NOTICE) for the
rippleradios.com) and other third-party components; see [NOTICE](NOTICE) for the
full list and their licenses. MeshCore-derived files keep their MIT notices; the
combined work is distributed under the GPL (MIT is GPL-compatible). The MeshCore
fork that wadamesh builds against stays **MIT** on purpose, so its Wi-Fi/BLE hooks
+46 -28
View File
@@ -1,22 +1,22 @@
# ThinkNode M9: Keyboard & D-pad Guide
The Elecrow ThinkNode M9 has **no touchscreen** — every screen, every setting,
The Elecrow ThinkNode M9 has **no touchscreen**: every screen, every setting,
and every chat is driven by the d-pad, the dedicated function keys, and the
QWERTY keyboard. If you're coming from the T-Deck or Heltec V4 (touch-first),
this page is the five-minute tour that makes the board feel native.
## The inputs
- **D-pad** — four arrows around a centre button (**OK**). Arrows move the
- **D-pad**: four arrows around a centre button (**OK**). Arrows move the
on-screen focus highlight; OK activates whatever is focused. **Holding OK**
is the board's long-press: it fires the held item's long-press action (a
chat message's context menu, the SD row's hold-to-format) and unlocks the
lock screen.
- **Function row** — dedicated keys for Chats, Home, Mentions, Advert, Map,
Back, Mic and Ctrl (see the table below).
- **QWERTY keyboard** — the keyboard controller resolves Shift/symbol layers
- **Function row**: dedicated keys for Chats, Home, Mentions, Advert, Map,
Back, Ctrl and Triangle (see the table below).
- **QWERTY keyboard**: the keyboard controller resolves Shift/symbol layers
itself, so keys always deliver their final character. It latches one key at
a time, which is why there are no chords on this board — long-press carries
a time, which is why there are no chords on this board; long-press carries
the second layer instead. Start typing in a chat and the composer focuses
itself; any key wakes the screen from idle.
@@ -28,7 +28,7 @@ dropdown → the power menu → the Control Center → a full-screen app → a d
else.
Once there is nothing left covering the screen, **Back returns you to the
previous screen** — the tab you came from, however you got there: a function
previous screen**, the tab you came from, however you got there: a function
key, an app-drawer tile, "Show on map", a contact's Send-msg button. Keep
pressing and you keep walking back through where you have been. When the trail
runs out Back lands on Home, and on Home with nothing open it does nothing,
@@ -42,21 +42,21 @@ the root after closing anything above it. The **Cmdr** tile still opens the
Commander screen when you need it.
The only exception is deliberate: progress overlays (SD format, bulk delete)
block all keys until the operation finishes — Back will not navigate out from
block all keys until the operation finishes; Back will not navigate out from
under a running operation.
## Function keys
| Key | Press | Hold |
|---|---|---|
| **MSG** | Jump to the Chats tab; inside a conversation, return to the chat list (closes an open app first) | — |
| **HOME** | Peel one layer off an open app; on the Home tab, toggle the app drawer; otherwise jump Home (and clear the Back trail) | — |
| **@ (Mentions)** | Open the Mentions screen | — |
| **MSG** | Jump to the Chats tab; inside a conversation, return to the chat list (closes an open app first) | None |
| **HOME** | Peel one layer off an open app; on the Home tab, toggle the app drawer; otherwise jump Home (and clear the Back trail). **Double-press** to open the launcher settings (what Home opens, icon size) | None |
| **@ (Mentions)** | Open the Mentions screen | None |
| **ADV** | Open the Send Advert page | **Toggle GPS on/off** |
| **MAP** | Jump to the Map tab — press again *on* the map to toggle pan mode | — |
| **BACK** | Close the top layer (see above) | — |
| **CTRL** | Open the Control Center (quick toggles, incl. the keyboard light: off / on / auto) | — |
| **MIC** | Nothing yet — deliberately reserved | — |
| **MAP** | Jump to the Map tab; press again *on* the map to toggle pan mode | None |
| **BACK** | Close the top layer (see above) | None |
| **CTRL** | Open the Control Center (quick toggles, incl. the keyboard light: off / on / auto) | None |
| **Triangle** | The focused item's hold action, e.g. a chat row's menu | None |
| **OK / Enter** | Activate the focused item; send a message; run a terminal command; newline in the editor | **Long-press the focused item / unlock the lock screen** |
The **MSG**, **HOME**, **@**, **ADV**, **MAP**, and **CTRL** shortcuts remain
@@ -84,9 +84,18 @@ select OSM, and enable **Tiles from SD card**. See the
[offline map tile guide](README.md#offline-map-tiles) for the QGIS export steps
and map-provider policy requirements.
## Web pages
In the Web browser, **Up** and **Down** read the page: each press moves the
highlight to the next link already on screen, or, when there is none, scrolls
the text by about two-thirds of a screen. If the highlighted link scrolls out
of view, the highlight moves to a link coming into view. Only at the top or
bottom of the page does focus leave for the address bar or the toolbar.
**OK** opens the highlighted link.
## Typing & editing
Inside a text field the left/right arrows move the caret — and when the caret
Inside a text field the left/right arrows move the caret, and when the caret
is already at the edge of the text, the same press steps focus *out* of the
field, so you're never stuck. Enter sends (toggleable under Settings), Back
leaves the field. In the Terminal, Enter runs the command; in the text editor
@@ -96,9 +105,10 @@ While a chat message is focused and newer messages are below it, a down arrow
appears at the right edge. Press **Right** to select it, then **OK** to jump to
the newest message.
Channels, direct messages, and room conversations place **#** and emoji to the
right of the message input. Press Right to move from the input to **#**, and
Right again to move to emoji. Press **OK** on either button to open its picker.
Channels, direct messages, and room conversations place emoji and **#** to the
right of the message input, next to Send. With the caret at the end of the
input, press Right to move to emoji, and Right again to move to **#**; Left
walks back to the input. Press **OK** on either button to open its picker.
In the symbol grid, use all four arrows to move the highlight and **OK** to
insert the selected character.
@@ -110,7 +120,7 @@ After inserting a symbol, focus returns to the originating text field.
## Lock screen
**Hold OK** to unlock — the keyboard's hardware long-press stands in for the
**Hold OK** to unlock: the keyboard's hardware long-press stands in for the
hold-to-unlock other boards do by touch. With "Flash on new message" enabled,
an incoming message wakes (or lock-reveals) the screen and pulses the keyboard
backlight.
@@ -118,26 +128,26 @@ backlight.
## Inside apps
Store apps (Snake and friends) get the d-pad natively: arrows steer, OK taps
the centre — start, steer, and tap-to-retry all work. Display-only apps
the centre, so start, steer, and tap-to-retry all work. Display-only apps
(Airtime, RF Monitor) keep normal navigation: arrows move between the app's
own buttons or scroll the feed. **Back and Home always escape an app** — no
own buttons or scroll the feed. **Back and Home always escape an app**: no
app can trap the keys, even while the screen is locked.
## Recent improvements (beta_66 branch)
If you last used the M9 on an earlier beta, the notable navigation changes:
- **Back now actually goes back** — once everything covering the screen was
- **Back now actually goes back**: once everything covering the screen was
closed, Back had nothing left to do and the press simply died on any bare
tab. It now walks back through the screens you visited, then Home. Jumps that
used to strand you ("Show on map" from Discover or Contacts, a route replay
from a chat, the Home unread line, the ✉ badge, app-drawer tiles) all return
properly. **Back also works in the first-boot wizard** now, stepping back
through its screens like the on-screen Back button.
- **Back restored and made consistent** — it had regressed to doing nothing
- **Back restored and made consistent**: it had regressed to doing nothing
outside text fields, and could close hidden popups beneath the Control
Center. Both fixed; the close-what-you-see rule above now holds everywhere.
- **The map opens faster the first time** — the cold open was doing a
- **The map opens faster the first time**: the cold open was doing a
whole-card zoom scan whose result was thrown away, and (with no GPS fix) was
repeating that scan on *every* open. The "Loading map…" hint now appears
immediately instead of after the scan, so the board no longer looks frozen on
@@ -145,14 +155,14 @@ If you last used the M9 on an earlier beta, the notable navigation changes:
- **The d-pad no longer goes dead for a second** when a message arrives, and
the Spectrum page no longer steers an invisible cursor around the app drawer
underneath it.
- **Apps can't strand you** — opening Advert/Mentions over a running app used
- **Apps can't strand you**: opening Advert/Mentions over a running app used
to orphan it with no key path back; app permission dialogs are now
answerable with the d-pad.
- **Map pan cleans up after itself** (see above — it used to leak across tab
- **Map pan cleans up after itself** (see above; it used to leak across tab
jumps).
- **Keyboard light "On" applies at boot**, not after the first dim cycle.
- **Terminal chat shows incoming replies**, not just your own sent lines.
- **GPS works from a cold boot** — the toggle (and ADV-hold) no longer depends
- **GPS works from a cold boot**: the toggle (and ADV-hold) no longer depends
on a boot-time detection race.
- **Spectrum analyzer** sweeps roughly twice as fast, survives read glitches
without flattening the trace, and waits for an in-flight transmission
@@ -160,3 +170,11 @@ If you last used the M9 on an earlier beta, the notable navigation changes:
The engineering record behind these lives in
[`variants/thinknode_m9/M9_PORT.md`](variants/thinknode_m9/M9_PORT.md).
## Tooltips and the cursor
The focus highlight is amber, so the selected control stands out on any
background. Icon buttons say what they are: rest on one for a moment and a
small card names it. On a control panel tile the card adds "Hold Enter:
settings": holding OK (the d-pad's Enter) there opens that tile's settings
page.
+87 -59
View File
@@ -1,19 +1,19 @@
# T-LoRa Pager: Keyboard & Rotary Encoder Guide
The LilyGo T-LoRa Pager has **no touchscreen and no trackball** — every
The LilyGo T-LoRa Pager has **no touchscreen and no trackball**: every
screen, every setting, and every chat is driven entirely by the physical
QWERTY keyboard and the rotary encoder knob. If you're coming from the
T-Deck or Heltec V4 (touch-first), this is the one board where learning a
handful of gestures up front pays off immediately. This guide covers both
radio variants (LR1121 and SX1262) — the UI and input handling are identical
radio variants (LR1121 and SX1262); the UI and input handling are identical
on both.
## The two inputs
- **Keyboard** — a physical QWERTY matrix with a Space bar, Backspace, Enter,
- **Keyboard**: a physical QWERTY matrix with a Space bar, Backspace, Enter,
a Shift key, and an orange **Fn/Alt** key (bottom-left). No dedicated Esc,
arrow keys, or number row — those are covered by combos below.
- **Rotary encoder** — turn to move the on-screen focus highlight; click
arrow keys, or number row; those are covered by combos below.
- **Rotary encoder**: turn to move the on-screen focus highlight; click
(press the knob) to select. It's your only pointing device, so almost
every screen is navigable with turn + click alone.
@@ -36,20 +36,40 @@ They do nothing inside an open chat, settings detail, app page, or popup, so a
letter cannot unexpectedly close the inner screen. They also remain inactive
while editing a text field, where the keys type normally.
**H** and **S** also work like holding those buttons on a touchscreen:
- **Tap H** on Home switches between the app drawer and your widgets.
- **Hold H** (about half a second) opens the launcher options.
- **Hold S** opens the control panel.
A hold opens them over the screen you are on.
Each tab remembers where you were: coming back puts the cursor on the item
you left. Contacts opens on the first contact, and a chat with unread
messages opens on the first of them (see [N](#n-catching-up)).
Icon buttons say what they are: rest the cursor on one for a moment and a
small card names it, sometimes with a line on what it does or, on a
control-panel tile, "Hold Enter: settings".
**Power off** (control panel, power button) asks first, with Cancel
selected, so one stray click never switches the Pager off. Press **BOOT** to
wake it again.
## Rotary encoder
| Gesture | Action | Keyboard equivalent |
|---|---|---|
| Turn | Move focus to the next/previous item on screen | none — encoder only |
| Turn | Move focus to the next/previous item on screen | none (encoder only) |
| Short click | Select / confirm the focused item | **Enter** |
| Hold ~1 s, then release | **Back**: closes a popup → closes an open chat → goes Home → Esc (whichever applies first) | **Backspace held ~1 s** |
| Hold ~1 s, then release | **Back**: closes a popup → closes an open chat → goes Home → Esc (whichever applies first). A ring fills while you hold | **Backspace** (tap) |
| **Fn (Alt) + turn**, on a main tab | Move between the 5 main tabs (Mail / Contacts / Home / Map / Settings) | **M / C / H / A / S** jumps directly |
| **Fn (Alt) + turn**, inside a settings page or chat | Scroll the page up/down | none — encoder only |
| **Fn (Alt) + short click**, with a text field focused | Open the special-character picker | none — encoder only |
| Turn, with a dropdown open | Scroll through the dropdown's options | none — encoder only |
| Turn, with the accent picker open | Cycle through the accent variants (see below) | none — encoder only (Fn+Space to enter is shared) |
| Turn, with the @-mention list open | Cycle through matching contacts (see below) | none — encoder only |
| Turn, at the oldest/newest **loaded** message in an open chat | Loads more history in that direction and keeps going — see [Chat screen](#chat-screen) | none — encoder only |
| **Fn (Alt) + turn**, inside a settings page or chat | Scroll the page up/down | none (encoder only) |
| **Fn (Alt) + short click**, with a text field focused | Open the special-character picker | none (encoder only) |
| Turn, with a dropdown open | Scroll through the dropdown's options | none (encoder only) |
| Turn, with the accent picker open | Cycle through the accent variants (see below) | none (encoder only; Fn+Space to enter is shared) |
| Turn, with the @-mention list open | Cycle through matching contacts (see below) | none (encoder only) |
| Turn, at the oldest/newest **loaded** message in an open chat | Loads more history in that direction and keeps going (see [Chat screen](#chat-screen)) | none (encoder only) |
Since there's no touch to tap the bottom bar, use its visible **M / C / H / A /
S** shortcuts to jump directly or **Fn+turn** to move through the tabs in
@@ -58,7 +78,7 @@ order.
## Chat screen
An open chat has no touch-only jump-to-oldest/jump-to-latest buttons on this
board — everything is reachable by turning and clicking, and the encoder
board: everything is reachable by turning and clicking, and the encoder
behaves a bit differently here than anywhere else in the app.
### Turning through messages
@@ -67,16 +87,16 @@ Message history is loaded lazily: only the messages currently near the
viewport actually exist as on-screen items, and older/newer ones are pulled in
as you approach them. Turning the encoder normally moves the focus highlight
one message at a time, and **at the edge of what's currently loaded, it keeps
going** — the turn instead scrolls the list to pull in the next batch of
going**: the turn instead scrolls the list to pull in the next batch of
history, then continues moving the highlight onto it. You'll only ever land
outside the message list (see below) once you've genuinely reached the very
oldest or very newest message in the whole conversation — never mid-history.
oldest or very newest message in the whole conversation, never mid-history.
### Leaving the message list
Turning **past the oldest loaded message** (top edge), once there's truly
nothing more to load, wraps around to whatever the last focusable item on the
screen is — the channel-settings gear in an open **channel**, or the Send
screen is: the channel-settings gear in an open **channel**, or the Send
button in a **DM** (a DM has no gear to reach). This is plain focus wrap, the
same as reaching the end of any other list in the app; there's no chat-specific
handling for this edge.
@@ -90,23 +110,23 @@ newest message instead.
### The reply row
Below the messages, turning steps through: **quick-reply (△) → emoji →
text box → Send**, in that order, in both directions — nothing is skipped.
text box → Send**, in that order, in both directions; nothing is skipped.
Turning past the △ chip (toward the messages) always jumps straight to the
newest message, regardless of channel or DM.
### Backspace — catching up
### N: catching up
**Backspace (tap)** jumps to whichever message needs your attention:
A chat with unread messages opens on the first of them. **N** jumps to
whichever message needs your attention:
- If there are unread messages, it jumps to the first one — right below the
**"NEW ----"** divider — and selects it, so you can then turn forward
- If there are unread messages, it jumps to the first one, right below the
**"NEW ----"** divider, and selects it, so you can then turn forward
through your unread messages in order, oldest-of-the-unread first.
- If nothing is unread, it jumps to the newest message instead (same
destination, with focus left on the message).
Backspace's override above only applies while you're inside an open chat and
*not* actively editing the text box — the usual rule from the top of this
guide; with a field focused, Backspace deletes a character as normal.
N only does this inside an open chat and while you are not typing in the
reply box. While you type, it is just the letter n.
## Keyboard layout
@@ -142,14 +162,14 @@ While any text field is focused, including a Wi-Fi or repeater password:
### Shift and Caps Lock
- **Hold Shift + a letter**: that letter (or letters, for as long as you
hold Shift) types uppercase — released, typing goes back to lowercase.
hold Shift) types uppercase; released, typing goes back to lowercase.
Real momentary Shift, just like a normal keyboard.
- **Hold Fn (Alt), then press Shift**: while you're editing a text field,
toggles **Caps Lock** on/off — stays uppercase until you repeat the chord.
toggles **Caps Lock** on/off; it stays uppercase until you repeat the chord.
Anywhere else (not editing a field), the chord does nothing.
- Shift alone, tapped with nothing else, does nothing (as expected).
- **Hold Fn (Alt), then press Backspace**: jumps straight to the **Home**
screen — works everywhere, including while you're actively editing a field
screen; it works everywhere, including while you're actively editing a field
(unlike Alt+Shift above, this one isn't context-dependent). Doesn't delete
a character or trigger the plain-Backspace gestures below.
@@ -157,18 +177,21 @@ While any text field is focused, including a Wi-Fi or repeater password:
| Key | While editing a text field | Otherwise |
|---|---|---|
| **Enter** | Send / submit / newline | Select the focused item — or, if a chat message bubble is focused, opens its action menu (Ack / Mention / Copy / Info / Block) |
| **Backspace** (tap) | Delete a character | In an open chat: jump to your first unread message, or the newest message if nothing's unread — see [Chat screen](#chat-screen) |
| **Backspace** (hold ~1 s) | — | Same as the encoder's long-press: **Back** |
| **Backspace** (hold ~1 s) *while the screen is locked* | — | Unlock the screen |
| **Space** (tap) | Types a space | — |
| **Space** (double-tap, within 250 ms) | Switches between English and your configured secondary keyboard layout | — |
| **Space** (hold ~1 s) | — | Locks the screen (shows a "Locking…" progress bar; tapping any key cancels) |
| **Enter** | Send / submit / newline | Select the focused item. On a chat message bubble, opens its action menu (Ack / Mention / Copy / Info / Block) |
| **Enter** (hold about half a second) | Nothing | What a long press does on a touchscreen: a chat's actions, hiding an app in the drawer, a control-panel switch's settings page |
| **Backspace** (tap) | Delete a character | **Back**: closes a popup, a page or the chat, otherwise goes Home |
| **Backspace** (hold ~1 s) | **Back**, leaving the screen the field is on | **Back** |
| **N** | Types n | In an open chat: jump to your first unread message, or the newest if nothing is unread. See [Chat screen](#chat-screen) |
| **Backspace** (hold ~1 s) *while the screen is locked* | Nothing | Unlock the screen |
| **Space** (tap) | Types a space | Nothing |
| **Space** (double-tap, within 250 ms) | Switches between English and your configured secondary keyboard layout | Nothing |
| **Space** (hold ~1 s) | Nothing | Locks the screen (shows a "Locking…" progress bar; tapping any key cancels) |
| **Fn (Alt)** (tap / double-tap) | Next key uses symbols / lock symbols until tapped again | Same |
| **Fn (Alt) held + Enter** | **Paste** the clipboard at the cursor (replaces a highlighted range). The clipboard is filled by **Copy** in a message bubble's action menu. Fn must be held, not tapped | Nothing |
The **BOOT** button (top of the device) instantly wakes the screen from
idle-dim. It does *not* unlock a screen you've manually locked with the
Space-hold gesture above — for that, hold Backspace.
Space-hold gesture above; for that, hold Backspace.
## Accented characters
@@ -176,30 +199,30 @@ Typing a letter that has accent variants (all vowels, plus `n c s y t z l r`,
both cases) pops up a small row of alternatives above the field. To pick one
without touch:
1. **Fn (Alt) + Space** — jumps focus into the popup.
2. **Turn the encoder** — cycles through the variants.
3. **Enter** — confirms your choice and replaces the letter you just typed.
4. **Backspace** — dismisses the popup and keeps the plain letter.
1. **Fn (Alt) + Space**: jumps focus into the popup.
2. **Turn the encoder**: cycles through the variants.
3. **Enter**: confirms your choice and replaces the letter you just typed.
4. **Backspace**: dismisses the popup and keeps the plain letter.
## @-mentions
Typing `@` followed by a few letters of a contact's name pops up a matching
list in a channel/group chat. Unlike the accent picker above, this one grabs
the encoder **immediately** — no Fn+Space needed, since it only appears once
the encoder **immediately**, no Fn+Space needed, since it only appears once
you've deliberately started typing a mention:
1. **Turn the encoder** — cycles through the matching contacts.
2. **Enter / encoder click** — confirms the highlighted one, replacing
1. **Turn the encoder**: cycles through the matching contacts.
2. **Enter / encoder click**: confirms the highlighted one, replacing
`@partial` with `@FullName ` in your message.
3. **Backspace** — dismisses the list without touching what you typed.
3. **Backspace**: dismisses the list without touching what you typed.
## Sliders
Any focused slider (Control Center brightness, a Settings slider, the Map
zoom bar) can't be dragged without touch — instead:
zoom bar) can't be dragged without touch. Instead:
- **Q** — decrease
- **E** — increase
- **Q**: decrease
- **E**: increase
Each press moves it proportionally (about 20 presses end-to-end) and
persists the new value immediately, the same as releasing a drag.
@@ -208,10 +231,10 @@ persists the new value immediately, the same as releasing a drag.
While the **Map** tab is active:
- **W** — pan north
- **A** — pan west
- **X** — pan south
- **D** — pan east
- **W**: pan north
- **A**: pan west
- **X**: pan south
- **D**: pan east
`X` replaces the usual `S` position because **S** is the visible Settings
shortcut. Pressing **A** on another main screen opens Map; once Map is already
@@ -225,9 +248,9 @@ needed.
**Settings → Keyboard → Keyboard backlight** cycles through three modes
(tap to advance):
- **Off** — always dark
- **On** — always lit
- **Auto** (default) — lights up on any keypress and follows the configured
- **Off**: always dark
- **On**: always lit
- **Auto** (default): lights up on any keypress and follows the configured
**Screen timeout**; `0 = never` keeps it lit until the screen is turned off
Unlike the T-Deck, this is a plain on/off strip under the keys, not a
@@ -252,9 +275,9 @@ that changes keyboard language.
|---|---|
| M / C / H / A / S (top-level main screens only) | Open Mail / Contacts / Home / Map / Settings |
| Turn encoder | Move focus |
| Turn, at the loaded edge of a chat | Load more history, keep moving — only exits the list at the true oldest/newest message |
| Turn, at the loaded edge of a chat | Load more history, keep moving; only exits the list at the true oldest/newest message |
| Click encoder (or Enter) | Select / confirm |
| Hold encoder ~1s (or hold Backspace ~1s) | Back |
| Hold encoder ~1 s, or tap Backspace | Back |
| Fn + turn (main tab) | Switch tabs |
| Fn + turn (page/chat) | Scroll |
| Fn + encoder click (text field focused) | Open special-character picker |
@@ -264,11 +287,16 @@ that changes keyboard language.
| Fn + Shift (editing a field) | Toggle Caps Lock |
| Fn + Shift (not editing a field) | Nothing |
| Fn + Backspace (anywhere) | Jump to Home |
| Fn held + Enter (editing a field) | Paste the clipboard |
| Fn + Space | Enter accent picker |
| @ + letters (composer) | Auto-focuses the mention list — no Fn+Space needed |
| @ + letters (composer) | Auto-focuses the mention list, no Fn+Space needed |
| Enter | Select / send / message action menu |
| Backspace (tap) | Delete / in a chat: jump to first unread message, or newest if none |
| Backspace (hold 1s) | Back, or unlock if locked |
| Hold Enter | Long press: a chat's actions, app options, a switch's settings |
| Backspace (tap) | Delete while typing, otherwise Back |
| Backspace (hold 1s) | Back, also while typing; or unlock if locked |
| N (in a chat) | Jump to the first unread message, or the newest if none |
| H on Home | Switch between the app drawer and widgets |
| Hold H / hold S | Launcher options / control panel |
| Space (tap) | Space |
| Space (double-tap) | Switch keyboard language |
| Space (hold 1s) | Lock screen |
+45
View File
@@ -0,0 +1,45 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=0",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "Elecrow CrowPanel Advance 5.0 (N16R8)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"use_1200bps_touch": false,
"wait_for_upload_port": false,
"require_upload_port": true,
"speed": 921600
},
"url": "https://www.elecrow.com/",
"vendor": "Elecrow"
}
+38
View File
@@ -0,0 +1,38 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_8MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "Seeed SenseCAP Indicator D1L (8 MB FLASH, 8 MB PSRAM)",
"upload": {
"flash_size": "8MB",
"maximum_ram_size": 8388608,
"maximum_size": 8388608,
"require_upload_port": true,
"speed": 921600
},
"url": "",
"vendor": "Seeed Studio"
}
+8
View File
@@ -0,0 +1,8 @@
# build output + component manager cache (project-local ESP-IDF comes in via the ../tanmatsu symlinks)
build/
managed_components/
sdkconfig
sdkconfig.old
dependencies.lock
# flashable images produced by build.sh merge
*.bin
+3
View File
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.10)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(application)
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env bash
# wadamesh / Elecrow CrowPanel Advanced 10.1" ESP32-P4 HMI build wrapper. EXPERIMENTAL.
# Standalone ESP-IDF app modelled on tdisplay_p4/build.sh: reuses the Tanmatsu's project-local
# ESP-IDF 5.5.1 (esp32p4) via the ../tanmatsu symlinks and the shared P4 components.
# ./build.sh build # board revision V1.2 (default, newest)
# WADA_CP_REV=1.1 ./build.sh build # V1.1 (SX1262 DIO1/RST on GPIO53/54)
# WADA_CP_REV=1.0 ./build.sh build # V1.0 (same LoRa pins as V1.1, reversed 1-bit C6 SDIO)
# ./build.sh merge # build + one flashable image at 0x0 (build/<target>/*-merged.bin)
# ./build.sh flash -p /dev/cu.usbserial-XXXX
# ./build.sh fullclean # remove this revision's build tree only
set -e
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
CP_REV="${WADA_CP_REV:-1.2}"
case "$CP_REV" in
1.2) CP_SDIO_FRAG="sdio_v11" ;;
1.1) CP_SDIO_FRAG="sdio_v11" ;;
1.0) CP_SDIO_FRAG="sdio_v10" ;;
*) echo "[build.sh] WADA_CP_REV must be 1.0, 1.1 or 1.2 (got '$CP_REV')" >&2; exit 1 ;;
esac
export WADA_CP_REV="$CP_REV"
P4_BUILD_TARGET="crowpanel_p4_101_v${CP_REV/./}"
P4_BUILD_DIR="$PROJECT_DIR/build/$P4_BUILD_TARGET"
# This project intentionally applies compatibility fixes inside managed_components
# before every build. ESP-IDF's stock fullclean also runs remove_managed_components,
# which rejects those expected hash changes and aborts. A P4 full clean therefore
# removes only the generated CMake/Ninja tree; dependencies stay in place and the
# idempotent patches below remain valid.
if [ "$#" -eq 1 ] && [ "$1" = "fullclean" ]; then
# Plain rm -rf: cmake is not on PATH before export.sh. Each revision has its own build tree
# (and its own sdkconfig inside it), so this removes only the selected one.
if [ -d "$P4_BUILD_DIR" ]; then
rm -rf "${P4_BUILD_DIR:?}" || { echo "[build.sh] FAILED to remove $P4_BUILD_DIR" >&2; exit 1; }
fi
if [ -d "$P4_BUILD_DIR" ]; then
echo "[build.sh] FAILED to remove $P4_BUILD_DIR" >&2
exit 1
fi
echo "[build.sh] removed build/$P4_BUILD_TARGET (patched managed components preserved)"
exit 0
fi
# Keep the baked-in translation + Lua app tables in step with deploy/apps/.
# Same for the seeded Lua apps. The PlatformIO envs get this from a pre: hook;
# the IDF builds need it here or the P4 boards ship stale copies.
python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/pre_gen_baked.py"
# LVGL is vendored (gitignored) by fetch-deps.sh, so a copy vendored before the
# anim_timer use-after-free fix (#428) still needs it. Idempotent; fails on drift.
python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/patch_lvgl_anim_uaf.py" \
--patch-file "$(cd "$(dirname "$0")" && pwd)/components/lvgl/upstream/src/misc/lv_anim.c"
python3 "$(cd "$(dirname "$0")/.." && pwd)/scripts/build/patch_lvgl_arabic.py" \
--patch-file "$(cd "$(dirname "$0")" && pwd)/components/lvgl/upstream/src/misc/lv_txt_ap.c"
cd "$PROJECT_DIR"
export IDF_TOOLS_PATH="$PWD/esp-idf-tools"
# VS Code may launch this wrapper from PlatformIO's virtualenv. Pin the
# project-local IDF environment before export.sh inspects that unrelated Python.
if [ -z "${IDF_PYTHON_ENV_PATH:-}" ]; then
for idf_py_env in "$IDF_TOOLS_PATH"/python_env/idf5.5_py*_env; do
if [ -x "$idf_py_env/bin/python" ]; then
export IDF_PYTHON_ENV_PATH="$idf_py_env"
break
fi
done
fi
unset VIRTUAL_ENV CONDA_PREFIX
# shellcheck disable=SC1091
source esp-idf/export.sh >/dev/null 2>&1
WADA_FW_TAG="${WADA_FW_TAG:-$(git describe --tags --match 'beta_*' --always 2>/dev/null || echo dev)}"
WADA_FW_DATE="$(date '+%-d %b %Y')"
IDF_ARGS=(-B "build/$P4_BUILD_TARGET" \
-DDEVICE=crowpanel_p4_101 \
-DSDKCONFIG="$P4_BUILD_DIR/sdkconfig" \
-DSDKCONFIG_DEFAULTS="sdkconfigs/general;sdkconfigs/wadamesh;sdkconfigs/crowpanel_p4_101;sdkconfigs/$CP_SDIO_FRAG" \
-DWADA_FW_TAG="$WADA_FW_TAG" -DWADA_FW_DATE="$WADA_FW_DATE" \
-DIDF_TARGET=esp32p4)
# A fresh clone has no managed_components yet. Configure once to download them,
# then apply the compatibility patches below before the first compilation.
if [ ! -f managed_components/espressif__libsodium/CMakeLists.txt ]; then
idf.py "${IDF_ARGS[@]}" reconfigure
fi
# --- Build-time patch: libsodium forced includes in paths with spaces -----------------------------
# The managed component emits `SHELL:-include <absolute path>`. CMake leaves that
# path unquoted in Ninja, so a checkout such as "T7 Shield" reaches GCC as two
# input files. GCC accepts the joined -include<path> form, which cannot split.
SODIUM_CMAKE="managed_components/espressif__libsodium/CMakeLists.txt"
if [ -f "$SODIUM_CMAKE" ] && grep -q 'SHELL:-include' "$SODIUM_CMAKE"; then
sed -i '' -e 's|SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}|-include${CMAKE_CURRENT_SOURCE_DIR}|g' \
-e 's|SHELL:-include${CMAKE_CURRENT_SOURCE_DIR}|-include${CMAKE_CURRENT_SOURCE_DIR}|g' "$SODIUM_CMAKE"
echo "[build.sh] patched libsodium forced includes (space-safe paths)"
fi
# --- Build-time patch: SD_MMC internal pull-ups (P4 boards) ------------------------------------------
# The board has no external pull-ups on the SD data lines; the IDF sdmmc host explicitly FLOATS the
# pads unless SDMMC_SLOT_FLAG_INTERNAL_PULLUP is set (so pre-begin gpio_pullup_en gets undone).
# Meck-P4's working SD init sets the flag; Arduino's SD_MMC never does and has no API for it. Patch
# the P4's own managed copy. Idempotent.
SDMMC_C="managed_components/espressif__arduino-esp32/libraries/SD_MMC/src/SD_MMC.cpp"
if [ -f "$SDMMC_C" ] && ! grep -q 'wadamesh P4: internal pullups' "$SDMMC_C"; then
sed -i '' 's|sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();|sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();\n slot_config.flags \|= SDMMC_SLOT_FLAG_INTERNAL_PULLUP; // wadamesh P4: internal pullups (no external ones on the SD lines)|' "$SDMMC_C"
echo "[build.sh] patched arduino SD_MMC.cpp (internal pull-ups on the SD slot)"
fi
# --- Build-time patch: a dead C6 must not spin the SDIO rx task -----------------------------------
# With CONFIG_ESP_HOSTED_TRANSPORT_RESTART_ON_FAILURE=n (see sdkconfigs/tdisplay_p4 for why it is
# off), sdio_read_task posts ESP_HOSTED_EVENT_TRANSPORT_FAILURE and then `continue`s: straight back
# into a register read that is going to fail again. On a unit whose C6 runs factory ESP-AT the
# transport NEVER comes up, so that is an unbounded retry storm: measured at ~16 failed reads per
# second, forever, with the whole transport re-initialising on top of it. It floods the USB-CDC the
# companion app talks over and keeps a task busy for the life of the session.
#
# Turning the restart back on is not the answer: that is the boot loop this board had before.
# Both are the same mistake: a co-processor that cannot be reached is a permanent condition, and
# the driver treats it as transient. Park the task instead. The failure event has already been
# posted, Wi-Fi and BLE are simply unavailable, and everything else on the device keeps running.
# Idempotent.
HOSTED_SDIO="managed_components/nicolaielectronics__esp-hosted-tanmatsu/host/drivers/transport/sdio/sdio_drv.c"
if [ -f "$HOSTED_SDIO" ] && ! grep -q 'wadamesh: park instead of spinning' "$HOSTED_SDIO"; then
python3 - "$HOSTED_SDIO" <<'PYEOF'
import re, sys
p = sys.argv[1]
s = open(p).read()
# Both failure arms look identical apart from the log line: event post, optional restart, continue.
old = """#if H_TRANSPORT_RESTART_ON_FAILURE
ESP_LOGI(TAG, "Host is resetting itself, to avoid any sdio race condition");
g_h.funcs->_h_restart_host();
#endif
continue;"""
new = """#if H_TRANSPORT_RESTART_ON_FAILURE
ESP_LOGI(TAG, "Host is resetting itself, to avoid any sdio race condition");
g_h.funcs->_h_restart_host();
#else
/* wadamesh: park instead of spinning. The transport is down and we are
* not allowed to reboot the host, so retrying can only fail again at
* full speed. The failure event is already posted; stop here and let the
* rest of the device run without Wi-Fi/BLE. */
ESP_LOGE(TAG, "transport down, host restart disabled: parking rx task (Wi-Fi/BLE unavailable)");
while (1) g_h.funcs->_h_msleep(60000);
#endif
continue;"""
n = s.count(old)
if n == 0:
sys.exit("esp-hosted sdio_drv.c: failure arm not found; patch needs updating")
s = s.replace(old, new)
open(p, "w").write(s)
print(" patched %d failure arm(s)" % n)
PYEOF
echo "[build.sh] patched esp-hosted sdio_drv.c (park the rx task on a dead transport)"
fi
# ⚠️ The P4/slot-0 branch REBUILDS slot_config from scratch with `.flags = 0`, silently discarding
# the flag added above (that's the branch that actually runs with BOARD_SDMMC_SLOT=0): so patch that
# struct literal too. Without the flag the IDF host explicitly FLOATS the pads -> deterministic 0x109
# CRC at the first data read regardless of card/frequency.
if [ -f "$SDMMC_C" ] && ! grep -q 'wadamesh P4: slot0 pullups' "$SDMMC_C"; then
sed -i '' 's|^ .flags = 0,$| .flags = SDMMC_SLOT_FLAG_INTERNAL_PULLUP, // wadamesh P4: slot0 pullups (struct rebuilt here; the earlier flag is discarded)|' "$SDMMC_C"
echo "[build.sh] patched arduino SD_MMC.cpp (slot-0 struct pull-up flag)"
fi
# --- Build-time patch: the generic esp32p4 variant's SD power pin is THIS board's touch SDA ---------
# Arduino's esp32p4 variant (the Espressif P4 Function EV board) defines BOARD_SDMMC_POWER_PIN 45, so
# SD_MMC.begin() does pinMode(45, OUTPUT) + a 200 ms power toggle, leaves it driven LOW, and
# SD_MMC.end() clears the pin again. On the CrowPanel GPIO45 is I2C0 SDA (GT911, codec, HY2.0 port):
# the pinMode makes the peripheral manager detach I2C0 (i2cDeinit), so every Wire call afterwards
# failed with "bus is not initialized" and touch never came up (beta_97, tester V1.0 log). The card
# here is powered straight from VDD_3V3 (no switch), so the block has no job on this board at all.
# main/CMakeLists.txt defines WADA_SDMMC_NO_POWER_PIN; the guard below makes SD_MMC honour it. Fails
# the build if the upstream lines change, because touch depends on it. Idempotent.
if [ -f "$SDMMC_C" ] && ! grep -q 'WADA_SDMMC_NO_POWER_PIN' "$SDMMC_C"; then
if [ "$(grep -c '^#if defined(BOARD_SDMMC_POWER_PIN)$' "$SDMMC_C")" -lt 2 ]; then
echo "[build.sh] SD_MMC.cpp: BOARD_SDMMC_POWER_PIN blocks not found; the GPIO45 patch needs updating" >&2
exit 1
fi
sed -i '' 's|^#if defined(BOARD_SDMMC_POWER_PIN)$|#if defined(BOARD_SDMMC_POWER_PIN) \&\& !defined(WADA_SDMMC_NO_POWER_PIN) // wadamesh CrowPanel P4: GPIO45 is I2C SDA|' "$SDMMC_C"
echo "[build.sh] patched arduino SD_MMC.cpp (no SD power pin: GPIO45 stays I2C SDA)"
fi
# --- Build-time patch: remember the C6's esp-hosted version from its init event ---------------------
# The slave reports its firmware version in the transport INIT event; the host only compares it
# (major.minor) and logs a warning. Keep the raw value so main.cpp can say plainly which C6
# firmware is fitted. Asking for it over RPC instead (esp_hosted_get_coprocessor_fwversion) is what
# an old co-processor may not answer. The variable is defined in variants/crowpanel_p4_101/target.cpp.
HOSTED_TDRV="managed_components/nicolaielectronics__esp-hosted-tanmatsu/host/drivers/transport/transport_drv.c"
if [ -f "$HOSTED_TDRV" ] && ! grep -q 'wadamesh_c6_fw_version' "$HOSTED_TDRV"; then
python3 - "$HOSTED_TDRV" <<'PYEOF'
import sys
p = sys.argv[1]
s = open(p).read()
old = "\t// if ESP_PRIV_FIRMWARE_VERSION was not received, slave version will be 0.0.0\n\tcompare_fw_version(slave_fw_version);\n"
new = ("\t// if ESP_PRIV_FIRMWARE_VERSION was not received, slave version will be 0.0.0\n"
"\t{ extern volatile uint32_t wadamesh_c6_fw_version; extern volatile int wadamesh_c6_fw_known;\n"
"\t wadamesh_c6_fw_version = slave_fw_version; wadamesh_c6_fw_known = 1; } /* wadamesh: C6 version for the boot log */\n"
"\tcompare_fw_version(slave_fw_version);\n")
if s.count(old) != 1:
sys.exit("esp-hosted transport_drv.c: version compare not found; patch needs updating")
open(p, "w").write(s.replace(old, new))
PYEOF
echo "[build.sh] patched esp-hosted transport_drv.c (keep the C6 firmware version)"
fi
# --- Build-time patch: this board's C6 ships esp-hosted, so Arduino's hostedInit() must be the real
# one. A managed_components tree copied from a legacy ESP-AT T-Display build may carry the stub.
HOSTED_C="managed_components/espressif__arduino-esp32/cores/esp32/esp32-hal-hosted.c"
if [ -f "$HOSTED_C" ] && grep -q 'wadamesh P4: never start esp_hosted' "$HOSTED_C"; then
sed -i '' 's|^static bool hostedInit() { return false; // wadamesh P4: never start esp_hosted.*$|static bool hostedInit() {|' "$HOSTED_C"
echo "[build.sh] restored arduino hostedInit() (hosted C6 build)"
fi
BLEDEV_CPP="managed_components/espressif__arduino-esp32/libraries/BLE/src/BLEDevice.cpp"
if [ -f "$BLEDEV_CPP" ] && grep -q 'int rc = ble_gap_read_local_irk(irk);' "$BLEDEV_CPP"; then
sed -i '' 's|int rc = ble_gap_read_local_irk(irk);|int rc = 0; (void)irk; // wadamesh: arduino BLE unused; symbol renamed in IDF 5.5 NimBLE|' "$BLEDEV_CPP"
echo "[build.sh] patched arduino BLEDevice.cpp (stub renamed ble_gap_read_local_irk)"
fi
# --- Build-time patch: neutralize esp-hosted's auto-init constructor (P4-ONLY) --------------------
# Initialization must not run before app_main: the C6 is brought up lazily through Arduino
# hostedInit() after the UI, so a C6 that does not answer can only cost Wi-Fi/BLE.
# Two copies get pulled in (espressif__esp_hosted via arduino-esp32, and the nicolaielectronics__
# esp-hosted-tanmatsu fork via tanmatsu-wifi/badge-bsp). The build actually links the fork, so patch
# BOTH so whichever is linked never grabs the SDIO.
for HDIR in espressif__esp_hosted nicolaielectronics__esp-hosted-tanmatsu; do
HINIT_C="managed_components/$HDIR/host/port/esp/freertos/src/port_esp_hosted_host_init.c"
if [ -f "$HINIT_C" ] && grep -q 'ESP_ERROR_CHECK(esp_hosted_init());' "$HINIT_C"; then
sed -i '' 's|ESP_ERROR_CHECK(esp_hosted_init());|/* wadamesh P4: C6 initialized lazily via hostedInit() after the UI */|' "$HINIT_C"
echo "[build.sh] neutralized $HDIR auto-init constructor (defer until board power)"
fi
done
if [ "$#" -eq 1 ] && [ "$1" = "merge" ]; then
idf.py "${IDF_ARGS[@]}" build
OUT="$P4_BUILD_DIR/wadamesh-crowpanel-p4-101-v${CP_REV/./}-merged.bin"
# Same offsets and flash settings as scripts/build/p4-release.sh (bootloader 0x2000 on the P4).
python -m esptool --chip esp32p4 merge_bin --flash_mode dio --flash_size 16MB --flash_freq 80m \
-o "$OUT" \
0x2000 "$P4_BUILD_DIR/bootloader/bootloader.bin" \
0x8000 "$P4_BUILD_DIR/partition_table/partition-table.bin" \
0xf000 "$P4_BUILD_DIR/ota_data_initial.bin" \
0x20000 "$P4_BUILD_DIR/application.bin"
echo "[build.sh] merged image: $OUT (flash at 0x0)"
exit 0
fi
exec idf.py "${IDF_ARGS[@]}" "$@"
+1
View File
@@ -0,0 +1 @@
../../tdisplay_p4/components/RadioLib
+1
View File
@@ -0,0 +1 @@
../../tanmatsu/components/ardlibs
+1
View File
@@ -0,0 +1 @@
../../tanmatsu/components/esp_hosted
+1
View File
@@ -0,0 +1 @@
../../tanmatsu/components/lvgl
+1
View File
@@ -0,0 +1 @@
../../tdisplay_p4/components/meshcore
@@ -0,0 +1,22 @@
# The wadamesh app layer (MyMesh + DataStore + helpers + transports + the LVGL UITask), compiled from
# the repo's src/ plus variants/crowpanel_p4_101/. Mirrors tdisplay_p4/components/wadamesh_app, minus
# the ESP-AT c6_at component (this board's C6 ships esp-hosted firmware).
set(REPO "${CMAKE_CURRENT_LIST_DIR}/../../..")
set(APP "${REPO}/src")
file(GLOB_RECURSE APP_SRCS "${APP}/*.cpp" "${APP}/*.c" "${REPO}/variants/crowpanel_p4_101/*.cpp" "${REPO}/variants/crowpanel_p4_101/*.c")
list(FILTER APP_SRCS EXCLUDE REGEX "/main\\.cpp$")
list(FILTER APP_SRCS EXCLUDE REGEX "/helpers/input/(HeltecV4CapTouch|TDeckKeyboard|TDeckTouch|TDeckTrackball|RakTapV2Touch)\\.cpp$")
file(GLOB LUA_SRCS "${REPO}/lib/lua/src/*.c")
idf_component_register(
SRCS ${APP_SRCS} ${LUA_SRCS}
INCLUDE_DIRS "${APP}" "${APP}/ui-touch" "${REPO}/include" "${REPO}/variants/crowpanel_p4_101" "${REPO}/lib/lua/src"
REQUIRES espressif__arduino-esp32 meshcore lvgl ardlibs RadioLib nvs_flash esp_lcd fatfs
esp_driver_i2c esp_driver_gpio esp_driver_ledc esp_hosted h2zero__esp-nimble-cpp bt
soc esp_hw_support app_update driver esp_timer esp_partition spi_flash esp_adc esp_pm espcoredump
espressif__esp_lcd_ek79007 esp_mm # esp_mm: esp_cache_msync (framebuffer clear)
WHOLE_ARCHIVE
)
target_compile_options(${COMPONENT_LIB} PRIVATE -w
-include "${REPO}/variants/tdisplay_p4/tdisplay_p4_compat.h") # adcAttachPin() shim (arduino 3.x dropped it)
+1
View File
@@ -0,0 +1 @@
../tanmatsu/esp-idf
+1
View File
@@ -0,0 +1 @@
../tanmatsu/esp-idf-tools
+98
View File
@@ -0,0 +1,98 @@
idf_build_get_property(idf_target IDF_TARGET)
idf_component_register(
SRCS
"main.cpp"
PRIV_REQUIRES
esp_lcd # MIPI-DSI (EK79007)
esp_driver_i2c
esp_driver_gpio
fatfs # SD_MMC storage
nvs_flash
bt # NimBLE companion (BLE over the C6)
espressif__arduino-esp32
esp_hosted # ESP32-C6 Wi-Fi/BLE over SDIO slot 1
meshcore
lvgl
RadioLib # target.h (included by main.cpp) pulls <RadioLib.h>
wadamesh_app
INCLUDE_DIRS
"."
)
# Same warning policy as tdisplay_p4/main: vendored code must not fail the build on warnings.
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=unused-variable" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=unused-const-variable" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=stringop-truncation" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=stringop-overflow" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=array-bounds" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=restrict" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-Wno-error=maybe-uninitialized" APPEND)
idf_build_get_property(_co COMPILE_OPTIONS)
list(REMOVE_ITEM _co "-Werror=all")
idf_build_set_property(COMPILE_OPTIONS "${_co}")
# ---- Elecrow CrowPanel Advanced 10.1" ESP32-P4 (EXPERIMENTAL) app define set. -------------------
# Pins: Elecrow schematics V1.0/V1.1/V1.2 + their ESP-IDF lessons; per-pin status and sources in
# the pin table at the top of variants/crowpanel_p4_101/target.h.
#
# Board revision (build.sh WADA_CP_REV=1.0|1.1|1.2, default 1.2). Only two things differ:
# - the SX1262 DIO1/NRST pair (J9.1 / J11.1): GPIO53/54 on V1.0 + V1.1, GPIO27/28 on V1.2;
# - the C6 SDIO data-pin order (sdkconfig fragment, build.sh).
if(NOT DEFINED ENV{WADA_CP_REV} OR "$ENV{WADA_CP_REV}" STREQUAL "1.2")
set(CP_REV_DEFS CP_REV=12 P_LORA_DIO_1=27 P_LORA_RESET=28)
message(STATUS "wadamesh: CrowPanel P4 10.1 board revision V1.2")
elseif("$ENV{WADA_CP_REV}" STREQUAL "1.1")
set(CP_REV_DEFS CP_REV=11 P_LORA_DIO_1=53 P_LORA_RESET=54)
message(STATUS "wadamesh: CrowPanel P4 10.1 board revision V1.1")
elseif("$ENV{WADA_CP_REV}" STREQUAL "1.0")
set(CP_REV_DEFS CP_REV=10 P_LORA_DIO_1=53 P_LORA_RESET=54)
message(STATUS "wadamesh: CrowPanel P4 10.1 board revision V1.0")
else()
message(FATAL_ERROR "WADA_CP_REV must be 1.0, 1.1 or 1.2 (got '$ENV{WADA_CP_REV}')")
endif()
set(WADA_DEFS
HAS_CROWPANEL_P4_101=1 ESP32_PLATFORM MC_VENDORED_TOUCH_APP UI_LVGL=1 HAS_TOUCH_UI=1
${CP_REV_DEFS}
USE_SX1262 RADIO_CLASS=CustomSX1262 WRAPPER_CLASS=CustomSX1262Wrapper
SX126X_RX_BOOSTED_GAIN=1 SX126X_CURRENT_LIMIT=140
SX126X_DIO2_AS_RF_SWITCH=true SX126X_DIO3_TCXO_VOLTAGE=1.6f # Elecrow Lesson14: RadioLib defaults + 1.6 V TCXO
MULTI_TRANSPORT_COMPANION TCP_PORT=5000 WS_PORT=8765 BLE_PIN_CODE=123456
LORA_FREQ=869.618 LORA_BW=62.5 LORA_SF=8 # EU 868 default like the other P4 boards; region is a setting
LORA_TX_POWER=22 MAX_LORA_TX_POWER=22
MAX_CONTACTS=2000 MAX_GROUP_CHANNELS=40 # 32 MB PSRAM
ENABLE_PRIVATE_KEY_IMPORT=1 ENABLE_PRIVATE_KEY_EXPORT=1
# --- SX1262 on the external wireless header J9/J11 (SPI3). DIO1/NRST from the revision block. ---
P_LORA_NSS=10 P_LORA_BUSY=9 P_LORA_SCLK=8 P_LORA_MISO=7 P_LORA_MOSI=6
# --- Display: EK79007 1024x600 MIPI-DSI, native landscape, LVGL at native resolution. ---
DISPLAY_CLASS=EK79007Display SCREEN_WIDTH=1024 SCREEN_HEIGHT=600
# --- Touch GT911 + shared board I2C0 (codec, HY2.0 port). Wire is begun by ESP32Board::begin. ---
PIN_BOARD_SDA=45 PIN_BOARD_SCL=46 PIN_TOUCH_SDA=45 PIN_TOUCH_SCL=46 PIN_TOUCH_RST=40 PIN_TOUCH_INT=42
# --- microSD: SDMMC slot 0, 1-bit (only D0 is wired): CLK43 / CMD44 / D0 39. C6 is slot 1. ---
BOARD_HAS_SDMMC=1 BOARD_SDMMC_SLOT=0
# The generic esp32p4 Arduino variant names GPIO45 as an SD power switch (BOARD_SDMMC_POWER_PIN);
# here 45 is the touch/codec I2C SDA and the card has no power switch. build.sh makes SD_MMC.cpp
# skip that pin when this is defined; without it every SD_MMC.begin()/end() tore down I2C0.
WADA_SDMMC_NO_POWER_PIN=1
)
foreach(_d ${WADA_DEFS})
idf_build_set_property(COMPILE_OPTIONS "-D${_d}" APPEND)
endforeach()
# Firmware version/tag (build.sh passes WADA_FW_TAG/-DATE from git describe + date).
if(NOT WADA_FW_TAG)
set(WADA_FW_TAG "dev")
endif()
if(NOT WADA_FW_DATE)
set(WADA_FW_DATE "")
endif()
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_RELEASE_TAG=\"${WADA_FW_TAG}\"" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_BUILD_DATE=\"${WADA_FW_DATE}\"" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-DFIRMWARE_VERSION=\"wadamesh ${WADA_FW_TAG}\"" APPEND)
# NOTE: no FIRMWARE_OTA_ENV on purpose. This is an experimental board with no release artifact on
# firmware.wadamesh.com yet, so the on-device update UI stays hidden (CAP_OTA 0 in device_caps.h).
+12
View File
@@ -0,0 +1,12 @@
dependencies:
idf: ">=5.5.1"
# Same component set as the T-Display P4 (esp-hosted 2.12.3 fork via tanmatsu-wifi, arduino-esp32
# for the shared Arduino src/, esp-nimble-cpp for BLE over the C6). badge-bsp is not initialised on
# this board; it is here because tanmatsu-wifi depends on it.
badgeteam/badge-bsp: "^0.9.9"
nicolaielectronics/tanmatsu-wifi: "^1.1.2"
nicolaielectronics/tanmatsu-lora: "^0.3.0"
espressif/arduino-esp32: "3.3.10"
h2zero/esp-nimble-cpp: "^2.3.0"
# EK79007 MIPI-DSI panel driver (Elecrow's lessons pin ^1.0.2).
espressif/esp_lcd_ek79007: "^1.0.2"
+277
View File
@@ -0,0 +1,277 @@
// wadamesh on the Elecrow CrowPanel Advanced 10.1" ESP32-P4 HMI (1024x600). EXPERIMENTAL.
//
// Standalone ESP-IDF app, modelled on tdisplay_p4/main/main.cpp (same P4 + C6 esp-hosted pairing,
// same storage law: hot store on the internal LittleFS 'storage' partition, the microSD for tiles and
// bulk). The board has no I/O expander, so bring-up is: on-chip LDO rails -> EK79007 panel -> radio
// -> storage -> UI, and the C6 last. Board globals + radio_init() live in
// variants/crowpanel_p4_101/target.cpp. Pin map: the table in variants/crowpanel_p4_101/target.h.
#include "../../variants/tdisplay_p4/tdisplay_p4_compat.h" // adcAttachPin() shim, BEFORE target.h
#include <Arduino.h>
#include <Mesh.h>
#include "MyMesh.h"
#include <new>
#include "esp_heap_caps.h"
#include "UITask.h"
#include "target.h"
#include <LittleFS.h>
#include <SD_MMC.h>
#include <WiFi.h>
#include "../../src/helpers/esp32/MultiTransportCompanionInterface.h"
#include <helpers/esp32/WifiRuntimeStore.h>
#include <helpers/esp32/TouchPrefsStore.h>
#include "../../src/helpers/esp32/BootTimeSync.h"
#include "esp_hosted.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "driver/gpio.h"
#include "lvgl.h"
#ifndef TCP_PORT
#define TCP_PORT 5000
#endif
#ifndef WS_PORT
#define WS_PORT 8765
#endif
#if defined(CONFIG_BT_NIMBLE_TRANSPORT_UART)
#error "CrowPanel P4 C6 requires NimBLE hosted HCI; disable CONFIG_BT_NIMBLE_TRANSPORT_UART"
#endif
extern "C" bool hostedInitBLE(); // arduino-esp32 BLE controller bring-up over esp-hosted
extern "C" bool hostedSetPins(int8_t clk, int8_t cmd, int8_t d0, int8_t d1, int8_t d2, int8_t d3, int8_t rst);
// C6 SDIO pin set. Arduino's hostedInit() does NOT take these from sdkconfig: it passes the generic
// esp32p4 variant's BOARD_SDIO_ESP_HOSTED_* (CLK18 CMD19 D0-D3 = 14,15,16,17, RST 54) to
// esp_hosted_sdio_set_config(), which overrides Kconfig. That order happens to match the T-Display
// P4 and the CrowPanel V1.0, but V1.1/V1.2 route D0..D3 to GPIO17..14, and C6_EN is GPIO32 on every
// revision. Must run before anything can start esp-hosted (Wi-Fi, BLE, boot time sync).
// The bus width (4-bit, or 1-bit on V1.0) still comes from the sdkconfig fragment.
// Say once, plainly, which esp-hosted firmware the C6 runs (known once the transport is up). A C6
// older than the host (V1.0 units have shipped with 2.3.0 against this 2.12.x host) can leave RPCs
// unanswered, each costing the 5 s RPC timeout on the calling task, so Wi-Fi reconnects are spaced
// out further on such a C6 rather than stalling the UI loop every 10 s.
static int s_c6_state = -1; // -1 unknown, 0 matches the host, 1 older than the host
static void crowpanelC6ReportOnce(const char* when) {
static bool s_done = false;
if (s_done) return;
char line[200];
const int st = crowpanelC6Describe(line, sizeof line);
if (st < 0) return;
s_done = true;
s_c6_state = st;
printf("[C6] %s (seen at %s)\n", line, when);
if (st == 1) printf("[C6] old co-processor: Wi-Fi reconnect attempts every 60 s instead of 10 s\n");
}
static void crowpanelC6Pins() {
#if CP_REV == 10
const bool ok = hostedSetPins(18, 19, 14, 15, 16, 17, 32);
#else
const bool ok = hostedSetPins(18, 19, 17, 16, 15, 14, 32);
#endif
printf("[BOOT] C6 SDIO pins set for V%d.%d: %s\n", CP_REV / 10, CP_REV % 10, ok ? "ok" : "FAILED");
}
// Boot-trace hooks the shared code defines on the S3 boards (src/main.cpp, excluded here).
volatile int g_boot_phase = 0;
extern "C" void set_boot_phase(int phase) { g_boot_phase = phase; }
// App globals: src/main.cpp declares these on the S3 boards; this build owns them.
DataStore store(LittleFS, rtc_clock);
bool g_fs_ok = false; // internal 'storage' (LittleFS) mounted
bool g_sd_ok = false; // microSD mounted
bool g_sd_no_card_at_boot = false; // boot probe: no card answered (UITask's first lazy mount skips)
MultiTransportCompanionInterface serial_interface;
StdRNG fast_rng;
SimpleMeshTables tables;
UITask ui_task(&board, &serial_interface);
static MyMesh& makeTheMesh() {
void* mem = heap_caps_malloc(sizeof(MyMesh), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!mem) mem = malloc(sizeof(MyMesh));
return *new (mem) MyMesh(radio_driver, fast_rng, rtc_clock, tables, store, &ui_task);
}
MyMesh& the_mesh = makeTheMesh();
static void wadameshSetup() {
Serial.begin(115200);
delay(150);
printf("[BOOT] wadamesh / CrowPanel P4 10.1 (board revision V%d.%d, EXPERIMENTAL)\n", CP_REV / 10, CP_REV % 10);
// 1. On-chip LDO rails (DSI PHY + the 3.3 V pad domain of touch/LCD-reset/SD), backlight + amp off.
crowpanelP4PowerOn();
// 2. Shared I2C0 (GT911, codec, HY2.0 port) on 45/46.
board.begin();
// 3. EK79007 panel, before LVGL flushes to it.
if (!display.begin()) printf("[BOOT] EK79007 display init FAILED\n");
// 4. lwIP/tcpip + default event loop before any Wi-Fi/netif call (esp_wifi_remote needs it).
esp_netif_init();
esp_event_loop_create_default();
crowpanelC6Pins();
printf("[BOOT] C6 = esp-hosted (lazy Wi-Fi/BLE initialization)\n");
// 5. Radio (SX1262 on J9/J11).
if (!radio_init()) printf("[BOOT] radio_init FAILED\n");
DisplayDriver* disp = &display;
// 6. Storage: internal LittleFS 'storage' = the hot store; the microSD = tiles, backups, bulk.
g_fs_ok = LittleFS.begin(true, "/lfs", 10, "storage");
printf("[storage] LittleFS(storage) = %s\n", g_fs_ok ? "OK" : "FAILED");
if (g_fs_ok) { LittleFS.mkdir("/identity"); LittleFS.mkdir("/bl"); }
// microSD on SDMMC slot 0, 1-bit: the J5 slot only has D0 routed (CLK43 / CMD44 / D0 39), as in
// Elecrow's Lesson08. Pad power is LDO4, which crowpanelP4PowerOn() already holds, so SD_MMC must
// not try to claim it again (-1 = external power; the card itself is on VDD_3V3 with no switch).
// The card has pull-ups on CMD/D0 (R27/R28); the build also enables the internal ones (build.sh
// patch), as Elecrow does. DAT1-DAT3 are not connected to the P4 (schematic V1.0-V1.2), so the slot
// can only run 1-bit. V1.0 also shows a 10k pull-down on the card's DAT3 (R33, with the pull-up
// R29 marked "10K/NC"); a card that sees DAT3 low at its first command enters SPI mode and never
// answers in SD mode, which would look exactly like an empty slot (unverified on hardware).
SD_MMC.setPowerChannel(-1);
SD_MMC.setPins(43, 44, 39);
for (int p : {44, 39}) gpio_pullup_en((gpio_num_t)p);
// Quiet presence check first: an empty slot (or a card that never answers the first command) is
// reported once, instead of three IDF + Arduino error pairs from the mount ladder below. The
// ladder only runs when a card answered, which is when a lower clock can actually help.
const esp_err_t sd_probe = crowpanelSdProbe();
if (sd_probe == ESP_ERR_TIMEOUT) {
g_sd_no_card_at_boot = true;
sdMountDiagBegin();
sdMountDiagAttempt(400000u, false, false);
sdMountDiagSetMounted(false, 0);
printf("[storage] SD_MMC = no card or not responding (1-bit slot, no answer at 400 kHz)\n");
} else {
if (sd_probe != ESP_OK)
printf("[storage] SD_MMC probe: card answered but init failed (%s); trying the mount ladder\n",
esp_err_to_name(sd_probe));
struct { int khz; const char* tag; } tries[] = {
{ SDMMC_FREQ_DEFAULT, "1-bit 20MHz" },
{ 10000, "1-bit 10MHz (Elecrow)" },
{ 5000, "1-bit 5MHz" },
};
sdMountDiagBegin();
uint32_t mounted_hz = 0;
for (auto& t : tries) {
const bool begin_ok = SD_MMC.begin("/sdcard", true, false, t.khz);
g_sd_ok = begin_ok && SD_MMC.cardType() != CARD_NONE;
sdMountDiagAttempt((uint32_t)t.khz * 1000u, begin_ok, g_sd_ok);
printf("[storage] SD_MMC try %s -> %s\n", t.tag, g_sd_ok ? "OK" : "fail");
if (g_sd_ok) { mounted_hz = (uint32_t)t.khz * 1000u; break; }
SD_MMC.end();
delay(120);
}
sdMountDiagSetMounted(g_sd_ok, mounted_hz);
printf("[storage] SD_MMC = %s\n", g_sd_ok ? "OK" : "card present but mount failed");
}
store.begin(); // internal LittleFS is the DataStore root (P4 storage law)
the_mesh.begin(disp != NULL);
wifiConfigBegin(); // NVS only, no radio
serial_interface.begin(Serial, TCP_PORT, WS_PORT);
serial_interface.setBroadcastResponses(true);
the_mesh.startInterface(serial_interface);
sensors.begin();
ui_task.begin(disp, &sensors, the_mesh.getNodePrefs());
board.onBootComplete();
printf("[BOOT] setup done\n");
// ---- C6 bring-up, AFTER the UI ----------------------------------------------------------------
// A C6 that does not answer (wrong firmware, wrong SDIO pin set for the revision) must cost Wi-Fi
// and BLE only: display, touch and LoRa are already up by now.
#if defined(BLE_PIN_CODE)
char* nm = the_mesh.getNodePrefs()->node_name;
serial_interface.prepareBle("wadamesh-", nm, the_mesh.getBLEPin());
if (wifiConfigGetBleEnabled()) {
if (hostedInitBLE()) serial_interface.beginBle("wadamesh-", nm, the_mesh.getBLEPin());
else printf("[BOOT] hostedInitBLE FAILED (BLE unavailable; LoRa, touch and the UI keep running)\n");
crowpanelC6ReportOnce("BLE start");
} else {
printf("[BOOT] BLE off in settings: C6 not started for it\n");
}
#endif
{
uint32_t synced_epoch = 0;
const BootTimeSyncResult r = bootTimeSyncRun(rtc_clock.timeIsCurrent(), synced_epoch);
if (r == BootTimeSyncResult::Ok) {
rtc_clock.setCurrentTime(synced_epoch);
printf("[BOOT] cold-boot time sync ok: %lu\n", (unsigned long)synced_epoch);
} else if (r != BootTimeSyncResult::Skipped) {
printf("[BOOT] cold-boot time sync: %s\n", bootTimeSyncResultName(r));
}
}
}
extern "C" void app_main(void) {
crowpanelP4InstallFullRestart(); // before anything else registers a shutdown handler
initArduino();
wadameshSetup();
// Wi-Fi state machine + SNTP + TCP/WS companion server, as in tdisplay_p4/main/main.cpp
// (esp-hosted backend: Arduino's real WiFi over esp_wifi_remote).
bool wifi_started = false, wifi_radio_prev = true, wifi_radio_inited = false;
bool sntp_kicked = false, sntp_pushed = false, modem_sleep_set = false;
uint32_t last_wifi_retry_ms = 0;
const uint32_t WIFI_RETRY_INTERVAL_MS = 10000;
const uint32_t WIFI_RETRY_INTERVAL_OLD_C6_MS = 60000; // see crowpanelC6ReportOnce()
for (;;) {
ui_task.loop();
bool wifi_radio_en = wifiConfigWantsWifi();
if (!wifi_radio_inited) {
wifi_radio_inited = true;
wifi_radio_prev = wifi_radio_en;
} else if (wifi_radio_en != wifi_radio_prev) {
wifi_radio_prev = wifi_radio_en;
if (!wifi_radio_en) { WiFi.disconnect(true); delay(50); WiFi.mode(WIFI_OFF); }
wifi_started = false;
}
if (wifiConfigConsumeApplyRequest()) {
if (wifi_started) {
if (!wifi_radio_en) { WiFi.disconnect(true); delay(50); WiFi.mode(WIFI_OFF); }
else { WiFi.disconnect(false, false); delay(50); }
}
wifi_started = false; last_wifi_retry_ms = 0;
}
if (wifi_radio_en) {
if (!wifi_started) {
wifi_started = true;
WiFi.mode(WIFI_STA);
if (wifiConfigHasRuntime()) {
char ssid[WIFI_CONFIG_SSID_MAX], pwd[WIFI_CONFIG_PWD_MAX];
wifiConfigGetSsid(ssid, sizeof(ssid)); wifiConfigGetPwd(pwd, sizeof(pwd));
if (strlen(ssid) > 0) { WiFi.begin(ssid, pwd[0] ? pwd : nullptr); last_wifi_retry_ms = millis(); }
}
}
if (wifiConfigHasRuntime() && WiFi.status() != WL_CONNECTED) {
uint32_t now = millis();
crowpanelC6ReportOnce("Wi-Fi start");
const uint32_t retry_ms = (s_c6_state == 1) ? WIFI_RETRY_INTERVAL_OLD_C6_MS : WIFI_RETRY_INTERVAL_MS;
if ((uint32_t)(now - last_wifi_retry_ms) >= retry_ms) {
last_wifi_retry_ms = now;
char ssid[WIFI_CONFIG_SSID_MAX], pwd[WIFI_CONFIG_PWD_MAX];
wifiConfigGetSsid(ssid, sizeof(ssid)); wifiConfigGetPwd(pwd, sizeof(pwd));
if (strlen(ssid) > 0) { WiFi.disconnect(false, true); WiFi.begin(ssid, pwd[0] ? pwd : nullptr); }
}
}
if (WiFi.status() == WL_CONNECTED) {
if (!modem_sleep_set) { WiFi.setSleep(true); modem_sleep_set = true; crowpanelC6ReportOnce("Wi-Fi connected"); }
serial_interface.startTcpServer(true);
if (!sntp_pushed) {
if (!sntp_kicked) { configTime(0, 0, "pool.ntp.org", "time.nist.gov"); sntp_kicked = true; }
const uint32_t e = (uint32_t)time(nullptr);
if (e > 1700000000) { rtc_clock.setCurrentTime(e); sntp_pushed = true; printf("[TIME] SNTP -> rtc %lu\n", (unsigned long)e); }
}
}
}
serial_interface.tickWebSocketHandshake();
sensors.loop();
the_mesh.loop();
vTaskDelay(1);
}
}
@@ -0,0 +1,12 @@
# Elecrow CrowPanel P4 10.1: standalone 16 MB partition table (same layout as the T-Display P4).
# NOT an AppFS/launcher app (that's the Tanmatsu): this flashes bootloader+app directly.
# Dual OTA app slots (4 MB each; the P4 wadamesh image is ~2.5-3 MB) + a big FAT for
# identity/prefs/contacts/chat history + map tiles.
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 24K,
otadata, data, ota, 0xF000, 8K,
phy_init, data, phy, 0x11000, 4K,
ota_0, app, ota_0, 0x20000, 4M,
ota_1, app, ota_1, 0x420000, 4M,
storage, data, fat, 0x820000, 8000K,
coredump, data, coredump, 0xFF0000, 64K,
1 # Elecrow CrowPanel P4 10.1: standalone 16 MB partition table (same layout as the T-Display P4).
2 # NOT an AppFS/launcher app (that's the Tanmatsu): this flashes bootloader+app directly.
3 # Dual OTA app slots (4 MB each; the P4 wadamesh image is ~2.5-3 MB) + a big FAT for
4 # identity/prefs/contacts/chat history + map tiles.
5 # Name, Type, SubType, Offset, Size, Flags
6 nvs, data, nvs, 0x9000, 24K,
7 otadata, data, ota, 0xF000, 8K,
8 phy_init, data, phy, 0x11000, 4K,
9 ota_0, app, ota_0, 0x20000, 4M,
10 ota_1, app, ota_1, 0x420000, 4M,
11 storage, data, fat, 0x820000, 8000K,
12 coredump, data, coredump, 0xFF0000, 64K,
@@ -0,0 +1,58 @@
# Elecrow CrowPanel Advanced 10.1" ESP32-P4 HMI (1024x600) board sdkconfig fragment. EXPERIMENTAL.
# Modelled on sdkconfigs/tdisplay_p4 (same ESP32-P4NRW32 + ESP32-C6-MINI-1 esp-hosted pairing). The
# C6 SDIO pin/width set lives in a per-revision fragment (sdio_v10 / sdio_v11), layered after this.
CONFIG_IDF_TARGET="esp32p4"
# 32 MB HEX PSRAM @ 200 MHz (in-package on the P4NRW32; Elecrow's V1.0/V1.1 lessons run HEX 200M).
# 200 MHz is behind CONFIG_IDF_EXPERIMENTAL_FEATURES on IDF 5.5; without it Kconfig drops to 20 MHz
# and the DPI panel underruns fetching its PSRAM framebuffer.
CONFIG_SPIRAM=y
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
CONFIG_SPIRAM_SPEED_200M=y
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=65536
# 16 MB W25Q128 + our standalone dual-OTA partition table (same layout as the T-Display P4).
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partition_tables/crowpanel_p4_101_16M.csv"
# MIPI-DSI (EK79007)
CONFIG_LCD_DSI_ISR_IRAM_SAFE=y
# ESP32-C6-MINI-1 co-processor over SDIO slot 1 (CLK18 / CMD19; data pins per revision fragment).
# Factory C6 firmware is esp-hosted MCU 2.12.3 (Elecrow "Advance_P4_On-Board_ESP32C6_Firmware_V2.12.3"),
# the same version as the hosted fork tanmatsu-wifi pulls in.
CONFIG_SLAVE_IDF_TARGET_ESP32C6=y
CONFIG_ESP_HOSTED_CP_TARGET_ESP32C6=y
CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ=40000
# C6_EN = GPIO32 (schematic, all revisions). Reset through hosted_reset_slave_callback() in
# variants/crowpanel_p4_101/target.cpp, so the polarity and the GPIO are explicit in code.
CONFIG_ESP_HOSTED_SLAVE_RESET_ONLY_IF_NECESSARY=y
CONFIG_ESP_HOSTED_RESET_SLAVE_USING_CALLBACK=y
CONFIG_ESP_HOSTED_SDIO_RESET_SLAVE_USING_CALLBACK=y
CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE=y
# The P4 has no Bluetooth controller of its own: NimBLE's HCI runs over esp-hosted SDIO.
CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_CONTROLLER_DISABLED=y
# CONFIG_BT_NIMBLE_TRANSPORT_UART is not set
# A C6 that cannot be reached must cost Wi-Fi and BLE, never the device (see sdkconfigs/tdisplay_p4).
CONFIG_ESP_HOSTED_HOST_RESTART_NO_COMMUNICATION_WITH_SLAVE=n
CONFIG_ESP_HOSTED_TRANSPORT_RESTART_ON_FAILURE=n
# Panic coredump -> the 64K 'coredump' partition (ELF, decodable against the build's elf).
CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=y
CONFIG_ESP_COREDUMP_DATA_FORMAT_ELF=y
CONFIG_ESP_COREDUMP_CHECKSUM_CRC32=y
CONFIG_ESP_COREDUMP_MAX_TASKS_NUM=64
# DPI/video panel: the 1.2 MB framebuffer lives in PSRAM and is streamed continuously, so the same
# underrun mitigations as the T-Display P4: run code from PSRAM (flash writes no longer drop the
# cache under the DSI DMA), a 256 KB L2 cache with 128 B lines, -O2.
CONFIG_SPIRAM_XIP_FROM_PSRAM=y
CONFIG_COMPILER_OPTIMIZATION_SIZE=n
CONFIG_COMPILER_OPTIMIZATION_PERF=y
CONFIG_CACHE_L2_CACHE_128KB=n
CONFIG_CACHE_L2_CACHE_256KB=y
CONFIG_CACHE_L2_CACHE_LINE_64B=n
CONFIG_CACHE_L2_CACHE_LINE_128B=y
+1
View File
@@ -0,0 +1 @@
../../tanmatsu/sdkconfigs/general
+9
View File
@@ -0,0 +1,9 @@
# CrowPanel P4 10.1 V1.0: the C6 data lines run the other way round (schematic: GPIO14 = SD2_D0 ->
# C6 IO20 ... GPIO17 = SD2_D3 -> C6 IO23), and V1.0 has none of the series resistors / caps V1.1
# added on those lines. Elecrow's V1.0 Lesson17 runs the link 1-bit (D0 = GPIO14, D1 = GPIO15 as the
# interrupt line), so this does the same.
CONFIG_ESP_HOSTED_SDIO_1_BIT_BUS=y
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_1=18
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_1=19
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_1=14
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_1BIT_BUS_SLOT_1=15
+10
View File
@@ -0,0 +1,10 @@
# CrowPanel P4 10.1 V1.1 and V1.2: C6 SDIO 4-bit, P4 GPIO17/16/15/14 = D0/D1/D2/D3.
# Source: V1.1/V1.2 schematics (net SD2_D0 = GPIO17 -> C6 IO20 ... SD2_D3 = GPIO14 -> C6 IO23) and
# Elecrow's V1.1/V1.2 Lesson17 sdkconfig (4-bit, same pins).
CONFIG_ESP_HOSTED_SDIO_4_BIT_BUS=y
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_1=18
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_1=19
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_1=17
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_1=16
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_1=15
CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_1=14
+1
View File
@@ -0,0 +1 @@
../../tanmatsu/sdkconfigs/wadamesh
+6
View File
@@ -0,0 +1,6 @@
{
"id": "airtime",
"name": "Airtime",
"ver": "1.5",
"desc": "Utilization bars with a % axis, rx/tx split, duty ceiling, budget. Reset button top-right. Keeps a Home widget with the channel load while it runs."
}
+105
View File
@@ -0,0 +1,105 @@
-- Airtime (Lua) — channel utilization over time: live + average bars with a %
-- axis, rx/tx air split, duty ceiling, tx budget and packet counters. While it
-- runs it also keeps its Home widget current (wada.widget, extended SDK boards).
local ui, sys, mesh, timer = wada.ui, wada.sys, wada.mesh, wada.timer
local widget = wada.widget -- nil on firmware or boards without it
-- Translate through the device's language table (wada.sys.tr, added for #257).
-- Older firmware has no sys.tr, so fall back to the English string rather than error.
local tr = sys.tr or function(x) return x end
local C = ui.colors
local app = {}
local W, narrow
local head, sub, legend, detail, chart
local base, t0, last
local peak_pct = 0
local spark, wlast = {}, 0 -- the Home widget's curve (one point per 5 s)
local function restart_window()
local st = mesh.stats()
base = { rx = st.rx_air_s, tx = st.tx_air_s }
t0 = sys.millis()
last = { ms = t0, rx = st.rx_air_s, tx = st.tx_air_s }
peak_pct = 0
chart:fill(1, 0)
chart:fill(2, 0)
head:set(tr("sampling..."))
head:color(C.accent)
sys.toast(tr("window restarted"), 900)
end
local function refresh()
local st = mesh.stats()
local now = sys.millis()
local wall = (now - t0) / 1000
if wall < 0.5 then return end
local rx_air, tx_air = st.rx_air_s - base.rx, st.tx_air_s - base.tx
local avg = math.min(100, (rx_air + tx_air) * 100 / wall)
local dw = (now - last.ms) / 1000
local live = 0
if dw > 0 then
live = math.min(100, ((st.rx_air_s - last.rx) + (st.tx_air_s - last.tx)) * 100 / dw)
end
last = { ms = now, rx = st.rx_air_s, tx = st.tx_air_s }
if live > peak_pct then peak_pct = live end
chart:push(1, math.floor(live))
chart:push(2, math.floor(avg))
head:set(string.format(tr("channel busy %.1f%%"), live))
head:color(live > 50 and C.bad or live > 20 and 0xE8A33D or C.good)
sub:set(string.format(tr("avg %.1f%% peak %.1f%% over %ds"), avg, peak_pct, math.floor(wall)))
if widget and now - wlast >= 5000 then
wlast = now
spark[#spark + 1] = math.floor(live * 10)
if #spark > 24 then table.remove(spark, 1) end
widget.set{ title = tr("Channel busy"), value = string.format("%.1f%%", live),
line = string.format(tr("avg %.1f%%, peak %.1f%%"), avg, peak_pct), spark = spark }
end
if narrow then
detail:set(string.format("air rx %ds / tx %ds\nduty %d%% budget %dms\ntx %d rx %d err %d",
rx_air, tx_air, st.duty_pct, st.tx_budget_ms, st.tx_pkts, st.rx_pkts, st.rx_err))
else
detail:set(string.format("air rx %ds / tx %ds duty %d%% budget %dms\ntx %d rx %d err %d drop %d SF%d",
rx_air, tx_air, st.duty_pct, st.tx_budget_ms, st.tx_pkts, st.rx_pkts, st.rx_err, st.rx_dropped, st.sf))
end
end
function app.on_open(w, h)
W, narrow = w, w < 280
local LH12, LH16 = ui.text_h(12), ui.text_h(16)
local y = 2
head = ui.label(tr("sampling..."), 4, y, 16, C.accent); head:width(w - 86)
ui.button(tr("Reset"), w - 76, y, 70, 30, function() restart_window() end) -- top-right
y = y + LH16 + 4
sub = ui.label("", 4, y, 12, C.sub); sub:width(w - 10)
y = y + LH12 + 5
local AX = 34
local det_lines = narrow and 3 or 2
local ch_h = h - y - (det_lines * LH12) - LH12 - 14
if ch_h < 48 then ch_h = 48 end
if ch_h > 110 then ch_h = 110 end
chart = ui.chart(w - AX - 8, ch_h, 60, C.accent, 0x4F9DF7, true)
chart:pos(AX, y)
chart:range(0, 100)
chart:axis(3, AX)
y = y + ch_h + 4
legend = ui.label(tr("teal = now blue = average"), 4, y, 12, C.sub); legend:width(w - 10)
y = y + LH12 + 2
detail = ui.label("", 4, y, 12, C.text); detail:width(w - 10)
local st = mesh.stats()
base = { rx = st.rx_air_s, tx = st.tx_air_s }
t0 = sys.millis()
last = { ms = t0, rx = st.rx_air_s, tx = st.tx_air_s }
timer.every(1000)
end
function app.on_tick(dt) refresh() end
return app
+1
View File
@@ -0,0 +1 @@
{"id":"airtime","name":"Airtime","ver":"1.6","desc":"How busy the channel is: the live load, average, peak, a filled chart with the duty ceiling and the rx/tx split. Keeps a Home widget with the channel load while it runs."}
+243
View File
@@ -0,0 +1,243 @@
-- Airtime (Lua): how busy the channel is. The live load as a big figure, tiles
-- for the average, the peak and this node's own time on the air, a filled
-- chart of the load with its average and the duty-cycle ceiling, and the
-- receive / transmit split as a bar. While it runs it also keeps its Home
-- widget current (wada.widget, extended SDK boards).
--
-- Looks best on firmware with wada.ui.panel (beta_95 and later); on older
-- firmware the same screen draws without the framed panels and the split bar.
local ui, sys, mesh, timer = wada.ui, wada.sys, wada.mesh, wada.timer
local widget = wada.widget -- nil on firmware or boards without it
-- Translate through the device's language table (wada.sys.tr, added for #257).
-- Older firmware has no sys.tr, so fall back to the English string rather than error.
local tr = sys.tr or function(x) return x end
local C = ui.colors
local WARN = C.warn or 0xE8A33D
local PANEL = C.panel or 0x15181B
local AVG = 0x4F9DF7
local app = {}
local function mix(a, b, t) -- colour a over b by t (0..1)
local function ch(s)
local x, y = (a >> s) & 0xFF, (b >> s) & 0xFF
return math.floor(y + (x - y) * t + 0.5) << s
end
return ch(16) | ch(8) | ch(0)
end
local AREA = { mix(C.accent, PANEL, 0.50), mix(C.accent, PANEL, 0.30), mix(C.accent, PANEL, 0.14) }
local GRID = mix(C.sub, PANEL, 0.30)
local W, H, narrow, LH12, LH16
local big, sub, tiles, cv, CW, CH, NPTS = nil, nil, {}, nil, 0, 0, 2
local grid_lbl = {}
local split_rx, split_tx, split_w, split_lbl, foot
local split_x0, split_y = 0, 0
local base, t0, last
local peak_pct = 0
local live_h, avg_h = {}, {} -- chart history, percent
local top = 10 -- chart ceiling, percent
local spark, wlast = {}, 0 -- the Home widget's curve (one point per 5 s)
local function panel(x, y, w, h, col)
if ui.panel then return ui.panel(x, y, w, h, col or PANEL, 6) end
end
local function load_col(p)
return p > 50 and C.bad or p > 20 and WARN or C.good
end
local function nice_top(v) -- the chart ceiling: 10, 20, 30, 50 or 100 percent
for _, t in ipairs({ 10, 20, 30, 50, 100 }) do if v <= t then return t end end
return 100
end
local function ypix(p)
if p < 0 then p = 0 elseif p > top then p = top end
return (CH - 1) - math.floor(p * (CH - 1) / top)
end
local function draw_chart(duty)
if not cv then return end
cv:fill(PANEL)
for g = 0, 2 do
local gy = g * (CH - 1) // 2
cv:line(0, gy, CW - 1, gy, GRID)
end
local n = #live_h
local b1, b2 = CH // 3, 2 * CH // 3
for i = 1, n do
local x = (NPTS - n + i - 1) * (CW - 1) // (NPTS - 1)
local x2 = (NPTS - n + i) * (CW - 1) // (NPTS - 1)
if x2 <= x then x2 = x + 1 end
local y = ypix(live_h[i])
if y < b1 then cv:rect(x, y, x2 - x, b1 - y, AREA[1]); y = b1 end
if y < b2 then cv:rect(x, y, x2 - x, b2 - y, AREA[2]); y = b2 end
if y < CH then cv:rect(x, y, x2 - x, CH - y, AREA[3]) end
end
local px, py, qx, qy
for i = 1, n do
local x = (NPTS - n + i - 1) * (CW - 1) // (NPTS - 1)
local y, ya = ypix(live_h[i]), ypix(avg_h[i])
if px then
cv:line(px, py, x, y, C.accent, 2)
cv:line(qx, qy, x, ya, AVG, 2)
end
px, py, qx, qy = x, y, x, ya
end
-- the duty-cycle ceiling, dashed, when the region has one and it is on the scale
if duty and duty > 0 and duty <= top then
local dy = ypix(duty)
for x = 0, CW - 1, 8 do cv:line(x, dy, math.min(x + 4, CW - 1), dy, WARN) end
end
end
local function set_split(rx, tx)
local tot = rx + tx
local rp = tot > 0 and math.floor(rx * 100 / tot + 0.5) or 0
local txt = tot > 0 and string.format(tr("rx %d%% tx %d%%"), rp, 100 - rp) or tr("no airtime yet")
if not split_rx then
if split_lbl then split_lbl:set(txt) end
return
end
local wr = tot > 0 and math.floor(split_w * rx / tot + 0.5) or 0
if tot > 0 and tx > 0 and wr >= split_w then wr = split_w - 1 end
split_rx:size(math.max(wr, 1), 8)
split_tx:pos(split_x0 + wr, split_y)
split_tx:size(math.max(split_w - wr, 1), 8)
split_tx:color(tot > 0 and tx > 0 and AVG or mix(C.sub, PANEL, 0.25))
if tot == 0 then split_rx:color(mix(C.sub, PANEL, 0.25)) else split_rx:color(C.accent) end
split_lbl:set(txt)
end
local function restart_window()
local st = mesh.stats()
base = { rx = st.rx_air_s, tx = st.tx_air_s }
t0 = sys.millis()
last = { ms = t0, rx = st.rx_air_s, tx = st.tx_air_s }
peak_pct = 0
live_h, avg_h = {}, {}
top = 10
big:set("--"); big:color(C.accent)
sys.toast(tr("window restarted"), 900)
end
local function refresh()
local st = mesh.stats()
local now = sys.millis()
local wall = (now - t0) / 1000
if wall < 0.5 then return end
local rx_air, tx_air = st.rx_air_s - base.rx, st.tx_air_s - base.tx
local avg = math.min(100, (rx_air + tx_air) * 100 / wall)
local dw = (now - last.ms) / 1000
local live = 0
if dw > 0 then
live = math.min(100, ((st.rx_air_s - last.rx) + (st.tx_air_s - last.tx)) * 100 / dw)
end
last = { ms = now, rx = st.rx_air_s, tx = st.tx_air_s }
if live > peak_pct then peak_pct = live end
live_h[#live_h + 1] = live; if #live_h > NPTS then table.remove(live_h, 1) end
avg_h[#avg_h + 1] = avg; if #avg_h > NPTS then table.remove(avg_h, 1) end
local hi = st.duty_pct or 0
for i = 1, #live_h do if live_h[i] > hi then hi = live_h[i] end end
local t = nice_top(hi * 1.15)
if t ~= top then
top = t
local v = { top, top // 2, 0 }
for g = 1, 3 do if grid_lbl[g] then grid_lbl[g]:set(v[g] .. "%") end end
end
draw_chart(st.duty_pct)
big:set(string.format("%.1f%%", live)); big:color(load_col(live))
local m = math.floor(wall)
sub:set(string.format(tr("over %d:%02d"), m // 60, m % 60))
tiles[1]:set(string.format("%.1f%%", avg))
tiles[2]:set(string.format("%.1f%%", peak_pct)); tiles[2]:color(load_col(peak_pct))
tiles[3]:set(string.format("%d s", tx_air))
tiles[4]:set(string.format("%d s", rx_air))
set_split(rx_air, tx_air)
foot:set(string.format(narrow and tr("budget %d s tx %d rx %d") or tr("budget %d s tx %d rx %d err %d duty %d%% SF%d"),
st.tx_budget_ms // 1000, st.tx_pkts, st.rx_pkts, st.rx_err, st.duty_pct, st.sf))
if widget and now - wlast >= 5000 then
wlast = now
spark[#spark + 1] = math.floor(live * 10)
if #spark > 24 then table.remove(spark, 1) end
widget.set{ title = tr("Channel busy"), value = string.format("%.1f%%", live),
line = string.format(tr("avg %.1f%%, peak %.1f%%"), avg, peak_pct), spark = spark }
end
end
function app.on_open(w, h)
W, H, narrow = w, h, w < 280
LH12, LH16 = ui.text_h(12), ui.text_h(16)
local LHB = ui.text_h(24)
-- the live figure, what it means, and Reset
local y = 4
big = ui.label("--", 6, y, 24, C.accent, true)
local bw = (ui.text_w and ui.text_w("100.0%", 24) or 70) + 10
local l1 = ui.label(tr("channel busy now"), 6 + bw, y + 2, 12, C.sub); l1:width(W - bw - 90)
sub = ui.label("", 6 + bw, y + 2 + LH12, 12, C.sub); sub:width(W - bw - 90)
ui.button(tr("Reset"), W - 76, y + 1, 70, math.max(28, LHB - 2), function() restart_window() end)
y = y + math.max(LHB, 2 * LH12 + 4, 30) + 6
local gap = 4
local labels = { tr("Average"), tr("Peak"), tr("TX"), tr("RX") }
local tw = (W - 8 - gap * 3) // 4
local th = LH12 + LH16 + 8
for i, k in ipairs(labels) do
local x = 4 + (i - 1) * (tw + gap)
panel(x, y, tw, th)
local kl = ui.label(k, x + 6, y + 3, 12, C.sub); kl:width(tw - 8)
tiles[i] = ui.label("--", x + 6, y + 3 + LH12, 16, C.text, true)
end
y = y + th + 6
-- the chart, with % labels in the gutter
local AX = (ui.text_w and ui.text_w("100%", 12) or 30) + 8
local foot_h = LH12 + 4
local split_h = LH12 + 6
CH = H - y - split_h - foot_h - 18
if CH > 120 then CH = 120 end
if CH < 40 then CH = 40 end
panel(AX, y, W - AX - 4, CH + 6)
CW = W - AX - 10
cv = ui.canvas(CW, CH); cv:pos(AX + 3, y + 3)
NPTS = math.max(CW // 2, 2)
local v = { top, top // 2, 0 }
for g = 0, 2 do
local l = ui.label(v[g + 1] .. "%", 0, y + 3 + g * (CH - 1) // 2 - LH12 // 2, 12, C.sub)
l:width(AX - 6, "right")
grid_lbl[g + 1] = l
end
y = y + CH + 6 + 6
-- the receive / transmit split
ui.label(tr("Air split"), 6, y, 12, C.sub)
local kw = (ui.text_w and ui.text_w(tr("Air split"), 12) or 60) + 10
local lw = (ui.text_w and ui.text_w("rx 100% tx 100%", 12) or 110) + 4
split_x0, split_y = 6 + kw, y + (LH12 - 8) // 2
split_w = W - split_x0 - lw - 12
if ui.panel and split_w > 20 then
ui.panel(split_x0, split_y, split_w, 8, mix(C.sub, PANEL, 0.25), 4)
split_rx = ui.panel(split_x0, split_y, 1, 8, C.accent, 4)
split_tx = ui.panel(split_x0 + split_w - 1, split_y, 1, 8, AVG, 4)
split_lbl = ui.label("", W - lw - 6, y, 12, C.text); split_lbl:width(lw, "right")
else
split_lbl = ui.label("", 6 + kw, y, 12, C.text); split_lbl:width(W - kw - 12)
end
y = y + split_h
foot = ui.label("", 6, y, 12, C.sub); foot:width(W - 12)
local st = mesh.stats()
base = { rx = st.rx_air_s, tx = st.tx_air_s }
t0 = sys.millis()
last = { ms = t0, rx = st.rx_air_s, tx = st.tx_air_s }
timer.every(1000)
end
function app.on_tick(dt) refresh() end
return app
+37 -8
View File
@@ -3,14 +3,14 @@
{
"id": "monitor",
"name": "RF Monitor",
"ver": "1.3",
"desc": "Scrollable: RSSI/noise chart with dBm axis, link margin, RX rate, radio params, heard feed."
"ver": "1.4",
"desc": "RSSI, noise, margin and packet-rate tiles, a filled signal chart and the recently heard packets. Tap a packet for who sent it, its route, margin, size and airtime."
},
{
"id": "airtime",
"name": "Airtime",
"ver": "1.4",
"desc": "Utilization bars with a % axis, rx/tx split, duty ceiling, budget. Reset button top-right."
"ver": "1.6",
"desc": "How busy the channel is: the live load, average, peak, a filled chart with the duty ceiling and the rx/tx split. Keeps a Home widget with the channel load while it runs."
},
{
"id": "snake",
@@ -22,7 +22,8 @@
"id": "tetris",
"name": "Tetris",
"ver": "1.0",
"desc": "The classic falling-block puzzle. Swipe left/right to move, up to rotate, down to hard-drop. Contributed by samuelcoustet."
"desc": "The classic falling-block puzzle. Swipe left/right to move, up to rotate, down to hard-drop. Contributed by samuelcoustet.",
"seed": false
},
{
"id": "protreck",
@@ -35,13 +36,15 @@
"id": "sdktest",
"name": "SDK Test",
"ver": "1.7",
"desc": "Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, private file access, audio API, read-only SD listing, crypto against published RFC vectors, channel discovery, and buttons that TRANSMIT test messages."
"desc": "Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, private file access, audio API, read-only SD listing, crypto against published RFC vectors, channel discovery, and buttons that TRANSMIT test messages.",
"requires": "sdk_ext"
},
{
"id": "2048",
"name": "2048",
"ver": "1.2",
"desc": "The classic sliding-tile puzzle. Swipe to move, tap to restart. Keeps your best score. Contributed by pisti87."
"desc": "The classic sliding-tile puzzle. Swipe to move, tap to restart. Keeps your best score. Contributed by pisti87.",
"seed": false
},
{
"id": "wardrive",
@@ -84,7 +87,8 @@
"id": "breakout",
"name": "Breakout",
"ver": "1.0",
"desc": "Classic brick-breaker. Swipe left/right to move the paddle, swipe up to launch the ball. Clear all bricks to advance levels. Contributed by samuelcoustet."
"desc": "Classic brick-breaker. Swipe left/right to move the paddle, swipe up to launch the ball. Clear all bricks to advance levels. Contributed by samuelcoustet.",
"seed": false
},
{
"id": "ping",
@@ -92,6 +96,31 @@
"ver": "1.0",
"desc": "Send a mesh DM ping to any contact and measure round-trip time. Includes your GPS position so both sides see distance and bearing. Auto-replies to incoming pings. Contributed by samuelcoustet.",
"requires": "sdk_ext"
},
{
"id": "mecp",
"name": "MECP",
"ver": "1.0",
"desc": "Emergency messages in MECP code: pick severity and codes, add people and position, post to a channel. Decodes MECP lines into words. Code table: MECP by xiang-dev-1, CC BY 4.0.",
"requires": "sdk_ext",
"seed": false
},
{
"id": "music",
"name": "Music",
"ver": "1.0",
"desc": "MP3 and WAV from the SD card's Music folder, shuffle, repeat.",
"icon": "music",
"requires": "audio",
"seed": false
},
{
"id": "blackjack",
"name": "Blackjack",
"ver": "1.0",
"desc": "Casino Blackjack (21) with split, double down, 6-deck shoe, persistent bankroll, and T-Deck/T-Display P4 controls. Contributed by halflive-743.",
"icon": "game",
"seed": false
}
]
}
+1
View File
@@ -0,0 +1 @@
{"id":"blackjack","name":"Blackjack","ver":"1.0","desc":"Casino Blackjack (21) with split, double down, 6-deck shoe, persistent bankroll, and auto-scaling UI for T-Deck & T-Display P4 (AMOLED/LCD).","icon":"game"}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Loaded 100 of 435 files, more files were not shown because too many files have changed in this diff. Show more