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>
The HTTP servers and MQTT command handling go to src/entryPoints/, the
socket emitter and the MQTT and socket output of shades, groups and rooms
to src/outputs/. GLOSSARY.md names what they become once split:
WebUiRoutes, ApiRoutes and MqttCommands in, MqttPublisher and
SocketEmitter out.
Only files and include paths change. esp32devdbg flash goes from
1,546,405 to 1,546,257 bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sending a command reads a remote's next rolling code, queues the frame and
commits the code, all on loop(). /setRollingCode, a shades restore and
/updateShadeConfig wrote the same codes from the web server task, so a set
could be undone by a command's commit, and a restore could save an older
code to NVS that survived the reboot. All three now go through the web
command queue to loop(). They don't use the radio, so they are never held
back or refused while it is busy.
/restore now answers after queueing the restore. A broken options string
used to send an error after the success answer had already gone out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/setMyPosition filled in a missing tilt with the lift favorite, so a
request without a tilt set the favorite tilt to the lift's. It also took
the stored position as a display one, which flipped it twice on a shade
with flipPosition. requestedFavoriteTilt() now gives the favorite tilt as
displayed. Home Assistant never calls /setMyPosition and the web UI always
sends a tilt, so only other API callers see the change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GLOSSARY.md gains the shade terms the new modules use (favorite, step,
sensor frame, sun flag, extend and retract) and the pulse decoder. The
split plan marks phase 2 done, with what changed in behaviour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The decoding of the time between received edges into frame bits moves out
of Transceiver::handleReceive unchanged into decodePulse(), with the RTS
timings and the receive buffer type. It stays in IRAM on the ESP32 and is
now covered by native tests that feed it the edges sendFrame() produces
for 56 and 80-bit first frames and repeats, a glitch and a broken pulse.
handleReceive keeps the noise guard, the edge timing and the RX queue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A received step and a group step each worked out where a step takes the
shade, the group copy with its own inline maths. stepTargets() in
ShadeMotion now does it for both: a tilt-only shade steps its tilt, an
integrated tilt turns its slats to the end first and then steps the lift,
and every other shade steps its lift.
Only edge cases change. A group step on a tilt-only shade already at the
end now goes through like a remote step, and a step that can't be sized
(no travel time) leaves the targets alone in both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The three places that act on the sun and wind each set different targets
for a tilt-only shade: the sun timer moved its lift, the no-sun and wind
timers moved tilt and lift, a group's sun flag moved its lift and only a
remote's sun flag moved its tilt. They now share sensorTargets(): a
tilt-only shade moves its tilt to the favorite tilt (or fully) and back to
0, and every other shade its lift, as Up and Down already do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sun and wind timers were spread over processFrame, checkMovement and
processInternalCommand. ShadeSensors now holds them and decides when the
weather extends or retracts a shade, when turning the sun flag on moves
it, and when wind blocks a remote Down. Each caller still sets the targets
itself, so no shade moves differently yet.
The timers are now 32-bit like millis(). As 64-bit values holding a 32-bit
millis() they fired at once after the rollover every 49.7 days. The debug
logs of each timer go: they were compiled out at CORE_DEBUG_LEVEL 3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
group_types, shade_types and tilt_types move out of Somfy.h unchanged into
a header without the shade model, so the shade maths in ShadeMotion and
the sun and wind rules can use tilt_types in the native tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop files the Arduino PlatformIO build never reads: the ESP32 SVD,
the ESP-IDF CMakeLists/idf_component.yml, the PlatformIO template
READMEs, and the already-ignored debug.cfg. Fix the esp32.vsd typo in
.gitignore so the SVD stays ignored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the Somfy.cpp split plan with phases 0 and 1 marked done, what the
split taught about flash size in the debug build, and the phase 2 work
still open. KNOWN_ISSUES.md now names the new files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only SomfyShade's behaviour is left: receiving frames, commands, moving to
targets and the favorite. Drop the includes and externs it no longer uses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Loading and committing the shade list, adding, finding and deleting rooms,
shades and groups, repeaters, and handing frames and radio events to the
shades move out of Somfy.cpp unchanged, with SomfyRoom's clear and save.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Linking shades, the group flags and direction, sending group commands and
saving move out of Somfy.cpp unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sending commands, repeats and sensor frames, rolling codes and the remote
flags move out of Somfy.cpp unchanged, with startsMovement(), which only
sendCommandAfter uses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setGPIOs, triggerGPIOs and usesPin, which drive the relay outputs of dry
contact and GPIO shades, move out of Somfy.cpp unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
validateJSON and fromJSON for shades, rooms, groups and the radio move out
of Somfy.cpp unchanged. Keeping every ArduinoJson parser in one file
shares its inlined helpers, which saves 2.8 KB of flash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The emitState and emitCommand events for rooms, shades, groups and the
controller move out of Somfy.cpp unchanged. Also drop the log tag from
SomfyMqtt.cpp, which never logs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The publish, unpublish and Home Assistant discovery code for rooms, shades
and groups moves out of Somfy.cpp unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CC1101 setup, receive decoding, radio task and frequency scan move out
of Somfy.cpp, and their types out of Somfy.h into Transceiver.h, which
Somfy.h includes. The code is unchanged apart from the bit length the
radio was set up for: bit_length becomes radioBitLength, shared with the
remotes that send without one.
The radio's fromJSON stays in Somfy.cpp for now so the JSON parsing can
move into one file: ArduinoJson costs flash in every file that uses it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At -Og GCC only caps inlining growth in units over large-unit-insns
(10000 instructions). Smaller files inline ArduinoJson's helpers into
every caller: moving the JSON parsing out of Somfy.cpp grew the image by
19 KB, past the 1.5 MB app partition. Treating every file as large keeps
inlining at the level the big files already had and frees 13 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
USE_NVS is never defined, so the NVS-era shade storage (loadLegacy,
SomfyShade::load and the NVS branches) never built. Also drop the
commented-out toJSON methods, snprintf emitters and unused CC1101 options,
useNVS() and sort_asc(), which nothing calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
loop(), socket client init and /discovery each read a step's time and
restarted the timer by hand. lap() does both. Also put the request timing
comment back on timeRequest() and say why /discovery logs its steps.
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>
List every web handler that still changes the shade model, add the
oversized /controller items that are left out without telling the client,
the /linkRemote rolling-code race, and the /setMyPosition tilt default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/unpairShade answers paired:false as soon as the unpair is queued. If TX
was suspended before loop() reached it, the Prog was dropped and the shade
stayed paired, with nothing to tell the client. The unpair now waits in the
queue until TX resumes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A websocket event too large for its buffer has been logged and not
sent since 8881289, not sent truncated. Retitle the entry and point at
MAX_SOCK_RESPONSE by name rather than a stale line number.
- List the web server task handlers that still change the shade model
loop() owns: /setPaired, /setRollingCode and restore.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clientMap[], connectedAt[] and lastPing[] were parallel arrays indexed by
the same slot. They are now one ws_slot_t per slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The repeater-frame flag was called raw and the ticket was seq, jobSeq,
stopSeq or mySeq depending on the file. They are now repeater and ticket
(stopTicket, myTicket, lastTicket) throughout, matching the glossary.
Renames only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Transceiver::flushTx() had no caller; suspendTx() is the only flush.
RadioScheduler::hasRoom() was only ever asked about a normal job, so it no
longer takes a priority flag. The tests check the reserved stop slots
through submit() instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- setMyPosition stored a new favorite the same way in each of its three
tilt branches; storeMyPosition() now does it. The plain-lift branch
now emits its state even when the radio refuses the favorite, which is
harmless since sendSetMy() has already put the old one back.
- tiltRepeats() replaces the repeated tiltmotor ? TILT_REPEATS : repeats.
- timeRequest's two timing log blocks share logRequestTime().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
processFrame worked out a StepUp/StepDown target with the same formula in
eight places, each with its own divide-by-zero guard. 4dd4a98 fixed one
that guarded on downTime but divided by upTime. stepPosition() takes the
travel time once, for both the guard and the maths, so they can't drift
apart again, and it is now covered by native tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
queueUnpairShade cleared shade->paired on the async_tcp task while loop()
owns the shade. A second unpair queued before the first ran recorded
wasPaired=false and, if its Prog was refused, put back the wrong value.
The shade is now only changed by loop() once the Prog is queued, and the
response reports paired=false through a serializeShade overload instead.
No native test: Web.cpp can't be built by the native env.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moveToTiltTarget, sendTiltCommand and setMyPosition returned nothing, so
the web command queue counted tilt_command and set_my_position as sent
and never logged a refusal. They now return false when the radio refused
a command they sent.
No native test: these live in Somfy.cpp, which the native env can't build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>