116 lines
3.0 KiB
Markdown
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.
|