Prepares the move to Arduino 3.x / ESP-IDF 5.5, which renumbers
eth_phy_type_t (ETH_PHY_GENERIC is now 0) and only has the RMII MAC
API on the original ESP32.
- Store the PHY chip as our own id (eth_phy_id_t, the 2.x values) and
translate it by name at ETH.begin(), so NVS, backups and the web UI
keep their numbers on either framework.
- Build Ethernet only where CONFIG_ETH_USE_ESP32_EMAC is set instead of
everywhere but the C6. C3 and S3 could never use it. A stored
Ethernet connection falls back to Wi-Fi on those chips.
- /networksettings reports hasEthernet; the UI hides the Ethernet
options without it.
- Fix the UI PHY list: 5 is the KSZ8041, add the KSZ8081 (6), drop the
DM9051, an SPI chip the MAC can't drive.
- SSDP takes its address from WiFi/ETH.localIP() instead of the
tcpip_adapter API IDF 5 removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NTPSettings::apply() waited up to 5 s in getLocalTime() for a clock that
can't be set before the network is up, then returned before it set TZ.
Every cold boot was 5 s slower and ran without its time zone. It now
calls configTzTime(), which sets the zone and starts SNTP without
waiting, and save() goes through apply(). /setgeneral checked
ntpServer twice, so a request that changed only the zone was ignored.
A restore ignored the result of ShadeConfigFile::restore() and rebooted
either way, so a bad file failed with no word to the user. The /restore
reply now validates the file and answers 400, which the restore dialog
shows, and an incomplete upload gets a 400 instead of no reply. A
failed restore changes nothing, so loop() no longer reboots after one.
/shades.tmp, a full copy of the backup with its rolling codes, is
deleted after every restore and after /updateShadeConfig.
The 6 extra bytes in the 2026-10-18 backup: TRANS_REC_SIZE stayed 74
when noiseDetection was added to the 80-byte transceiver record, so
every backup since declares it short. It is 80 now. The size check
stays a warning so those backups still restore, and the sum moves to
expectedFileBytes() with tests. The repeater record length is checked
as a warning only, since validate() also runs on shades.cfg at boot.
Commands for an unknown shade, tilt or group now log a warning, and a
socket that never pinged says so. KNOWN_ISSUES.md is updated, including
the real cause of the ~365 ms loop stall (radio_tx busy-waits each frame
on loop()'s core).
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>
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>
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>
setConfig() swapped the config in and released the radio lock, and apply()
only ran later from save(), so the radio task could send queued jobs with
the new TX pin and settings on a CC1101 that wasn't set up for them.
setConfig() now swaps, saves and applies under one lock and save() goes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restoring a backup wrote the transceiver config in place from the async
task while the radio task could be transmitting with it.
Read the record into a copy and swap it in through the new
Transceiver::setConfig, which Transceiver::fromJSON now uses as well.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>