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>
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>
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>
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>
- 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>
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>
- 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>
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>
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>
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>
#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>
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>
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
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
#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>