An allocation that fails inside library code (the web server's
operator new) aborts with only a backtrace. A failed-allocation
callback now prints the requested size, the caps, the function and
what the heap had left (free, largest block, minimum free), using the
ROM printf as the logger may need the heap itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PlatformIO converts an .ino into a fresh .cpp on every build, so the
main file recompiled and the firmware relinked even when nothing had
changed (about 25s of a no-change build of esp32dev). As a .cpp it
only rebuilds when it changes. The release assets keep their
SomfyController.ino.*.bin names, which the workflow sets itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The debug build logs at info level, and each line holds up the task that
logs it for ~9ms at 115200. At 921600 that drops to ~1ms. SERIAL_BAUD
defaults to 115200, so the other envs are unchanged. The ROM's boot
banner still prints at 115200 and shows as garbage in the monitor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A client that stops reading (a frozen browser tab) keeps its TCP link
alive, so neither AsyncTCP's ack timeout nor lwIP ever closes it. With
setCloseClientOnQueueFull(false) every message dropped for it printed a
warning, and each warning held up loop() for about 10ms on the UART.
One such client stayed full for 44 minutes and made every step of
loop() slow while shades moved.
Closing it frees the queue. The web UI and Home Assistant reconnect
and get the full state again from delayInit().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every socket event is formatted into one static buffer by one shared
formatter. setConnected() runs on the WiFi/Ethernet event task and sent
memStatus, wifiStrength and ethernet, then ran sockEmit.loop(), while
loop() could be building its own event. _safecat() checks the room and
then writes at _len, so another task moving _len in between could write
past the end of g_response.
setConnected() now sends nothing. loop() broadcasts the status every
STATUS_EMIT_MS (30 s): memStatus, then ethernet, or wifiStrength when it
moved 3 dBm or changed channel. A client that connects still gets all
of it at once from initClients(), so needsBroadcast and the 1.5 s tick
go, and so does the 1.5 s memStatus for the config panel.
SocketEmitter records loop()'s task in begin(). Off it, beginEmit()
logs and hands out a formatter that writes nothing, and endEmit(),
endEmitRoom() and loop() do nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only loop() is subscribed. setConnected() runs on the WiFi event task
and reached SocketEmitter::endEmit(), whose plain esp_task_wdt_reset()
logged "esp_task_wdt_reset(707): task not found" on every boot. The web
and socket handlers on async_tcp did the same.
/scanaps on async_tcp called esp_task_wdt_add(NULL) after the scan,
subscribing a task that nothing resets, so the board could panic 15 s
after a scan from the UI.
TaskWdt.h resets only a subscribed task and re-adds a task after a long
call only when it was watched before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
KNOWN_ISSUES.md loses the fixed entries, and the open ones that are
worth fixing are now issues #77-#86, listed at the top. The three
finished plans go: the LoopQueue decisions nothing else recorded move
into ADR-0002's consequences, the shade behaviour plan's open items
become #85 and #86, and the comments that pointed at the plans now say
what they meant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the coredump bounds fix (#74). Utils.h, Utils.cpp and
test_utils.cpp conflicted only because both sides added helpers in the
same spot; kept otaProven/releaseAssetName and formatBacktrace.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Arduino 3.x tracks which peripheral owns each pin and logs when a pin the
SPI bus owns is used through digitalRead()/digitalWrite(). The vendored
SmartRC-CC1101 driver does both:
- Every register access waits for CHIP_RDYn with digitalRead(MISO): one
"IO 19 is not set as GPIO" warning per access, about one every 4 s from
the RSSI read in decodeFrame(). It now reads the pad with gpio_get_level(),
which returns the same level without the ownership check.
- Init() set SCK and MOSI with digitalWrite() after SpiStart(), which the core
refused with an error each boot. The writes are gone; the bus idles them.
The MISO wait also had no bound, so a missing or dead module hung the caller.
It now gives up after 10 ms.
No native test: the driver only builds for the ESP32. To check on hardware:
the serial log should have no "is not set as GPIO" lines, and frames should
still decode with an RSSI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copied from the registry package lsatan/SmartRC-CC1101-Driver-Lib@2.5.7
(upstream unmaintained since 2021) so it can be fixed for Arduino 3.x in
the next commit. This commit is the untouched copy, so that diff shows
every change. Licence: LICENSE.txt ("permission to modify and publish").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setup() prints the stored coredump summary on every boot and trusted its
backtrace depth. A half-written dump reads back as the 0xa5 stack fill, so
depth was 0xa5a5a5a5: the loop read past bt[16], snprintf ran past its
256-byte buffer once the size went negative, and the interrupt watchdog
reset the board. The dump stays in flash, so every boot hit it again
(seen 2026-10-21: 162 TG1WDT resets in a row, each stopping after "Cause:").
formatBacktrace() in Utils reads at most bt[]'s 16 frames and stops when the
buffer is full, and the line notes IDF's corrupted flag. The task name is
printed at most 16 chars, since exc_task isn't always terminated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up for #72.
- releaseAssetName() takes ConfigSettings::chipModel ("", "s3", "c3", "c6"),
so GitOTA no longer keeps its own chip switch. C3 keeps its name for when
the build comes back.
- hasFS is set only when this board's own LittleFS image is in the release.
isFsAsset() is gone; it also matched the old unsuffixed littlefs.bin, which
the updater no longer asks for.
- releaseAssetName() returns void: no caller checked the result and every
name fits currentFile.
- Both manual-upload hints name this board's file.
- KNOWN_ISSUES: the "Main" install can't work, the repo holds no .bin files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Releases publish SomfyController.littlefs.<chip>.bin, but the updater asked
for SomfyController.littlefs.bin. The download 404'd and the device rebooted
into the new firmware with the old web UI.
Also: hasFS now matches per-chip images, C6 picks its own firmware file, and
recoverFilesystem() points at cjkas instead of rstrouse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IDF 4.4 gets no more security fixes, and the planned RMT radio needs
IDF 5's RMT driver. See docs/adr/0003-pioarduino-idf55.md.
- Every env on one pinned pioarduino release. C6 drops its own
library list and flags but stays a debug build on huge_app.
- Release envs build at CORE_DEBUG_LEVEL=2 so the ESP32 image fits its
1.5 MB OTA slot (17 KB free); debug envs stay at 3.
- Drop the C3: its image is 76.5 KB over the OTA slot on IDF 5.5.
- CI installs pioarduino's Core and checks firmware.bin against the app
partition, failing under 8 KB free. PlatformIO's "Flash:" figure
reads ~45 KB low and passed a C3 image that doesn't fit.
- esp_task_wdt_reconfigure(): on 3.x the core already starts the watchdog
and esp_task_wdt_init() fails, so the C6 build kept the 5 s default.
- An updated firmware is kept only once it has run 60 s with the
network up; a reset before then rolls back to the previous one.
- ETH.begin() takes the 3.x argument order; Arduino 2.x branches gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Prepares the move to Arduino 3.x / ESP-IDF 5.5, which renumbers
eth_phy_type_t (ETH_PHY_GENERIC is now 0) and only has the RMII MAC
API on the original ESP32.
- Store the PHY chip as our own id (eth_phy_id_t, the 2.x values) and
translate it by name at ETH.begin(), so NVS, backups and the web UI
keep their numbers on either framework.
- Build Ethernet only where CONFIG_ETH_USE_ESP32_EMAC is set instead of
everywhere but the C6. C3 and S3 could never use it. A stored
Ethernet connection falls back to Wi-Fi on those chips.
- /networksettings reports hasEthernet; the UI hides the Ethernet
options without it.
- Fix the UI PHY list: 5 is the KSZ8041, add the KSZ8081 (6), drop the
DM9051, an SPI chip the MAC can't drive.
- SSDP takes its address from WiFi/ETH.localIP() instead of the
tcpip_adapter API IDF 5 removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NTPSettings::apply() waited up to 5 s in getLocalTime() for a clock that
can't be set before the network is up, then returned before it set TZ.
Every cold boot was 5 s slower and ran without its time zone. It now
calls configTzTime(), which sets the zone and starts SNTP without
waiting, and save() goes through apply(). /setgeneral checked
ntpServer twice, so a request that changed only the zone was ignored.
A restore ignored the result of ShadeConfigFile::restore() and rebooted
either way, so a bad file failed with no word to the user. The /restore
reply now validates the file and answers 400, which the restore dialog
shows, and an incomplete upload gets a 400 instead of no reply. A
failed restore changes nothing, so loop() no longer reboots after one.
/shades.tmp, a full copy of the backup with its rolling codes, is
deleted after every restore and after /updateShadeConfig.
The 6 extra bytes in the 2026-10-18 backup: TRANS_REC_SIZE stayed 74
when noiseDetection was added to the 80-byte transceiver record, so
every backup since declares it short. It is 80 now. The size check
stays a warning so those backups still restore, and the sum moves to
expectedFileBytes() with tests. The repeater record length is checked
as a warning only, since validate() also runs on shades.cfg at boot.
Commands for an unknown shade, tilt or group now log a warning, and a
socket that never pinged says so. KNOWN_ISSUES.md is updated, including
the real cause of the ~365 ms loop stall (radio_tx busy-waits each frame
on loop()'s core).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cold-boot 5 s wait in NTPSettings::apply() that also skips the time
zone, restores that reboot even when validation fails and leave
shades.tmp behind, the ~365 ms each moving shade command holds up
loop(), one unexplained slow /controller read, and three misleading or
missing log lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every WiFi connect took about 17.6 s. Our scan already picks the AP and
passes its BSSID and channel to WiFi.begin(), but the connect still ran
with WIFI_ALL_CHANNEL_SCAN. So the driver swept every channel again
before it sent the first auth frame. A capture of the join showed the AP
silent for 17.6 s, the radio on ch7 while the AP is on ch6, and then
auth, association and the handshake all done in 80 ms.
When the BSSID is known (the normal connect, the strongest-AP path and
roaming), use WIFI_FAST_SCAN: the driver starts on that channel and joins
the first match, and with bssid_set only that AP matches. The hidden-SSID
path still uses the all-channel scan, since there it is what finds the
strongest AP (rstrouse/ESPSomfy-RTS#294). Soft reboots now connect in
0.3-1.5 s.
Also log how long each join took and the AP's security mode, on the
connect line and on the roaming-scan lines, so a slow connect shows up.
One boot in the old logs connected in 2.47 s, and nothing in its log
explains it. After a power cut the first attempts can still fail on the
AP side; a soft reboot sends a deauth first and doesn't have that problem.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SECURITY_PLAN.md records the agreed design: an access table for port 80
and the 8081 alias, websocket handshake auth, CORS removal, a random
token key, /login throttling, and moving API and websocket to port 80.
KNOWN_ISSUES.md gets an entry for each open issue it fixes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Firmware and UI files update separately, so an old index.js can run
against firmware that no longer returns secrets. It sends "" for the
field it can't fill in, which cleared the WiFi passphrase (reboot into
AP mode) or the MQTT password.
The firmware now decides: "" keeps the stored secret, unless the same
request changes the SSID or MQTT username, then it clears it (open
network). /setNetwork compares the settings after applying them, so a
kept passphrase no longer triggers a reboot. The UI cache-buster moves
to 3.0.14d.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The settings routes returned the WiFi passphrase, MQTT password and the
login password and PIN in plain text, and the debug log printed them and
the API key. Responses now carry hasPassphrase/hasPassword/hasPin flags,
logs mask the values, and an omitted secret keeps the stored one.
The UI loads secret fields empty and treats a blank field as "keep" while
the SSID or username is unchanged. Clearing a secret without changing the
SSID or username is listed in docs/KNOWN_ISSUES.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
memStatus now carries loopRate (loop() passes per second) and loopMaxUs
(the longest pass, in microseconds) over the window since the last
broadcast. Only a broadcast closes the window, so a client connecting
does not wipe a slow pass before Home Assistant has seen it.
/discovery is formatted on the web server's task, so its memory block
reports the last closed window through atomics instead of reading the
counters the loop task is still filling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both are minor updates with no API change for us. The espressif32 7.x
platform and SmartRC-CC1101-Driver-Lib 3.x major updates are left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The restore skip, the short id range and the dropped group members move
from LOOP_QUEUE_PLAN.md's "still open" list to "fixed later". The Home
Assistant apikey item stays open until ESPSomfy-RTS-HA#4 merges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Socket events that don't fit the static buffer are logged and not sent.
The state events are small (shadeState is about 400 bytes), but
remoteFrame lists up to 300 pulse timings and real frames already came
to 1.8-2.1 KB against the 2 KB buffer. MAX_SOCK_RESPONSE is now 4096 in
SocketEmitter.h, and a static_assert next to remoteFrame fails the build
if 300 pulses of 10 digits no longer fit. KNOWN_ISSUES.md closes the
entry with the bounds of every event.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>