# 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")
```
