The release check, release list, download, cancel and
/recoverFilesystem go, with the GitHub button and the Auto Check for
Updates setting in the web UI (docs/adr/0007-no-github-update.md).
The HTTPS session it needed failed for want of a 35-40 KB block (#76),
it checked no certificate, and the web UI's buttons had been getting
404. Updates are uploads only (/updateFirmware, /updateApplication).
The old socket still sends fwStatus with the installed versions, which
the web UI and Home Assistant read. checkForUpdate stays in NVS and the
backup file so their formats don't change.
The release image shrinks by 126.5 KB: 142 KB of the OTA slot is free.
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>
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>
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>
Code moves unchanged; only files and names change.
- entryPoints/WebUiRoutes.cpp: registerWebUiRoutes(), the port 80 routes
and the UI's files, with the helpers only they use.
- entryPoints/ApiRoutes.cpp: registerApiRoutes(), the port 8081 routes.
- entryPoints/Web.cpp keeps the servers' lifecycle, auth and the handlers
both route sets call; entryPoints/WebRoutes.h is what the three share.
- entryPoints/MQTT.* -> MqttConnection.*, class MQTTClass -> MqttConnection.
- outputs/SomfyMqtt.cpp -> outputs/MqttPublisher.cpp.
- outputs/Sockets.* -> outputs/SocketEmitter.*.
The subscribe and unsubscribe lists (the latter repeated
groups/+/direction/set) become MqttCommands' MQTT_COMMAND_TOPICS, and a
native test checks the parser takes every topic in it. GLOSSARY.md gains
MqttConnection and Change fan-out.
328 native tests. Flash: esp32devdbg +3,112 bytes (the routes are a second
ArduinoJson file), esp32dev -2,336 bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>