firmware-base/docs/ws-updates.md

116 lines
3.0 KiB
Markdown

# WebSocket Compact Update Format Proposal
## 1. Problem Statement
The current WebSocket update mechanism uses verbose JSON payloads for frequent events.
**Example Payload:**
```json
{"type":"register_update","data":{"slaveId":13,"address":1235,"fc":3,"count":1,"id":633,"value":166}}
```
**Issues:**
- High network overhead (~100 bytes/update).
- JSON parsing/serializing CPU cost on ESP32.
## 2. Proposed Solution: Hybrid Binary Protocol
Use **WebSocket Binary Frames** for high-frequency updates while maintaining **Text Frames** for JSON (backward compatibility).
### Key Features
- **Efficiency:** ~10-12 bytes per update (vs ~100 bytes).
- **Performance:** Direct memory replication (C++ struct) -> DataView (JS).
- **Compatibility:** Legacy clients ignore binary frames; new clients handle both.
## 3. Binary Specification (Little Endian)
### Packet Structure
`[Type (1B)] [Payload (Variable)]`
| Hex | Type | Description |
| :--- | :--- | :--- |
| `0x01` | `COIL_UPDATE` | Single Coil Update |
| `0x02` | `REGISTER_UPDATE` | Single Register Update |
### 3.1 Packet 0x01: COIL_UPDATE (10 Bytes)
| Offset | Field | Type | Description |
| :--- | :--- | :--- | :--- |
| 0 | `Type` | `UInt8` | `0x01` |
| 1 | `SlaveId` | `UInt8` | Modbus Slave ID |
| 2 | `Fc` | `UInt8` | Function Code (e.g. 5) |
| 3 | `Value` | `UInt8` | 0 or 1 |
| 4 | `ComponentId` | `UInt16` | Internal ID |
| 6 | `Address` | `UInt16` | Modbus Address |
| 8 | `Count` | `UInt16` | Count (usually 1) |
### 3.2 Packet 0x02: REGISTER_UPDATE (12 Bytes)
| Offset | Field | Type | Description |
| :--- | :--- | :--- | :--- |
| 0 | `Type` | `UInt8` | `0x02` |
| 1 | `SlaveId` | `UInt8` | Modbus Slave ID |
| 2 | `Fc` | `UInt8` | Function Code (e.g. 3) |
| 3 | `Reserved` | `UInt8` | Padding |
| 4 | `ComponentId` | `UInt16` | Internal ID |
| 6 | `Address` | `UInt16` | Modbus Address |
| 8 | `Count` | `UInt16` | Count (usually 1) |
| 10 | `Value` | `UInt16` | Register Value |
## 4. Implementation
### Server (C++)
Define packed structs:
```cpp
#pragma pack(push, 1)
struct BinaryRegisterUpdate {
uint8_t type = 0x02;
uint8_t slaveId;
uint8_t fc;
uint8_t reserved;
uint16_t componentId;
uint16_t address;
uint16_t count;
uint16_t value;
};
#pragma pack(pop)
```
Send using `ws.binaryAll((uint8_t*)&update, sizeof(update));`.
### Client (TypeScript)
Handle binary frames:
```typescript
ws.binaryType = 'arraybuffer';
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
handleBinary(new DataView(event.data));
} else {
// legacy JSON
}
};
function handleBinary(view: DataView) {
const type = view.getUint8(0);
if (type === 0x02) {
const value = view.getUint16(10, true); // Little Endian
// Dispatch update
}
}
```
## 5. Alternative: Compact Text
If binary is not feasible, use delimiter-separated strings:
`RU|13|3|633|1235|1|166`
- **Pros:** Human readable.
- **Cons:** Larger, string parsing overhead.