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>
9.7 KiB
Plan: split and refactor Somfy.cpp
Status: done. Phases 0 and 1 in bbfb23e..505108c, phase 2 in 0eb0e63..a176017 (2026-10-13).
Written: 2026-10-13, against commit 4544eb6 on fix/live-bugs. Line numbers in the analysis below refer to that commit. The code now lives in the files listed under Phase 1.
Why
src/Somfy.cpp is 4,766 lines. About 690 of them never compile. Six classes, two config structs and the receive interrupt handler share the file. The methods of each class are scattered: SomfyShade's 71 methods are spread over about 20 separate runs. Some logic exists in several copies that have already drifted apart (see Problems).
What the file contains
| What | Lines | Live lines (approx.) | Changes when… |
|---|---|---|---|
Dead code: /* */ blocks and #ifdef USE_NVS blocks (USE_NVS is never defined) |
everywhere | ~690 (392 + 296) | never |
Radio: Transceiver, transceiver_config_t, receive interrupt and pulse decoder, radio task, frequency scan, RadioLock/TxQueueLock |
1–116, 3700–4766 | ~950 | the radio hardware or RX/TX changes |
MQTT: publish*/unpublish* for shades, groups and rooms, plus Home Assistant discovery |
980–1348, 3113 | ~400 | the MQTT topics change |
Socket events: emitState/emitCommand |
1451–1611, 3105 | ~90 | the socket protocol changes |
JSON config: validateJSON/fromJSON |
2775–3066 | ~200 | the web config form changes |
GPIO and dry contacts: setGPIOs, triggerGPIOs, usesPin |
570–695, 2758 | ~140 | the relay wiring changes |
SomfyRemote: rolling codes, sendCommandAfter, repeatFrame |
scattered | ~200 | how frames are sent changes |
Shade behaviour: processFrame, processInternalCommand, processWaitingFrame, checkMovement, my-position, moveTo*, sendCommand |
696–923, 1612–2706 | ~1,350 | what a shade does |
SomfyShadeController and SomfyRoom: add/delete/find, ids, commit |
117–300, 3076–3700 | ~600 | the shade list changes |
Problems beyond file size
- The same logic exists in several copies, and the copies have drifted apart.
- Step targets.
processFrameuses the testedstepPosition()fromShadeMotion.processInternalCommand(~2172–2242) has its own inline maths, which ignores the frame's step count.processWaitingFrameuses a fixed 1% step. - Sun/wind. Sensor state is set in the
Sensorcase ofprocessFrame, acted on by timers incheckMovement, and checked again in theDowncase ofprocessInternalCommand(windLast). The SunFlag handling exists twice. These look like real bugs for tilt-only shades:- the copy in
processFrame(~1865) moves the tilt target of a tilt-only shade; - the copy in
processInternalCommand(~2262) moves the lift target instead; - the sun timer in
checkMovement(~744) also moves the lift target of a tilt-only shade, while the no-sun timer two branches later moves the tilt.
- the copy in
- Step targets.
- The receive pulse decoder has no tests.
handleReceive(~3839–3969) turns pulse durations into bits. It is pure logic, so recorded pulse trains could test it in the native env, the waySomfyFrameis tested. SomfyShadehas a wide interface: 6publishoverloads, 12p_*setters,publicfields, and seven sun/wind fields (timestamps and done flags) that are really one sensor state.- Hidden dependencies. Shade methods use the global
somfyforisDirty,commit(),transceiver.configandupdateGroupFlags(). This plan leaves them alone. Removing them belongs to the "onlyloop()owns the shade model" work in KNOWN_ISSUES.md.
Plan
Phase 0: delete the dead code
Done in bbfb23e. sort_asc() was dead too (only called from a dead block) and went with it.
One commit, no behaviour change.
- Remove every
#ifdef USE_NVSblock (296 lines), includingloadLegacy()andSomfyShade::load(). Also removeuseNVS(): it sits outside the blocks, but only the blocks call it. - Remove the 16 commented-out blocks (392 lines), mostly old
toJSONversions andsnprintfemitters. - Remove the matching declarations in
Somfy.h.
This takes the file to about 4,050 lines before anything moves.
Phase 1: split the file by concern
Done in 0a329ca..505108c, one commit per file. Code moved unchanged, apart from bit_length, which became the shared radioBitLength. The 253 top-level definitions before and after match one for one.
Several commits. Code moves as it is: the classes and Somfy.h's interface stay the same, only the files change. A class's methods may end up in several files by concern. Check each move with the esp32devdbg build and the native tests.
| New file | Lines | Contents |
|---|---|---|
Transceiver.h/.cpp |
125 + 949 | Transceiver, transceiver_config_t, RadioLock/TxQueueLock and the radio statics, the receive interrupt handler and RX queue, the radio task, the frequency scan, and somfy_frame_t::decodeFrame(somfy_rx_t*) (it needs RadioLock). Somfy.h includes Transceiver.h, so callers don't change. |
SomfyRemote.cpp |
170 | SomfyRemote and SomfyLinkedRemote |
SomfyShade.cpp |
1,532 | shade behaviour (the rest of Somfy.cpp, renamed so history follows it) |
ShadeGpio.cpp |
148 | the GPIO and dry-contact methods |
SomfyGroup.cpp |
134 | SomfyGroup behaviour |
ShadeController.cpp |
449 | SomfyShadeController and SomfyRoom. Not SomfyController.cpp, which would look like a pair with SomfyController.ino. |
SomfyMqtt.cpp |
412 | every publish*/unpublish*/publishDisco |
SomfyEvents.cpp |
99 | every socket emitState/emitCommand |
SomfyJson.cpp |
235 | validateJSON/fromJSON for shades, groups and rooms, and the radio's (see below) |
Flash size: what the split taught us
The esp32devdbg app partition is 1,572,864 bytes, and before the split the image used 99.4% of it.
- At
-Og, GCC only caps inlining growth in units overlarge-unit-insns(10,000 instructions). The bigSomfy.cppwas over that, so ArduinoJson's helpers stayed calls. In smaller files they were inlined into every caller: moving the JSON parsing out grew the image by 19 KB, past the limit.--param=large-unit-insns=1000in[env:esp32devdbg](4cb676a) treats every file as large. That freed 13 KB, and the split itself then cost nothing. - Every file that uses ArduinoJson also gets its own copy of ArduinoJson's debug-assert strings, and its own copies of helpers it inlines. So all JSON parsing lives in
SomfyJson.cpp, including the radio'sfromJSON, which the plan had kept inTransceiver.cpp. Grouping it saved 2.8 KB.SomfyMqtt.cpp(publishDiscobuilds aJsonDocument) is the only other new file that uses ArduinoJson. - Result: 1,546,797 bytes (98.3%), 16.8 KB less than before phase 0.
For phase 2, keep new modules free of ArduinoJson, and check the flash size of every commit. If space runs out, remove log lines first.
Phase 2: pull out the copied logic
Done in 0eb0e63..a176017. Each module is plain C++ like ShadeMotion, with native tests (222 tests in all now):
| Module | What it holds |
|---|---|
ShadeTypes.h |
group_types, shade_types and tilt_types, moved out of Somfy.h so the modules below can use them in the native tests. |
ShadeSensors.h/.cpp |
The sun and wind timers and their rules: onSensorFrame(), check(), onSunFlag(), windBlocksDown(). sensorTargets() turns extend/retract into targets for all three callers. |
stepTargets() in ShadeMotion |
Where a step takes a shade, shared by received steps (processFrame) and group steps (processInternalCommand). |
PulseDecoder.h/.cpp |
decodePulse(), the RTS timings and somfy_rx_t, moved out of handleReceive. It stays in IRAM (0x4008xxxx), and the tests feed it the edges sendFrame() produces. |
What changed in behaviour:
- Tilt-only shades and the weather (
5f26340): sun and wind now move only the tilt of a tilt-only shade, to the favorite tilt (or fully) and back to 0, and only the lift of every other shade. Before, each of the three callers did something different. - Sun and wind timers across the
millis()rollover: the timers are now 32-bit likemillis(). As 64-bit values they fired at once after the rollover every 49.7 days. This also saves 768 bytes of RAM. - Step edge cases: a group step on a tilt-only shade already at its end now goes through like a remote step. A step that can't be sized (no travel time) leaves the targets alone.
- The 8 debug logs of the sun and wind timers are gone. They were compiled out at
CORE_DEBUG_LEVEL=3.
Kept as it was: the fixed 1% step of a held step button in processWaitingFrame.
Left out on purpose, since neither fixes a bug or can be reached by the native tests:
- the four near-copy arrive-at-target blocks in
checkMovement; - the six
getNext*Id/getMax*Ordercopies in the controller.
Flash went from 1,546,797 to 1,545,717 bytes.
Risks
- IRAM. The decoder runs inside the interrupt and must stay in IRAM. Watch the build for
dangerous relocationor IRAM link errors. - Flash. See Flash size.
- Inlining flag.
--param=large-unit-insns=1000changes inlining across the whole debug build. Check on the device that radio RX/TX still work.
Decisions
Decided for phases 0 and 1:
- Phases ran in the order 0 → 1.
- Headers:
Somfy.hstays the one header, with onlyTransceiver.hsplit out. - Log tags: every file keeps
"Somfy". Files that never log (SomfyMqtt.cpp,SomfyEvents.cpp) have no tag.
Decided for phase 2:
- The tilt-only sun/wind bugs were fixed in their own commit, right after
ShadeSensorswas extracted with behaviour unchanged. - A tilt-only shade's lift is never moved by the weather.