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>
The read routes leave out a room, shade, group or linked shade that
doesn't fit the 3 KB unit, without telling the client (KNOWN_ISSUES.md).
test_snapshot_limits fills a snapshot with the largest possible items and
checks every read document written with the real unit is identical to
the same document written with an unlimited one. The largest item, a
shade, is 1,097 bytes. The unit is now SNAPSHOT_UNIT_SIZE in
ModelSnapshot.h, used by Web.cpp and the tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
onWaitingFrame only acts on a frame that isn't processed yet, and the
step rule in onFrame marks a step frame processed as soon as it arrives;
a repeat or synonym keeps the mark. So the "fixed 1% step of a held step
button" never ran. A new test pins what a held step button does: one
step by the frame's step size. SOMFY_SPLIT_PLAN.md is corrected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fromJSON read linkedShades into a local array and dropped it, so /addGroup
and /saveGroup never changed a group's members; /linkToGroup and
/unlinkFromGroup do, with ModelEditor's checks. The loop also wrote past
the end of that stack array when a request listed more than 32 shades.
No client sends the list: the web UI's group form has no members field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New ids stopped two short of the limits, at 30 shades, 14 groups and 14
rooms instead of 32, 16 and 16, while the web UI showed the full limits.
The original loop ran i < MAX - 1, an off-by-one: nothing indexes by id
or treats the last two ids as special. ModelSnapshot's next-id answers
and SomfyShadeController's allocation now run from 1 to the maximum.
Found during LOOP_QUEUE_PLAN.md's review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
esp32_huge_app.csv had a 0xD0000 file system where the release table has
0xE0000 at the same offset. LittleFS can grow but not shrink, so flashing
the debug build onto a device formatted by a release build failed the
lfs_fs_grow assert on every boot. The debug table now uses 0xE0000 and
moves the coredump into the 64 KB that was unused at the end of flash,
so both builds share one data layout. Devices formatted at 0xD0000 (debug,
esp32c6) grow on their next mount.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A restore without "shades" skipped the shade and group records but not
the room records, which come first, so it read the repeaters and the
settings from room data. The skip is now shadeModelBytes(), in a
ConfigHeader.h the native tests can reach, which counts rooms, shades
and groups. Found during LOOP_QUEUE_PLAN.md's review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The web and group Up/Down put a tilt-only shade's lift back at 100, but a
remote's Up/Down/My, a group's My and the web's My while moving left a
lift that something had moved, and the simulation then ran it. Every
Up/Down/My path now pins the lift at 100 and acts on the tilt only.
Reconcile Q22 in SHADE_BEHAVIOUR_PLAN.md, which also records Q23 (the
integrated tilt turn-back) as deferred.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 500 ms wait that tells a short press from a long one compared
now > await, so a press just before the wrap acted at once. It now
compares the wrap-safe difference. Reconcile Q21 in
SHADE_BEHAVIOUR_PLAN.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A SunFlag or Flag frame makes every group work its flags out again, but
the same command from a group only updated that group, so other groups
holding the shade kept stale sun flags. The group rule now reports the
sensor flags as the frame rule does. Reconcile Q20 in
SHADE_BEHAVIOUR_PLAN.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The step rule read lastFrame.stepSize, which copy() never updates, so a
remote's step moved by whatever step size this shade last sent. It now
reads the frame's step size, 1 when the frame has none. Reconcile Q19 in
SHADE_BEHAVIOUR_PLAN.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Arriving at a lift target never cleared settingPos (the tilt arrival
clears settingTiltPos), so the next step or group move also sent a stop
when it arrived. A stop to a motor that has already stopped is a My,
which sends it to its favorite. Reconcile Q18 in SHADE_BEHAVIOUR_PLAN.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR B of SHADE_BEHAVIOUR_PLAN.md. What a shade does with frames, commands,
group commands and radio events moves unchanged from SomfyShade into
ShadeBehaviour, which builds natively and is pinned by test_shadebehaviour.
ShadeBehaviour reads ShadeSettings and RemoteSettings, gets the time on
every input, and holds the motion state that other files now read through
state(). It sends through MotorLink and reports through ShadeEvents
(changes, commands, save wanted, sensor flags) as things happen.
SomfyShade wraps it: its MotorLink is SomfyRemote::sendCommandAfter, which
now leaves the sender out of the echo and radio hold because the module
hears its own frame. lastFrame stays shared with the send path.
ConfigFile writes through restore() and clearTilt(), SomfyShadeOps through
setPosition(). The pinning tests label where the copies of a rule disagree
"today:", for the reconcile phase.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shade behaviour module (SHADE_BEHAVIOUR_PLAN.md, PR B) reads its
settings through read-only structs, so they get one home each:
RemoteSettings (flipCommands, bitLength, repeats) under SomfyRemote,
shared with groups, and ShadeSettings (types, travel times, step size,
flipPosition, simMy, sunSensor) under SomfyShade. Both are base structs,
so shade->upTime and group->repeats keep working.
A shade's flags byte is split by owner: simMy and sunSensor are settings,
light is a field, and the sun flag, sunny, windy, demo mode and lighted
bits are stateFlags. getFlags()/setFlags() put the byte together and take
it apart for the config file, JSON, MQTT, events and group flags, so
nothing saved or sent changes. Groups keep their own flags byte.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records the design state for candidate #1 of the architecture review so a
new session can pick it up: the drifted Up/Down/My copies in SomfyShade.cpp,
the echo path through SomfyRemote::sendCommandAfter, what steps 1-5 already
did, round 1 of the design questions with the recommended answers, and the
topics waiting for round 2.
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>
The p_* setters and emitState() no longer publish or emit straight away:
they set SHADE_CHANGED_* / GROUP_CHANGED_* bits on the shade or group.
At the end of every loop() pass SomfyShadeController::sendChanges()
publishes the MQTT topics whose bits are set and sends one shadeState or
groupState, then clears the bits. emitState(evt) with an event name and
emitState(num) for one client still go out at once.
publishShadeTopics() (ShadeChanges.cpp, tested natively) writes the topics
from a shade snapshot; publishState() on connect uses it too. That fixes
MQTT position and target, a constant 50 since c94819e, and direction, a
constant 0 on connect. position and tiltPosition are now retained when
they change, as they were on connect, and a group whose flags change sends
one groupState instead of two. somfy_flags_t moves to ShadeTypes.h.
8 new native tests, 327 in all. Flash: esp32devdbg +196 bytes, esp32dev
+228 bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 19 routes that changed rooms, shades, groups and repeaters on the web
server task (add, save, delete, link and unlink, setPaired, the sort
orders) now copy their request into a model_edit_t, queue it on the
LoopQueue and reply {status:accepted,requestId:N}. loop() applies it
(SomfyJson.cpp, next to fromJSON) and broadcasts requestResult {requestId,
ok, id, code, desc}. One edit at a time; a second gets 503, a body over
4 KB gets 413.
ModelEditor.h/.cpp holds the rules, tested natively: the lowest free id,
the next sort order, a free remote address, adding to and removing from
the group-member and repeater lists, and the GPIO pin-conflict check. They
replace the getNext*Id/getMax*Order copies, the remote-address search,
the list code and the inline pin checks. The pin check now uses the
protocol being saved and checks every pin against Ethernet, not only up.
/getNextRoom, /getNextShade, /getNextGroup, /groupOptions and a new
/repeaters (the UI already called it) are ModelSnapshot reads.
The web UI's editJSON() sends an edit, waits for its requestResult, then
reads the item back from /shade, /group, /room or /repeaters, so the
callbacks that used the old reply bodies are unchanged. No result within
5 s offers a reload; reads and edits told Busy are retried. It now also
refreshes its lists on shadeAdded/shadeRemoved/groupAdded/groupRemoved
from other clients, and procRoomRemoved keeps the right rooms.
Also fixed: a shade that fails validation is no longer added; linked
repeaters and sort orders are saved; linking reports a full list.
27 new native tests, 319 in all. Flash: esp32devdbg -11,484 bytes,
esp32dev -7,180 bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/controller, /discovery, /rooms, /shades, /groups, /room, /shade and /group
now take a SnapshotSlots slot, queue a fill on the LoopQueue and send a
chunked response whose body waits (RESPONSE_TRY_AGAIN) until loop() has
copied the shade model into the slot. SnapshotWriter then streams the
document from that copy a unit at a time, so each document describes one
moment. At most two reads run at once; a third gets 503. A slot's 5.6 KB
snapshot is allocated only while a read holds it, and its atomic state
decides whether loop() or the read frees it when the read goes away first.
The JSON of each item is unchanged and now lives in ModelSnapshot.cpp,
where the native tests check it. Replies still written on the web server
task copy their one item with SomfySnapshot.cpp, so serializing a group
no longer calls updateFlags(), which emitted groupState and published MQTT
from async_tcp.
Also: every read route is chunked and leaves out an item too big for its
3 KB unit, like /controller did; /discovery drops its timing log; the
SOMFY_MAX_* limits move to ShadeTypes.h.
21 new native tests, 292 in all. Flash: esp32devdbg +440 bytes, esp32dev
+2,064 bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records the design state for candidate #1 of the architecture review so a
new session can pick it up: the drifted Up/Down/My copies in SomfyShade.cpp,
the echo path through SomfyRemote::sendCommandAfter, what steps 1-5 already
did, round 1 of the design questions with the recommended answers, and the
topics waiting for round 2.
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>
The p_* setters and emitState() no longer publish or emit straight away:
they set SHADE_CHANGED_* / GROUP_CHANGED_* bits on the shade or group.
At the end of every loop() pass SomfyShadeController::sendChanges()
publishes the MQTT topics whose bits are set and sends one shadeState or
groupState, then clears the bits. emitState(evt) with an event name and
emitState(num) for one client still go out at once.
publishShadeTopics() (ShadeChanges.cpp, tested natively) writes the topics
from a shade snapshot; publishState() on connect uses it too. That fixes
MQTT position and target, a constant 50 since c94819e, and direction, a
constant 0 on connect. position and tiltPosition are now retained when
they change, as they were on connect, and a group whose flags change sends
one groupState instead of two. somfy_flags_t moves to ShadeTypes.h.
8 new native tests, 327 in all. Flash: esp32devdbg +196 bytes, esp32dev
+228 bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 19 routes that changed rooms, shades, groups and repeaters on the web
server task (add, save, delete, link and unlink, setPaired, the sort
orders) now copy their request into a model_edit_t, queue it on the
LoopQueue and reply {status:accepted,requestId:N}. loop() applies it
(SomfyJson.cpp, next to fromJSON) and broadcasts requestResult {requestId,
ok, id, code, desc}. One edit at a time; a second gets 503, a body over
4 KB gets 413.
ModelEditor.h/.cpp holds the rules, tested natively: the lowest free id,
the next sort order, a free remote address, adding to and removing from
the group-member and repeater lists, and the GPIO pin-conflict check. They
replace the getNext*Id/getMax*Order copies, the remote-address search,
the list code and the inline pin checks. The pin check now uses the
protocol being saved and checks every pin against Ethernet, not only up.
/getNextRoom, /getNextShade, /getNextGroup, /groupOptions and a new
/repeaters (the UI already called it) are ModelSnapshot reads.
The web UI's editJSON() sends an edit, waits for its requestResult, then
reads the item back from /shade, /group, /room or /repeaters, so the
callbacks that used the old reply bodies are unchanged. No result within
5 s offers a reload; reads and edits told Busy are retried. It now also
refreshes its lists on shadeAdded/shadeRemoved/groupAdded/groupRemoved
from other clients, and procRoomRemoved keeps the right rooms.
Also fixed: a shade that fails validation is no longer added; linked
repeaters and sort orders are saved; linking reports a full list.
27 new native tests, 319 in all. Flash: esp32devdbg -11,484 bytes,
esp32dev -7,180 bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/controller, /discovery, /rooms, /shades, /groups, /room, /shade and /group
now take a SnapshotSlots slot, queue a fill on the LoopQueue and send a
chunked response whose body waits (RESPONSE_TRY_AGAIN) until loop() has
copied the shade model into the slot. SnapshotWriter then streams the
document from that copy a unit at a time, so each document describes one
moment. At most two reads run at once; a third gets 503. A slot's 5.6 KB
snapshot is allocated only while a read holds it, and its atomic state
decides whether loop() or the read frees it when the read goes away first.
The JSON of each item is unchanged and now lives in ModelSnapshot.cpp,
where the native tests check it. Replies still written on the web server
task copy their one item with SomfySnapshot.cpp, so serializing a group
no longer calls updateFlags(), which emitted groupState and published MQTT
from async_tcp.
Also: every read route is chunked and leaves out an item too big for its
3 KB unit, like /controller did; /discovery drops its timing log; the
SOMFY_MAX_* limits move to ShadeTypes.h.
21 new native tests, 292 in all. Flash: esp32devdbg +440 bytes, esp32dev
+2,064 bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HTTP routes and MQTT commands now both build a shade request and submit it
to the LoopQueue, which loop() drains into ShadeRequests. ShadeRequests
finds the shade or group, turns client positions into stored ones, checks
ranges and fills in default repeats, and reaches the shades through the
ShadeOps seam: SomfyShadeOps on the device, a recording fake in the native
tests. MqttCommands turns a .../set topic into a shade request.
Behaviour that changes:
- MQTT tiltTarget, mypos, myTiltPos and tiltPosition, and /setPositions,
now honour flipPosition like the other routes.
- MQTT mypos keeps the favorite tilt (was set to 0) and myTiltPos keeps
the favorite lift.
- repeat 0 on /shadeCommand means no repeats, as it did for groups; a
JSON repeat over 127 is capped instead of wrapping to the default.
- Position corrections go through the setters, so MQTT publishes them.
- MQTT commands wait while the radio is full instead of being refused.
- The queue is drained every loop() pass and no longer from inside a
GitHub firmware download.
WebCommand.h is replaced by LoopQueue.h and ShadeRequests.h. 45 new native
tests, 271 in all. Flash: esp32devdbg +976 bytes, esp32dev +1,380 bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
esp32devdbg used 98.3% of the 1.5 MB OTA slot. On esp32_huge_app.csv the
same 1,546,405-byte image uses 49.2%, so the refactors ahead have room.
The release envs keep esp32_3MB.csv and CI keeps them within limits.
NVS stays at 0x9000, but the file system shrinks from 0xE0000 to 0xD0000,
so a board moving to this table needs a backup, a USB flash with
uploadfs, and a restore.
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>