mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
feat: settings ops, the WiFi and frequency scans, and reboot for socket protocol 2
Step 1d of the protocol 2 core (docs/SECURITY_PLAN.md). - getSettings/saveSettings by section: general, network, mqtt, security, radio. A save is the settings route's request, so loop() applies it with the settings' own fromJSON(). ProtoSettings checks every field first: kind, range, string length, IP addresses. - Secrets are only reported as has*. A security save replies with the client's new token and closes the other sockets; other saves send settingsChanged to the other clients. - scanWifi runs an async scan from ESPNetwork, kept apart from the roaming scan, and wifiNetworks goes to the clients that asked. - beginFrequencyScan, endFrequencyScan and reboot run at once on async_tcp, as their routes do: a scan's end mustn't wait in the loop queue behind an unpair that waits for the scan to end (new op kind 'a'). - e2e: test_settings.py and a --reboot flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
97a4e98183
commit
2517f40709
22 changed files with 1221 additions and 49 deletions
|
|
@ -39,8 +39,8 @@ The keys of `hello` (`fw`, `sh`) and of the schema itself (`T`, `F`, `E`, `Q`) n
|
|||
| Range | Sender | Types |
|
||||
|---|---|---|
|
||||
| 1–15 | control | `hello` 1, `reply` 2, `next` 3, `sync` 4, `syncEnd` 5, `subscribe` 6, `unsubscribe` 7 |
|
||||
| 16–127 | client ops | commands: `shadeCommand` 16, `groupCommand` 17, `tiltCommand` 18, `repeatCommand` 19, `setPositions` 20, `setSensor` 21, `setMyPosition` 22, `unpairShade` 23, `setRollingCode` 24, `sendRemoteCommand` 25; edits: 26–44 (`addRoom` … `groupSortOrder`); reads: `getNextRoom` 45, `getNextShade` 46, `getNextGroup` 47, `groupOptions` 48; settings follow |
|
||||
| 128–255 | device events | `shadeState`, `shade`, `shadeRemoved`, `group`, `groupState`, `groupRemoved`, `room`, `roomRemoved`, `roomOrder`, `shadeOrder`, `groupOrder`, `shadeCommanded`, `memory`, `wifi`, `ethernet`, `frame`, `frequencyScan`, `repeaters` |
|
||||
| 16–127 | client ops | commands: `shadeCommand` 16, `groupCommand` 17, `tiltCommand` 18, `repeatCommand` 19, `setPositions` 20, `setSensor` 21, `setMyPosition` 22, `unpairShade` 23, `setRollingCode` 24, `sendRemoteCommand` 25; edits: 26–44 (`addRoom` … `groupSortOrder`); reads: `getNextRoom` 45, `getNextShade` 46, `getNextGroup` 47, `groupOptions` 48; settings and the device: `getSettings` 49, `saveSettings` 50, `scanWifi` 51, `beginFrequencyScan` 52, `endFrequencyScan` 53, `reboot` 54 |
|
||||
| 128–255 | device events | `shadeState`, `shade`, `shadeRemoved`, `group`, `groupState`, `groupRemoved`, `room`, `roomRemoved`, `roomOrder`, `shadeOrder`, `groupOrder`, `shadeCommanded`, `memory`, `wifi`, `ethernet`, `frame`, `frequencyScan`, `repeaters`, `settingsChanged`, `wifiNetworks` |
|
||||
|
||||
A number is never reused. A retired type stays reserved.
|
||||
|
||||
|
|
@ -73,6 +73,8 @@ Events go out to every client from the moment it connects, so they can arrive du
|
|||
| `repeaters` | a repeater was linked or unlinked | the list |
|
||||
| `shadeCommanded` | a remote or the device commanded a shade | id, address, command, source, rolling code, source address |
|
||||
| `memory`, `wifi`, `ethernet` | every 30 s, WiFi only when the signal moved | |
|
||||
| `settingsChanged` | another client saved settings | `section` |
|
||||
| `wifiNetworks` | a WiFi scan this client asked for is done; to that client only | `networks`, each with `ssid`, `channel`, `strength`, `macAddress` |
|
||||
|
||||
## Commands
|
||||
|
||||
|
|
@ -124,6 +126,44 @@ What an edit form needs before it is sent, worked out by the device as the edit
|
|||
|
||||
When every slot is taken the reply is a bad request; `groupOptions` for a group that doesn't exist is not found.
|
||||
|
||||
## Settings
|
||||
|
||||
The settings are in five sections. `getSettings` and `saveSettings` name one in `section`:
|
||||
|
||||
| Section | Fields | The routes it replaces |
|
||||
|---|---|---|
|
||||
| `general` | `hostname`, `ssdpBroadcast`, `ntpServer`, `posixZone` | `/modulesettings`, `/setgeneral` |
|
||||
| `network` | `connMode`; WiFi: `ssid`, `passphrase`, `roaming`, `hidden`; Ethernet: `boardType`, `phyType`, `CLKMode`, `phyAddress`, `PWRPin`, `MDCPin`, `MDIOPin`; the address: `dhcp`, `ip`, `gateway`, `subnet`, `dns1`, `dns2` | `/networksettings`, `/setNetwork`, `/setIP`, `/connectwifi` |
|
||||
| `mqtt` | `enabled`, `pubDisco`, `protocol`, `hostname` (the broker's), `port`, `username`, `password`, `rootTopic`, `discoTopic` | `/mqttsettings`, `/connectmqtt` |
|
||||
| `security` | `securityType`, `username`, `password`, `pin`, `permissions` | `/getSecurity`, `/saveSecurity` |
|
||||
| `radio` | `bitLength`, `proto`, `enabled`, `TXPin`, `RXPin`, `SCKPin`, `MOSIPin`, `MISOPin`, `CSNPin`, `frequency`, `rxBandwidth`, `deviation`, `txPower`, `noiseDetection` | `/getRadio`, `/saveRadio` |
|
||||
|
||||
- **Reading.** `getSettings` replies with the section's fields. A secret is never sent: `hasPassphrase`, `hasPassword` and `hasPin` say whether one is set. `network` also has `hasEthernet`, and has the Ethernet fields only on a device with Ethernet. `radio` also has `radioInit`.
|
||||
- `connMode` is the connection the settings ask for: 1 WiFi, 2 Ethernet, 3 Ethernet with WiFi as the fallback. The header's `connType` is the connection in use.
|
||||
- **Saving.** `saveSettings` takes the section and the fields to change. As with every op, a field left out stays as it is. A secret sent empty keeps the stored one, except alongside a new `ssid` or MQTT `username`, as on the routes.
|
||||
- Every field is checked before anything is saved:
|
||||
- its kind;
|
||||
- its range: `port` 1–65535, `txPower` -30–12, `bitLength` 56 or 80, a pin 0–48, and so on;
|
||||
- a string's length, which is what the setting holds: `hostname` 31, `pin` 4, and so on;
|
||||
- an address: four numbers 0–255, or `""` for none.
|
||||
|
||||
A bad request also comes for a field the section doesn't save (a `has*` field, `radioInit`, a field of another section, Ethernet on a device without it), and for a save with no fields.
|
||||
- One save runs at a time, whichever client sent it: while one is waiting, another gets busy.
|
||||
- The reply comes once the settings are saved and applied, as their route applies them. A saved time zone takes effect at once, where the route waited for a reboot.
|
||||
- `rebooting` 1 means the device restarts a second later for them. That happens for a new `connMode`, for a new SSID or passphrase while on WiFi with nobody on the soft AP, and for Ethernet settings while on Ethernet.
|
||||
- A security save's reply has the client's new token in `apiKey`, or none under None.
|
||||
- The other clients get `settingsChanged` with the section. After a security save they are closed instead, since their tokens may no longer be good. They connect again with what they have, and log in when it is refused.
|
||||
- `/connectwifi` refused an SSID the device couldn't hear; the save doesn't check. The check stalled the device for a scan, and a hidden network can't be heard anyway. A client offers the networks from `scanWifi`.
|
||||
|
||||
| Op | Payload | Reply |
|
||||
|---|---|---|
|
||||
| `scanWifi` | | ok at once. When the scan is done, `wifiNetworks` goes to each client that asked since the last one: the named networks that can be heard, at most 30. A failed scan lists none. |
|
||||
| `beginFrequencyScan` | | ok, or radio unavailable when the radio is disabled. The radio sends nothing until the scan ends. Subscribe to the `frequencyScan` feed to follow it. |
|
||||
| `endFrequencyScan` | | ok. A scan nobody ends stops after 5 minutes. |
|
||||
| `reboot` | | ok, and the device restarts half a second later. |
|
||||
|
||||
The frequency scan ops and `reboot` run at once, as their routes do, not through `loop()`. A scan's end mustn't wait in the loop queue behind an unpair that is itself waiting for the scan to end.
|
||||
|
||||
## Replies
|
||||
|
||||
A client message with an `id` gets exactly one `reply` with that `id`, to that client only, whether it is an op or a control message such as `next`: `ok` 1 on success, or an error `code` and a `desc` for people. Error codes are in the schema: bad request, unknown op, unauthorized, not found, busy, radio unavailable, internal.
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
- **Handshake:** the socket is `ws://host/ws` on port 80. The token goes in `?apikey=<token>`, because browsers can't set headers on a websocket. `handleHandshake` checks it with the same comparison the HTTP routes use. `request->url()` is the path only, so the token doesn't reach the slow-request log.
|
||||
- **Rules:** under None anyone can connect. Under Secured the handshake needs a valid token, and a connection that has one may send every op and subscribe to every feed.
|
||||
- **Logging in with a socket already open:** the UI reconnects the socket with the token. There is no in-band upgrade message.
|
||||
- **Changing the security settings:** a `saveSettings` of the security section closes every other socket, since their tokens may no longer be good, and a socket opened under None had none. They connect again and log in when refused. The client that saved gets its new token in the reply.
|
||||
- **Origin:** a handshake whose `Origin` header doesn't match `Host` is refused.
|
||||
|
||||
### CORS
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue