# Frame protocol
{.lead}
The UDP packets the block sends to `nmproxy` in proxy mode, for anyone writing their own sender
or relay.
Each UDP packet holds one frame, of one of three types:
- A HELLO announces a dataset RID, an ordered list of channel names, the tags every channel
carries, and the [run](#the-run) to make in Nominal.
- A DATA frame carries rows: a timestamp and one value per channel, in the HELLO's order.
- An END says the sender has stopped, and when its rows end.
Every frame carries a `config_id`, a hash of the RID, the channel names and the tags, and a DATA
frame is matched to its HELLO through the `config_id`.
A sender sends a HELLO when it starts and repeats it every second for as long as it runs. A
relay that starts, or restarts, partway through an execution learns the channel list within a
second, and drops DATA frames whose `config_id` it hasn't seen in a HELLO until then. When the
sender stops, it sends END three times, 50 ms apart. Nothing travels back from the relay to the
sender, so nothing confirms that a frame arrived, and frames carry no authentication.
Version 1 is the version described here.
## Encoding
Every numeric field is little-endian. The magic is the four bytes `NMS1` in that order, so it
reads forwards in a packet capture. No frame is longer than 1400 bytes, which keeps it out of IP
fragmentation.
Every frame starts with the same 24-byte header:
| Offset | Size | Field | Value |
|---|---|---|---|
| 0 | 4 | magic | `4E 4D 53 31`, `NMS1` |
| 4 | 1 | version | `1`. A relay rejects frames of any other version. |
| 5 | 1 | type | `1` for HELLO, `2` for DATA, `3` for END |
| 6 | 2 | reserved | `0` |
| 8 | 8 | session_id | Distinguishes executions. Any value that differs between executions sending at once. |
| 16 | 8 | config_id | The [hash](#config-id) of the dataset RID, channel names and tags |
In ABNF ([RFC 5234](https://www.rfc-editor.org/rfc/rfc5234)), where `nOCTET` is exactly n
octets and `%xNN` a literal octet:
```{frame-spec}
```
The grammar can't express these rules, which every frame also follows:
1. A HELLO has exactly `channel-count` channel names and exactly `tag-count` tags.
2. A DATA frame has exactly `row-count` rows.
3. Each row has exactly `channel-count` values.
4. A DATA frame's `channel-count` equals that of the HELLO with the same `config_id`.
5. No frame is longer than 1400 bytes.
6. `channel-count` is 1 to 169, `tag-count` is at most 64, and a DATA frame's `row-count` is
at least 1.
7. `run-tag-count` is at most `tag-count`, and unless `run-mode` is 0, `run-title` and
`ref-name` aren't empty.
A decoder has to check every count against the bytes it actually received. A frame can match
the grammar and still claim more rows than it carries.
## The sequence number
`sequence` counts DATA frames within a session, going up by one per frame. A sender advances it
even when sending a frame fails, so a relay that sees a jump knows frames were lost on the way. A
lower number than one already seen is a frame that arrived out of order, not a lost one. The
first DATA frame of a session can carry any sequence number.
## The run
A HELLO's last fields say what run in Nominal the relay makes for the execution:
- `run-mode` 0 asks for none, and 1 for a run of the execution's own. 2 asks for one run shared
by every execution with the same title: the relay uses the newest run with exactly that title,
and creates one only when there is none.
- The run starts at `run-start`, and shows the dataset's points that carry the HELLO's first
`run-tag-count` tags, with the dataset under `ref-name`.
The run belongs to the execution, not the channel list, so it isn't part of `config_id`. Every
HELLO of an execution repeats it, and a relay creates the run from the first, unless a run with
that title and `run-start` exists already, made before the relay restarted, in which case it
uses that one. It sets the run's end to `end-time` from the execution's first END. When every
END is lost, the relay sets the end to the newest row it received, once the execution has sent
nothing for the relay's idle timeout. Nominal needs a run's end after its start, so a relay
moves an end at or before `run-start` to 1 ns after it. An end on a shared run only ever moves
it later.
(config-id)=
## config_id
`config_id` is the 64-bit FNV-1a hash of the dataset RID, followed by each channel name in
order, then each tag's key and value in order. Each of those strings goes into the hash preceded
by its length as a 4-byte little-endian integer, which keeps `["ab", "c"]` and `["a", "bc"]`
apart. That length prefix is 4 bytes, unlike the 2-byte lengths in the frame itself. With no
tags, the hash covers the RID and names alone.
```
h = 0xcbf29ce484222325 offset basis
for each string s in [rid, name1, name2, ..., key1, value1, key2, value2, ...]:
for each byte b in u32_le(len(s)) followed by the bytes of s:
h = (h XOR b) * 0x100000001b3 modulo 2^64
```
To check an implementation, FNV-1a/64 of the single byte `a` is `0xaf63dc4c8601ec8c`. Go's
standard library has the hash as `hash/fnv` (`fnv.New64a`). A relay drops a HELLO whose
`config_id` doesn't match its contents.
## Limits
A DATA row is 8 bytes of timestamp plus 8 bytes per channel, and at least one row has to fit
after the header and the fixed DATA fields:
8 × channels ≤ 1400 − 24 − 12 − 8, so at most 169 channels.
A DATA frame holds ⌊1364 ÷ (8 + 8 × channels)⌋ rows, which is 42 rows at 3 channels.
A HELLO holds the whole channel list, every tag and the run. A sender doesn't split them across
frames, so they have to fit in one:
45 + (RID length) + (run title length) + (ref name length) + the sum over channels of
(2 + name length) + the sum over tags of (4 + key length + value length) ≤ 1400
The RID, each name, each tag key and value, the run title and the ref name can each be at most
256 bytes, and a HELLO carries at most 64 tags.
## Example
The three frames below are the shared fixtures that every implementation in the repository is
tested against. They encode this input:
| | |
|---|---|
| dataset RID | `ri.catalog.test.dataset.abc` |
| channels | `engine.rpm`, `engine.egt`, `engine.psi` |
| tags | `model` = `A`, `execution-id` = `7` |
| run | a run of its own, starting at 1000000000, showing both tags, titled `Model A, execution 7`, with the dataset under `default` |
| session_id | `0x1122334455667788` |
| config_id | `0x5bbb5bdfcbf6bf55` |
| sequence | 42 |
| rows | 1000000000: 1500.0, 700.0, 30.0
1000001000: 1510.5, 701.5, 30.1
1000002000: 1521.25, 703.0, 30.2 |
| end | 1000002000 |
The HELLO, 162 bytes:
```
4e4d5331 magic "NMS1"
01 version 1
01 type HELLO
0000 reserved
8877665544332211 session_id
55bff6cbdf5bbb5b config_id
1b00 RID length 27
72692e636174616c6f672e746573742e646174617365742e616263
"ri.catalog.test.dataset.abc"
0300 3 channels
0a00 656e67696e652e72706d "engine.rpm"
0a00 656e67696e652e656774 "engine.egt"
0a00 656e67696e652e707369 "engine.psi"
0200 2 tags
0500 6d6f64656c key "model"
0100 41 value "A"
0c00 657865637574696f6e2d6964
key "execution-id"
0100 37 value "7"
01 run mode 1, a run of its own
00ca9a3b00000000 run start 1000000000
0200 the run shows the first 2 tags
1400 4d6f64656c20412c20657865637574696f6e2037
title "Model A, execution 7"
0700 64656661756c74 ref name "default"
```
The DATA frame, 132 bytes:
```
4e4d5331 01 02 0000 magic, version 1, type DATA, reserved
8877665544332211 session_id
55bff6cbdf5bbb5b config_id
2a00000000000000 sequence 42
0300 3 channels
0300 3 rows
00ca9a3b00000000 timestamp 1000000000
0000000000709740 1500.0
0000000000e08540 700.0
0000000000003e40 30.0
e8cd9a3b00000000 timestamp 1000001000
00000000009a9740 1510.5
0000000000ec8540 701.5
9a99999999193e40 30.1
d0d19a3b00000000 timestamp 1000002000
0000000000c59740 1521.25
0000000000f88540 703.0
3333333333333e40 30.2
```
The END, 32 bytes:
```
4e4d5331 01 03 0000 magic, version 1, type END, reserved
8877665544332211 session_id
55bff6cbdf5bbb5b config_id
d0d19a3b00000000 end time 1000002000
```
## Implementations
The [nominal-simulink repository](https://github.com/nominal-io/nominal-simulink) has the
protocol in three languages. The block's encoder in C (`cstream/src/nm_frame.c`) produces the
frames above, and two decoders are tested against them: the relay's in Go (`nmproxy/frame/`) and
one in MATLAB (`matlab/nmframe_decode.m`). `matlab/nmproxy_reference.m` is a small relay in MATLAB that
decodes and prints what arrives without writing to Nominal, written to be read alongside this
page.