The current codebase uses legacy* / current* to distinguish two devbin
record layouts. This is short-lived terminology: as soon as a third
variant ships, today's "current" becomes tomorrow's "legacy" and the
names need to be churned again. The names also conflate two independent
axes — how records are wrapped on the wire (the envelope) and how a
record's body is laid out (the payload format).
This document defines a stable naming scheme tied to the actual wire identity of each variant, so future formats slot in cleanly.
A devbin message is [msgType:2][envelope?][record][record]....
What sits between the 2-byte transport msgType prefix and the first
record. Three variants are observed in the field / supported by code:
| Variant | Length | Bytes | Notes |
|---|---|---|---|
EnvelopeNone |
0 | — | Cog v1.9.5: records start immediately |
EnvelopeV1 |
2 | magic(0xDB..0xDF), topicIndex |
Intermediate firmware |
EnvelopeV2 |
3 | magic(0xDB..0xDF), topicIndex, seqNum |
Current firmware |
Detection: peek rxMsg[2]; if (byte & 0xF0) === 0xD0 it is an envelope
magic, otherwise EnvelopeNone. Distinguishing V1 vs V2 is done by
probing whether hasValidRecordAt validates at offset 4 or 5.
What a single record body looks like after the common header
[statusBus:1][address:4][devTypeIdx:2]. Two variants are currently
defined:
| Variant | Per-record body after common header | Notes |
|---|---|---|
DevbinV0Fixed |
[sample][sample]... with each sample fixed-size from typeinfo |
Cog v1.9.5: no deviceSeq, no sampleLen prefix |
DevbinV1Framed |
[deviceSeq:1] [sampleLen:1][sample]... |
Current firmware: length-prefixed per sample |
Detection per-record: resolveRecordPayloadFormat validates that
sampleLen prefixes line up with the record length. The probe is cheap
because all the typeinfo needed is already in hand.
A sample longer than 255 bytes is written with an escape instead of the 1-byte length:
sample := [len:1][data] len 1..255 (unchanged)
| [0x00][lenHi][lenLo][data] len 256..65535
- Short samples are unchanged. Firmware uses the escape only for samples over 255 bytes, so every existing device stays byte-identical on the wire.
- Not a new variant. A 0 length was never valid (every sample holds
at least the 2-byte timestamp), so this extends
DevbinV1Framedrather than defining a new one. Parsers handle both forms in one place (readFramedSampleLen), and the format probe uses the same function. - Older parsers drop that device cleanly. A parser without the escape
stops reading the record at the 0 length, so it drops that device's
remaining samples in the record without misreading them. Other records
in the frame are unaffected because records are framed by
recordLen. This is covered by a test run against the pre-escape parser. - Record size. A record body is limited to 65,535 bytes by its 16-bit
recordLen. Firmware starts another record for the same device, with the nextdeviceSeq, when a run of samples would overflow one, and raftjs appends samples from repeated records in order. Firmware:RaftDevice::appendLengthPrefixedSample/lengthPrefixedSampleSize(RaftCore), used by every devbin producer.
The two axes have already drifted independently once: there exists
intermediate firmware that emits EnvelopeV1 + DevbinV1Framed, where
the envelope grew while the payload format stayed put. Coupling them
into a single enum would force every future combination to be encoded
as a new opaque name. Keeping them orthogonal means EnvelopeV3 and
DevbinV2* can be added independently.
- Envelope variants use
EnvelopeNoneorEnvelopeV<n>where<n>increments by 1 each time the envelope shape changes (additional fields, reordering, magic-range expansion). - Payload-format variants use
Devbin<scheme>V<n><Trait>where<n>increments with each on-wire layout change to the record body<Trait>is one short word describing the dominant property:Fixed(samples sized from typeinfo),Framed(length-prefixed), later perhapsTagged,Mixed, etc.
- The magic byte is documentation, not the version selector. The
reserved range
0xDB..0xDFis still accepted as a single class; if the firmware ever differentiates within that range it goes into the envelope variant name, not the payload format name. - No firmware references in type names. "Cog v1.9.5" and similar stay in comments next to the variant where they apply, not in the identifier.
- Capability flags are separate from format variants. Whether the
peer supports
devman/typeinfo?deviceid=versus?bus=&type=is a per-endpoint capability, not part of the devbin format enum, even though the two correlate today.
| Current symbol / string | Renamed to |
|---|---|
BinaryRecordPayloadFormat |
DevbinPayloadFormat |
"legacyRaw" (string literal) |
"DevbinV0Fixed" |
"lengthPrefixed" (string literal) |
"DevbinV1Framed" |
legacyDevbinEnvelopeLen (= 2) |
envelopeV1Len |
devbinEnvelopeLen (= 3) |
envelopeV2Len |
legacyMsgPos (local) |
envelopeV1RecordPos |
currentMsgPos (local) |
envelopeV2RecordPos |
legacyRecordHeaderLen (= 7) |
devbinV0FixedHeaderLen |
currentRecordHeaderLen (= 8) |
devbinV1FramedHeaderLen |
getLegacyRawSampleLen() |
getDevbinV0FixedSampleLen() |
payloadFormat / recordPayloadFormat |
same names, narrowed to new union |
| comment "legacy raw layout" | "DevbinV0Fixed payload format" |
| comment "current 3-byte envelope" | "EnvelopeV2 (3-byte)" |
| comment "legacy 2-byte envelope" | "EnvelopeV1 (2-byte)" |
setLegacySoktoMode on RaftConnector / RaftStreamHandler is
unrelated (it is about a different protocol — sokto streaming) and
stays as-is.
- The capability-cache approach for
devman/typeinfoURL form. That is already implemented as a per-device fallback and is unaffected by this renaming. - Renaming
_devbinEnvelopeSeqNumand similar instance state that refers to actual on-wire fields, not variant identifiers. - Anything in the dashboard or examples — no devbin format names leak out through the public API today.
| Firmware seen so far | Envelope | Payload |
|---|---|---|
| Cog v1.9.5 | EnvelopeNone |
DevbinV0Fixed |
| Intermediate (pre-current) | EnvelopeV1 |
DevbinV1Framed |
| Current (Axiom018, recent Cog) | EnvelopeV2 |
DevbinV1Framed |
Combinations not observed but legal under the discriminator code:
EnvelopeNone+DevbinV1Framed: detected via the "no envelope but first record looks length-prefixed" path already present inhandleClientMsgBinary.EnvelopeV*+DevbinV0Fixed: detected via the fallback path that validateslegacyRecordHeaderLenafter an envelope.
Both fallback paths remain valid under the renamed types; they are the reason payload format must be resolved per-record rather than per-message.