mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
test: start the e2e suite for socket protocol 2
A pytest suite run by hand against a device (test/e2e/README.md): hello, the schema and its hash, the sync with next and resync, replies to bad frames, feed subscriptions, the status event, every client syncing at once, a sixth client refused, a page load, and the socket under the device's security mode. It never transmits, changes settings or reboots. --record saves hello, the sync and the schema as fixtures. PROTOCOL.md now describes the protocol as built. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
482e495678
commit
10521cf78d
9 changed files with 685 additions and 20 deletions
|
|
@ -1,51 +1,89 @@
|
|||
# Socket protocol 2
|
||||
|
||||
**Status:** outline. The type numbers, field keys and payloads are filled in with the firmware protocol core. Why the protocol looks like this: [ADR 0005](adr/0005-one-websocket-protocol.md). Terms: [GLOSSARY.md](../GLOSSARY.md).
|
||||
Why the protocol looks like this: [ADR 0005](adr/0005-one-websocket-protocol.md). Terms: [GLOSSARY.md](../GLOSSARY.md).
|
||||
|
||||
**The table of types, fields, error codes and feeds is defined once, in [src/ProtoSchema.h](../src/ProtoSchema.h).** The device sends it to every client as the schema, and `test/fixtures/proto2/schema.json` is a copy recorded by the e2e suite (`pytest test/e2e --record`). This page describes how the protocol works, not every field.
|
||||
|
||||
## Connection
|
||||
|
||||
- `ws://host/ws` on port 80. Under Secured the upgrade needs `?apikey=<token>` ([ADR 0006](adr/0006-two-security-modes-eleven-http-routes.md)).
|
||||
- Optional `?schema=<hash>`: the client already has that schema and doesn't need it again.
|
||||
- Text frames only. A binary encoding, if one comes, would be asked for at the upgrade (for example `?enc=bin`) and use binary frames.
|
||||
- `ws://host/ws` on port 80. Under Secured the upgrade needs `?apikey=<token>` from `/login`; without a valid one it is refused with 401 ([ADR 0006](adr/0006-two-security-modes-eleven-http-routes.md)).
|
||||
- Optional `?schema=<hash>`: the client already has the schema with that hash and doesn't need it again.
|
||||
- At most 5 clients. A sixth is closed as soon as it connects.
|
||||
- Text frames only, and each message in one frame: a fragmented message is answered with a bad request. A binary encoding, if one comes, would be asked for at the upgrade (for example `?enc=bin`) and use binary frames.
|
||||
|
||||
## Envelope
|
||||
|
||||
Every frame, in both directions, is one JSON object:
|
||||
|
||||
```json
|
||||
{"v":2,"t":20,"id":17,"d":{}}
|
||||
{"v":2,"t":129,"id":17,"d":{"s":3,"p":40}}
|
||||
```
|
||||
|
||||
| Key | Meaning |
|
||||
|---|---|
|
||||
| `v` | protocol version |
|
||||
| `t` | message type, a number from the table below |
|
||||
| `id` | only on an op and its reply: the client's number for the op |
|
||||
| `d` | the payload, with short keys from the schema |
|
||||
| `v` | protocol version, 2 |
|
||||
| `t` | message type, a number from the schema |
|
||||
| `id` | only on a client message that wants a reply, and on that reply |
|
||||
| `d` | the payload, with keys from the schema's field dictionary |
|
||||
|
||||
Within `d`, booleans are `1`/`0` and fields at their default are left out. Keys are one or two characters from one dictionary shared by every type.
|
||||
Within `d`:
|
||||
- keys are one or two characters, from one dictionary shared by every type;
|
||||
- booleans are `1` and `0`;
|
||||
- a field at its default is left out: a client fills it in from the schema;
|
||||
- lists and objects use the same dictionary for the keys inside them.
|
||||
|
||||
The keys of `hello` (`fw`, `sh`) and of the schema itself (`T`, `F`, `E`, `Q`) never change, since a client needs them before it has a schema.
|
||||
|
||||
## Message types
|
||||
|
||||
| Range | Sender | Kinds |
|
||||
| Range | Sender | Types |
|
||||
|---|---|---|
|
||||
| 1–15 | either | control: `hello`, `reply`, `next`, `sync`, `syncEnd`, `subscribe`, `unsubscribe` |
|
||||
| 16–127 | client | ops: the commands, reads, model edits and `getSettings`/`saveSettings` |
|
||||
| 128–255 | device | events: `shadeState`, `shade`, `group`, `room`, `*Removed`, `settingsChanged`, status, feeds |
|
||||
| 1–15 | control | `hello` 1, `reply` 2, `next` 3, `sync` 4, `syncEnd` 5, `subscribe` 6, `unsubscribe` 7 |
|
||||
| 16–127 | client ops | none yet; commands, model edits and settings follow |
|
||||
| 128–255 | device events | `shadeState`, `shade`, `shadeRemoved`, `group`, `groupState`, `groupRemoved`, `room`, `roomRemoved`, `roomOrder`, `shadeOrder`, `groupOrder`, `shadeCommand`, `memory`, `wifi`, `ethernet`, `frame`, `frequencyScan`, `repeaters` |
|
||||
|
||||
A number is never reused. A retired type stays reserved.
|
||||
|
||||
## Connect sequence
|
||||
|
||||
1. The device sends `hello`: protocol version, firmware version, schema hash.
|
||||
2. Unless the client passed the matching `?schema=`, the sync starts with the schema: types, fields (key, name, kind, default) and error codes.
|
||||
3. Sync: header, rooms, shades, groups, repeaters, status. Each message is at most 4 KB, items are never split, and the client sends `next` after each one.
|
||||
4. `syncEnd`. From then on, events keep the client current. Events also arrive during the sync, so the client upserts by id.
|
||||
1. The device sends `hello`: `{"fw":"<firmware>","sh":"<schema hash>"}`.
|
||||
2. The device sends the first `sync` frame straight away. Unless the client passed the matching `?schema=`, the sync starts with the schema:
|
||||
- `T`: types, as `[number, name]`
|
||||
- `F`: fields, as `[key, name, kind, default]`, kind being `b` bool, `i` integer, `n` number, `s` string, `l` list, `o` object
|
||||
- `E`: error codes, as `[number, name]`
|
||||
- `Q`: feeds, as `[number, name]`
|
||||
3. Then the device: `header` (limits, starting address, what a new shade gets, server id, hostname, model, chip, firmware, connection type), `rooms`, `shades`, `groups`, `repeaters` and `status` (memory, and WiFi or Ethernet). A frame is at most 4 KB, an item is never split across frames, and several sections can share one.
|
||||
4. The client sends `next` after each `sync` frame, and the device answers with the following one. With no `next` for 30 s the device closes the socket.
|
||||
5. `syncEnd` follows the last `sync` frame.
|
||||
|
||||
The client can send `sync` at any time to get the shade model and status again (without the schema).
|
||||
|
||||
Events go out to every client from the moment it connects, so they can arrive during the sync, and even before the schema. A client keeps any frame it can't decode yet until it has the schema. Each sync frame is formatted from the live model, so an item in a later frame already holds every change before it. A client treats sync items and events alike: insert or update by id, and ignore removing an id it doesn't have.
|
||||
|
||||
## Events
|
||||
|
||||
| Event | When | Payload |
|
||||
|---|---|---|
|
||||
| `shadeState` | a shade moved or its state changed | id, position, target, direction, favorite, tilt, flags, rolling code |
|
||||
| `shade` | a shade was added or a setting changed (edit, pairing, linked remote, group, its room deleted) | the whole shade, as in the sync |
|
||||
| `group`, `groupState` | the same for a group: its settings or linked shades, or its flags and direction | |
|
||||
| `room` | a room was added or saved | the whole room |
|
||||
| `*Removed` | a shade, group or room was deleted | its id |
|
||||
| `roomOrder`, `shadeOrder`, `groupOrder` | the sort order changed | `is`: the ids in order |
|
||||
| `repeaters` | a repeater was linked or unlinked | the list |
|
||||
| `shadeCommand` | 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 | |
|
||||
|
||||
## Replies
|
||||
|
||||
Every op gets exactly one reply, to its sender only: `{"ok":1, ...}`, or `{"ok":0,"code":<n>,"desc":"..."}`. The error codes are bad request, unknown op, unauthorized, not found, busy, radio unavailable and internal; their numbers are in the schema.
|
||||
A client message with an `id` gets exactly one `reply` with that `id`, to that client only: `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.
|
||||
|
||||
A malformed message gets a bad request, with its `id` when it had one. A type a client may not send gets an unknown op.
|
||||
|
||||
## Feeds
|
||||
|
||||
`subscribe`/`unsubscribe` with a feed: `frames` (received remote frames) or `frequencyScan`. Feed events go only to subscribed clients.
|
||||
`subscribe` and `unsubscribe` take a feed number in `fe`: `frames` (received remote frames, with their pulses) or `frequencyScan`. Their events go only to clients subscribed to them.
|
||||
|
||||
## Inside the device
|
||||
|
||||
async_tcp reads client frames and answers with fixed frames that hold nothing of the shade model: `hello`, errors and `ok`. Everything formatted from the shade model goes out from `loop()`, which is the only task that reads and writes it ([ADR 0002](adr/0002-loop-task-is-the-only-writer-of-the-shade-model.md)). Sync frames and events share the socket event buffer, so protocol 2 needs no more static memory.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue