209 lines
7.4 KiB
Markdown
209 lines
7.4 KiB
Markdown
# UDPStreamer Wire Protocol
|
||
|
||
This document specifies the binary protocol used between UDPStreamer (server) and any
|
||
compatible client (the included Go WebUI, a Python script, etc.).
|
||
|
||
All multi-byte integers are **little-endian**.
|
||
|
||
---
|
||
|
||
## Packet Header (17 bytes, packed)
|
||
|
||
Every datagram begins with a 17-byte header:
|
||
|
||
```
|
||
Offset Size Type Field
|
||
────── ──── ────── ────────────────────────────────────────────────────
|
||
0 4 uint32 magic = 0x53504455 ('UDPS' LE)
|
||
4 1 uint8 type see Packet Types below
|
||
5 4 uint32 counter per-update sequence number
|
||
(same across all fragments of one update)
|
||
9 2 uint16 fragmentIdx 0-based index of this fragment
|
||
11 2 uint16 totalFragments number of fragments for this update
|
||
13 4 uint32 payloadBytes bytes of payload following this header
|
||
```
|
||
|
||
**Total header size:** 17 bytes
|
||
**Magic:** `0x55 0x44 0x50 0x53` (`UDPS`)
|
||
|
||
---
|
||
|
||
## Packet Types
|
||
|
||
| Value | Direction | Name | Description |
|
||
|-------|-----------|------|-------------|
|
||
| 0 | Server → Client | DATA | Signal data (may be fragmented) |
|
||
| 1 | Server → Client | CONFIG | Signal metadata sent on connect |
|
||
| 2 | Client → Server | ACK | Acknowledge a data counter (reserved) |
|
||
| 3 | Client → Server | CONNECT | Request a session |
|
||
| 4 | Client → Server | DISCONNECT | End the session |
|
||
|
||
---
|
||
|
||
## Session Flow
|
||
|
||
```
|
||
Client Server
|
||
────── ──────
|
||
CONNECT (type=3) →
|
||
← CONFIG (type=1)
|
||
← DATA (type=0) ┐
|
||
← DATA (type=0) │ repeated every RT cycle
|
||
← DATA (type=0) ┘
|
||
DISCONNECT (type=4) →
|
||
```
|
||
|
||
1. Client sends a 17-byte CONNECT packet (`payloadBytes = 0`).
|
||
2. Server responds immediately with one or more CONFIG fragments describing all signals.
|
||
3. Server sends DATA fragments on every `Synchronise()` call while a client is connected.
|
||
4. Client sends DISCONNECT to terminate cleanly. A new CONNECT replaces an existing session.
|
||
|
||
---
|
||
|
||
## CONFIG Payload
|
||
|
||
The CONFIG payload is sent as one or more fragmented packets (`type = 1`).
|
||
After reassembly the layout is:
|
||
|
||
```
|
||
Offset Size Type Field
|
||
────── ──── ─────── ────────────────────────────────────
|
||
0 4 uint32 numSignals
|
||
── for each signal (136 bytes) ──────────────────────────────
|
||
0 64 char[64] name null-terminated
|
||
64 1 uint8 typeCode see Type Codes
|
||
65 1 uint8 quantType see Quantization Types
|
||
66 1 uint8 numDimensions 0 = scalar, 1 = 1-D array, 2 = matrix
|
||
67 4 uint32 numRows 0 or 1 for scalar/1-D
|
||
71 4 uint32 numCols number of elements along fastest axis
|
||
75 8 float64 rangeMin
|
||
83 8 float64 rangeMax
|
||
91 1 uint8 timeMode see Time Modes
|
||
92 8 float64 samplingRate Hz (0 if PacketTime)
|
||
100 4 uint32 timeSignalIdx index of the time-reference signal;
|
||
0xFFFFFFFF = PacketTime (no reference)
|
||
104 32 char[32] unit null-terminated physical unit string
|
||
── (total per signal: 136 bytes) ────────────────────────────
|
||
```
|
||
|
||
### Type Codes
|
||
|
||
| Code | C type | Bytes/element |
|
||
|------|--------|---------------|
|
||
| 0 | uint8 | 1 |
|
||
| 1 | int8 | 1 |
|
||
| 2 | uint16 | 2 |
|
||
| 3 | int16 | 2 |
|
||
| 4 | uint32 | 4 |
|
||
| 5 | int32 | 4 |
|
||
| 6 | uint64 | 8 |
|
||
| 7 | int64 | 8 |
|
||
| 8 | float32 | 4 |
|
||
| 9 | float64 | 8 |
|
||
|
||
### Quantization Type Codes (wire side)
|
||
|
||
| Code | Wire type | Description |
|
||
|------|-----------|-------------|
|
||
| 0 | — | No quantization; raw type as above |
|
||
| 1 | uint8 | Linear map `[rangeMin, rangeMax]` → `[0, 255]` |
|
||
| 2 | int8 | Linear map `[rangeMin, rangeMax]` → `[-127, 127]` |
|
||
| 3 | uint16 | Linear map `[rangeMin, rangeMax]` → `[0, 65535]` |
|
||
| 4 | int16 | Linear map `[rangeMin, rangeMax]` → `[-32767, 32767]` |
|
||
|
||
### Time Mode Codes
|
||
|
||
| Code | Name | Meaning |
|
||
|------|------|---------|
|
||
| 0 | PacketTime | HRT timestamp at `Synchronise()` — see DATA payload |
|
||
| 1 | FullArray | `timeSignalIdx` signal has same `numElements`; element `[k]` time = `timeSignal[k]` |
|
||
| 2 | FirstSample | `timeSignalIdx` is scalar; `t[k] = t[0] + k / samplingRate` |
|
||
| 3 | LastSample | `timeSignalIdx` is scalar; `t[k] = t[N-1] - (N-1-k) / samplingRate` |
|
||
|
||
---
|
||
|
||
## DATA Payload
|
||
|
||
After reassembly, the DATA payload layout is:
|
||
|
||
```
|
||
Offset Size Type Field
|
||
────── ──── ────── ────────────────────────────────────────────────────
|
||
0 8 uint64 hrtTimestamp hardware reference timer count at Synchronise()
|
||
── for each signal (in config order) ────────────────────────────────────
|
||
varies N×sz — signal data N = numRows×numCols, sz = element size
|
||
(wire size if quantized, raw size otherwise)
|
||
```
|
||
|
||
Signal data for quantized signals uses the wire element size (see Quantization Type Codes),
|
||
not the original MARTe2 type size.
|
||
|
||
### Dequantization
|
||
|
||
To recover physical values from quantized integers:
|
||
|
||
```
|
||
// uint16 → float
|
||
span = rangeMax - rangeMin
|
||
physical = rangeMin + (wire_uint16 / 65535.0) × span
|
||
|
||
// int16 → float
|
||
physical = rangeMin + ((wire_int16 + 32767) / 65534.0) × span
|
||
```
|
||
|
||
---
|
||
|
||
## Fragmentation
|
||
|
||
When a payload exceeds `MaxPayloadSize` bytes, it is split into fragments:
|
||
|
||
```
|
||
chunkSize = MaxPayloadSize - 17 // usable bytes per datagram
|
||
numFragments = ceil(payloadSize / chunkSize)
|
||
```
|
||
|
||
Fragment `i` carries bytes `[i × chunkSize .. min((i+1) × chunkSize, payloadSize))`.
|
||
All fragments share the same `counter`; `fragmentIdx` and `totalFragments` allow
|
||
the client to reassemble them in any order.
|
||
|
||
**Example:** `MaxPayloadSize = 1400`, payload = 8016 B
|
||
→ `chunkSize = 1383`, `numFragments = ceil(8016/1383) = 6`
|
||
|
||
---
|
||
|
||
## Minimal Python Client Example
|
||
|
||
```python
|
||
import socket, struct, time
|
||
|
||
MAGIC = 0x53504455
|
||
HDR_FMT = '<IBHHI' # magic, type, counter, fragIdx, totalFrags, payloadBytes
|
||
HDR_SIZE = 17
|
||
|
||
def build_connect():
|
||
return struct.pack(HDR_FMT, MAGIC, 3, 0, 0, 1, 0)
|
||
|
||
def parse_header(data):
|
||
return struct.unpack_from(HDR_FMT, data)
|
||
|
||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
sock.bind(('', 44900))
|
||
sock.sendto(build_connect(), ('127.0.0.1', 44500))
|
||
sock.settimeout(5.0)
|
||
|
||
fragments = {}
|
||
while True:
|
||
data, _ = sock.recvfrom(65536)
|
||
magic, ptype, counter, frag_idx, total_frags, payload_bytes = parse_header(data)
|
||
payload = data[HDR_SIZE:]
|
||
|
||
if ptype == 1: # CONFIG
|
||
print(f"CONFIG fragment {frag_idx+1}/{total_frags}")
|
||
elif ptype == 0: # DATA
|
||
fragments.setdefault(counter, {})[frag_idx] = payload
|
||
if len(fragments[counter]) == total_frags:
|
||
full = b''.join(fragments.pop(counter)[i] for i in range(total_frags))
|
||
hrt = struct.unpack_from('<Q', full)[0]
|
||
print(f"DATA counter={counter} hrt={hrt} payload={len(full)}B")
|
||
```
|