Step 4a of docs/SECURITY_PLAN.md. Port 80 keeps the eleven HTTP routes of
ADR 0006 and the socket at /ws; everything else moved to protocol 2.
- Port 8081 (ApiRoutes) and the socket on 8080 (SocketEmitter) are gone.
Protocol 2 owns its frame buffer and loop-task check.
- Port 80 drops every route the old web UI called: reads, edits,
settings, scans, reboot, /shades.cfg, /discovery. HttpRoutes.cpp holds
what stays.
- ModelSnapshot, SnapshotSlots and the read documents are gone. The item
snapshot types stay, as ItemSnapshot, for events, MQTT and the command
replies.
- Model edits reply only to their protocol 2 op: requestResult goes.
- The settings' toJSON writers, used only by the settings routes, go.
- The _espsomfy_rts._tcp mDNS record moves to port 80, with path=/ws.
- The memory event reports the loop-rate window it closes.
- A deleted room no longer re-publishes its retained MQTT topics.
- e2e: the heap check reads the sync's status; the sync is checked on
its own instead of against /controller.
- README gets an API and ports section.
BREAKING CHANGE: ports 8081 and 8080 and the old HTTP routes are gone.
Home Assistant needs integration 3.0.0 or newer.
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>
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>
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>
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>
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>
Move the roam check into worthRoaming() and the memStatus routing into
heapEmitTarget() in Utils so the native tests reach them.
worthRoaming() roams for a gain of at least ROAM_MIN_GAIN_DB, as the
comment and the 6 dB commit said. The old check needed one dB more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The emit to a newly connected client also stored the RSSI and time, which
restarted the 10s hold-off and hid a real change from the clients already
connected. The baseline moves into RssiBroadcast, which ignores
per-client emits, with unit tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At -70 dBm the RSSI moves a dBm or two constantly, so the >1 dBm check
every 1.5s sent wifiStrength to every client about 270 times an hour.
That is extra traffic on an already lossy link and a state write in
Home Assistant for each one.
Broadcast on a channel change, or for a change of at least 3 dBm no
more than every 10s. A newly connected client still gets the current
value straight away.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>