The C5 builds like the C6: a debug build with one 3 MB app slot, so it
updates over USB. At CORE_DEBUG_LEVEL=2 its image left 3.4 KB of the
1.5 MB OTA slot, under CI's 8 KB.
- platformio.ini: env esp32c5 (esp32-c5-devkitc-1).
- Chip model "c5", so the updater and releases name its files
SomfyController.*.esp32c5.bin.
- Radio defaults: the framework's SPI pins (CSN 6, MOSI 8, MISO 9,
SCK 10), TX 4, RX 5.
- Web UI: GPIO 0-28, without 13-14 (USB) and 15-22 (flash, PSRAM).
- CI builds and releases it; its bootloader sits at 0x2000.
- ADR 0003: amended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the old API gone, the C3 image is 1,498,512 bytes at
CORE_DEBUG_LEVEL=2 and leaves 72.6 KB of its 1.5 MB OTA slot. It was
76.5 KB over when ESP-IDF 5.5 dropped it.
- platformio.ini: env esp32c3, single core, esp32_3MB.csv, level 2.
- CI builds and releases it with the other chips.
- ADR 0003: amended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The routes matched any path below them, and a web asset name there
passed the token check: GET /backup/a.js returned the backup under
Secured. Every route is now an exact match, and a web asset sits in
the root only.
- e2e: those paths answer 401 or 404; the restore test now fails when
the chunks skip the token check (checked with the check removed).
- WEB_CORS lets the socket take Origin: null instead of skipping the
Origin check.
- ADR 0006, GLOSSARY and README: the Origin check in either mode, /ws
as a public route, the Host header behind a reverse proxy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- e2e: the heap check uses a socket slot now, so it waits for the device
to free it, and test_every_client_reconnecting_at_once closes its
clients before the check runs.
- Comments and docs that still named removed things (the /controller read,
the event buffer, memStatus, SocketEmitter, ModelSnapshot).
- ESPNetwork::emitSockets is emitStatus: one socket, and it sends the status.
- Unused includes, whitespace-only lines left by the toJSON removal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The throttle runs on a 64-bit clock in ms (esp_timer_get_time()), so a
slot left alone for 49.7 days isn't brought back by millis() wrapping
round to its old failure.
- Web.cpp turns the client into the throttle's key in one place, and
says that an IPv6 client would share key 0; names are clearer
(retryMs, vacantSlot).
- The shared slot's test now succeeds from the IP counted in it, so a
success that cleared the shared slot would fail it.
- GLOSSARY.md names it the login throttle, with lockouts, and
SECURITY_PLAN.md and ADR 0006 use that name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- One socket at a time: a socket being replaced is let go without a word,
and connect() or a new token while a reconnect waits opens no second one.
A new token reconnects at once, without a refused.
- Events that came before the schema go as soon as it is whole, ahead of
the sync frames formatted after them, not at syncEnd over newer state.
- A schema's hash counts only once it has come whole, so a sync cut short
gets it again.
- Feeds are subscribed again after a reconnect; an unknown feed is refused.
- A long op refused as busy without its id goes again.
- A sync without repeaters clears them; order changes name their list; a
refused resync is called off.
- refused: a listener can close() to stop the retries, which the UI does
once /loginContext says a login is needed. The client itself can't tell a
refused token from a device that is down.
- localStorage keeps the schema by default. minify.py removes its work
folder even when a bundle fails. ADR 0005 records the two bundles.
- Smaller: ProtoSchema.ROWS/withoutRows, ProtoModel.ITEMS, named type
numbers, unused feeds map and FakeSocket.all gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR 0005: the web UI and Home Assistant talk to the device over one
websocket protocol (ops on loop(), sync with next, a JSON envelope a
binary encoding can reuse, short keys described by a schema).
ADR 0006: two security modes and eleven HTTP routes on port 80.
ADR 0007: the firmware no longer updates itself from GitHub.
ADR 0002 is amended: its single-writer rule stands, but edits now get
one reply and reads no longer go through a ModelSnapshot.
SECURITY_PLAN.md is revised to match, and keeps the token key, login
backoff, CORS and Origin work. PROTOCOL.md starts as an outline, and
GLOSSARY.md gains the socket protocol and security terms.
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>
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>
ADR-0002 makes loop() the only writer of the shade model. The plan
puts ShadeRequests, ModelEditor and ModelSnapshot behind one LoopQueue
that the HTTP routes and MQTT commands feed, with the outputs fed by
per-field dirty bits. GLOSSARY.md gains the terms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
loop() and the async web server task shared one global Preferences. A
begin() from one task failed while the other had a namespace open, and the
clear() or remove() after it then hit the other task's namespace: saving the
radio settings while a command committed its rolling code could erase every
rolling code.
Each function now opens NVS with a local Preferences. NVS serializes access
internally and the destructor closes the handle, so no lock is added (see
docs/adr/0001). No new tests: nothing here builds natively, and deleting the
global makes any missed use a compile error.
The read-then-commit races on rolling codes across tasks remain and are
tracked in docs/KNOWN_ISSUES.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>