Skip to content

STE register

Anatomy of a frame#

Everything Valence sends is a frame. A frame is an eight-byte header followed by a payload.

This is the only page in this section that shows bytes. Read How it works first; this page explains the envelope that carries everything on it. The same colors apply: blue is measured truth, purple is a request, amber and red are safety.

1. Four layers, four jobs#

What each layer is responsible for, and what it deliberately knows nothing about.

flowchart TD
    S["▶ START<br/>Session layer<br/>identity · roles · grants · liveness · reconnect"]:::layer
    C["Channel layer<br/>what a class means: snapshot, sample, command, edge, document"]:::layer
    F["Framing<br/>header · length · sequence · fragmentation"]:::layer
    T["Transport binding<br/>open · close · write · read, plus its declared properties"]:::layer

    S --> C --> F --> T

    T --> W["WebSocket"]:::plumb
    T --> E["ESP-NOW"]:::plumb
    T --> B["BLE"]:::plumb
    T --> R["Serial"]:::plumb
    T --> P["In-process"]:::plumb

    classDef layer fill:none,stroke:#8a8f98,stroke-width:1.5px
    classDef plumb fill:none,stroke:#8a8f98,stroke-dasharray:3 3

The point. Everything above the binding line is transport-blind. A binding is four operations plus an honest declaration of what it can do, so adding a transport never changes the protocol above it.

Layer Owns Knows nothing about
Session Who this client is, what it may do, what it was granted, how silence is detected How bytes travel
Channel What a class promises: a snapshot supersedes, a sample is one instant, a command is confirmed Which transport carries it
Framing Where a frame starts and ends, which channel it belongs to, its sequence number What the payload means
Transport binding Moving one frame, and declaring its own size limit, ordering and reliability Anything about sessions or channels

The weakest transport writes the rules. Every guarantee is stated against unordered, lossy, 242-byte datagrams. Anything correct there is correct everywhere, and a reliable transport simply gets stronger behavior for free.

2. Every frame starts the same way#

The eight-byte header, byte by byte, and the payload that follows it.

typeu8
flagsu8
channelu16
sequ16
lenu16
payloadlen bytes

The point. The header is the same eight bytes on every transport and for every frame type. A receiver can therefore skip a frame it does not understand instead of disconnecting, because the length is always in a place it can read.

Field Size Its job
type 1 byte Which kind of frame this is. The frame type table lists every one.
flags 1 byte Fragmentation marks. Unlisted bits are sent as zero and ignored on receipt.
channel 2 bytes Which catalog channel this belongs to. Session-scoped frames use zero.
seq 2 bytes The sequence number, per channel and per direction. Classes that do not need it send zero.
len 2 bytes Payload length. This is what makes a frame self-delimiting on a transport that is a byte pipe rather than a message queue.

DEMO-CANDIDATE: a live frame trace — capture real bytes off a running hub and highlight this eight-byte header, field by field, against the table above.

Three rules hang off this header, and together they are why an old client keeps working against a new machine.

Unknown means ignore. An unknown frame type, an unknown channel, an unknown key: skip it and carry on. No endpoint may disconnect or flood a log over novelty. The sender of something new carries the burden of making it ignorable.

One frame is one message wherever the transport allows it. Fragmentation exists only for large control frames on small-datagram transports. Data frames never fragment.

Emergency stop is deliberately different. The stop frame is twelve bytes, starts with a four-byte magic pattern, and carries a CRC. A receiver can recognize it in a raw byte stream without decoding anything, which is what lets every queue on the path admit it at the front. Its bytes are specified in the safety codes reference.

3. Two encodings, and why there are two#

The header never changes. The payload is encoded one of two ways, and the choice follows how often the frame is sent.

The same two numbers, encoded for the data plane and for the control plane.

Packed, on the data plane. One motion sample: a target position and a velocity. No keys, no type tags, nothing but values in the order the catalog declared.

targetu16
velocityi16

CBOR, on the control plane. The same two numbers as a map. Each key is an integer from one registry, and each value carries its own type.

map(2)1 byte
keytarget
valueuint
keyvelocity
valuenegative int

The point. Self-description costs bytes. A frame sent hundreds of times a second does not pay that cost, and a frame sent when somebody presses a button gladly does. Same numbers here: four bytes packed, eight as a map.

Packed layout CBOR map
Used by STATE and STREAM — the data plane Everything that negotiates, commands or confirms
Sent Continuously, up to hundreds of times a second Occasionally
Self-describing No. The catalog's layout is the only reader Yes. Every value carries its type, every key means one thing everywhere
Decoding cost A field read at a known offset A small parser, or a canned template on a device too small for one
Evolution Append at the tail only. Old readers parse the prefix they know Add a key. Old readers ignore what they do not recognize

Both halves of that last row are the same promise made twice, which is the reason for the split.

A layout may only grow at the tail. Fields are never reordered, resized or removed. A client compiled a year ago still reads every field it knows from a machine whose catalog has grown since. Changing a field means allocating a new channel and retiring the old one, which keeps its number forever.

A map may only gain keys. A client that meets a key it has never seen skips the pair. This is the same tolerance rule as the header's, applied one level down.

One more restriction earns its keep. Valence uses a deterministic CBOR profile: definite lengths, shortest-form integers, sorted keys, no tags. Any message therefore has exactly one valid encoding. That is what lets test vectors compare byte for byte, and what lets a client too small for an encoder ship a canned template and patch values into it, knowing the bytes are what a real encoder would have produced.

Where to go next#

  • How it works — the mental model these frames serve.
  • Frame types — every type, generated from the registry.
  • CBOR keys — every key, generated from the registry.