blackbox_format.hpp¶
The SHULIB BLACKBOX on-disk format, v1 — the binary record SdSink writes and BlackboxReader reads.
This header declares 6 types (55 members), 12 free functions, and 8 constants.
Extracted from include/shulib/diag/blackbox_format.hpp — this page is that header's documentation, reformatted, so it cannot disagree with the code. Prose about how to think about the API lives in the user guide; worked recipes live in the cookbook; this page is the complete, mechanical list of what exists.
Contents¶
kMagic— constantkFormatVersion— constantkHeaderBytes— constantkFrameHeaderBytes— constantkTickPayloadBytes— constantkSummaryPayloadBytes— constantkTriagePayloadBytes— constantkEndPayloadBytes— constantenum class FrameTypeTickSummaryTriageEndstruct TriageInfofaultbrownouttickIndexfaultTimeprecedingTicksstruct EndInfotickFramesdroppedFramesbytesBeforemessagesSeenbrownoutdeviceFailedendTimestruct BlackboxHeaderformatVersionheaderBytestickRecordBytesflagsepochSecondsringCapacitybyteBudgetbuildHashroutineIdalliancesideportMapbuildHash_routineId_alliance_side_portMap_class ByteWriterByteWriteru8booleanu16u32i32f64textzerosoffsetokclass ByteReaderByteReaderu8booleanu16u32i32f64textskipoffsetokencodeHeader— free functiondecodeHeader— free functionencodeTick— free functionsafeAngle— free functiondecodeTick— free functionencodeSummary— free functiondecodeSummary— free functionencodeTriage— free functiondecodeTriage— free functionencodeEnd— free functiondecodeEnd— free functionencodeFrameHeader— free function
kMagic¶
The four magic bytes every blackbox file starts with ("SHulib BlackBox").
constant, declared at include/shulib/diag/blackbox_format.hpp:70.
kFormatVersion¶
On-disk format version. BUMP THIS whenever any layout below changes — a reader refuses a version it was not built for rather than misreading it (header note).
constant, declared at include/shulib/diag/blackbox_format.hpp:74.
kHeaderBytes¶
Size of the fixed file header, in bytes (v1). Fixed width so a reader can seek past it without parsing, and generous enough to hold full provenance.
constant, declared at include/shulib/diag/blackbox_format.hpp:78.
kFrameHeaderBytes¶
Size of the per-frame prefix: {u8 type, u8 reserved, u16 payloadBytes}.
constant, declared at include/shulib/diag/blackbox_format.hpp:81.
kTickPayloadBytes¶
Payload size of one Tick frame (v1). Pinned by the golden test; the encoder asserts it wrote exactly this many bytes.
constant, declared at include/shulib/diag/blackbox_format.hpp:85.
kSummaryPayloadBytes¶
Payload size of one Summary frame (v1).
constant, declared at include/shulib/diag/blackbox_format.hpp:88.
kTriagePayloadBytes¶
Payload size of one Triage frame (v1): the D-7 triage fields PLUS the complete record of the tick the fault fired on (header note on dump ordering in sd_sink.hpp).
constant, declared at include/shulib/diag/blackbox_format.hpp:92.
kEndPayloadBytes¶
Payload size of one End frame (v1) — the graceful-end stamp.
constant, declared at include/shulib/diag/blackbox_format.hpp:95.
enum class FrameType¶
What a frame carries. WIRE-STABLE: explicit values, append-only — an unknown type is skipped by length, never guessed at.
enum class, declared at include/shulib/diag/blackbox_format.hpp:99.
FrameType::Tick¶
one DebugRecord (kTickPayloadBytes)
enumerator, declared at include/shulib/diag/blackbox_format.hpp:100.
FrameType::Summary¶
one RunSummary (kSummaryPayloadBytes)
enumerator, declared at include/shulib/diag/blackbox_format.hpp:101.
FrameType::Triage¶
the D-7 fault triage block + the fault tick's own record
enumerator, declared at include/shulib/diag/blackbox_format.hpp:102.
FrameType::End¶
the graceful-end stamp: counts, brownout latch, end time
enumerator, declared at include/shulib/diag/blackbox_format.hpp:103.
struct TriageInfo¶
The D-7 triage block, as data: which fault, when, on which tick, and how many preceding ticks follow it in the file. The record of the fault tick itself travels in the same frame (see sd_sink.hpp's dump-ordering rule).
struct, declared at include/shulib/diag/blackbox_format.hpp:109.
TriageInfo::fault¶
the fault that triggered the dump
field, declared at include/shulib/diag/blackbox_format.hpp:110.
TriageInfo::brownout¶
the latched brownout marker at dump time
field, declared at include/shulib/diag/blackbox_format.hpp:111.
TriageInfo::tickIndex¶
how many records the sink had seen when it fired
field, declared at include/shulib/diag/blackbox_format.hpp:112.
TriageInfo::faultTime¶
the fault tick's t, seconds since the run epoch
field, declared at include/shulib/diag/blackbox_format.hpp:113.
TriageInfo::precedingTicks¶
Tick frames that follow, oldest first (0 when streaming)
field, declared at include/shulib/diag/blackbox_format.hpp:114.
struct EndInfo¶
The end frame: what the sink knows about its own run when it closes cleanly. A file WITHOUT this frame ended abruptly — that absence is the truncation signal a reader can act on.
struct, declared at include/shulib/diag/blackbox_format.hpp:120.
EndInfo::tickFrames¶
Tick frames staged over the run
field, declared at include/shulib/diag/blackbox_format.hpp:121.
EndInfo::droppedFrames¶
frames dropped for want of buffer (byte budget)
field, declared at include/shulib/diag/blackbox_format.hpp:122.
EndInfo::bytesBefore¶
Bytes of this file that PRECEDE this frame — i.e. the frame's own offset. A reader can verify it against where it actually found the frame, which is how a file that was appended to, interleaved, or spliced gives itself away. (It is NOT "bytes the device confirmed": at close() the bulk of a caller-paced run is still staged and goes out in the same write as this frame, so that figure would read 0 for the most common run of all.)
field, declared at include/shulib/diag/blackbox_format.hpp:129.
EndInfo::messagesSeen¶
log() lines handed to the sink and NOT carried (header note)
field, declared at include/shulib/diag/blackbox_format.hpp:130.
EndInfo::brownout¶
the latched brownout marker
field, declared at include/shulib/diag/blackbox_format.hpp:131.
EndInfo::deviceFailed¶
a write() or flush() reported failure during the run
field, declared at include/shulib/diag/blackbox_format.hpp:132.
EndInfo::endTime¶
clock time at close, seconds since the run epoch
field, declared at include/shulib/diag/blackbox_format.hpp:133.
struct BlackboxHeader¶
A decoded file header. Value type with bounded storage, like RunSummary: a decoded header must never hold views into a buffer the caller may free.
struct, declared at include/shulib/diag/blackbox_format.hpp:138.
BlackboxHeader::formatVersion¶
as read from the file
field, declared at include/shulib/diag/blackbox_format.hpp:139.
BlackboxHeader::headerBytes¶
self-declared header size (lets a reader seek)
field, declared at include/shulib/diag/blackbox_format.hpp:140.
BlackboxHeader::tickRecordBytes¶
self-declared Tick payload size (cross-checked)
field, declared at include/shulib/diag/blackbox_format.hpp:141.
BlackboxHeader::flags¶
reserved, 0 in v1
field, declared at include/shulib/diag/blackbox_format.hpp:142.
BlackboxHeader::epochSeconds¶
the injected clock's reading when the file opened
field, declared at include/shulib/diag/blackbox_format.hpp:143.
BlackboxHeader::ringCapacity¶
flight-recorder ring size the writer was configured with
field, declared at include/shulib/diag/blackbox_format.hpp:144.
BlackboxHeader::byteBudget¶
RAM byte budget the writer was configured with
field, declared at include/shulib/diag/blackbox_format.hpp:145.
BlackboxHeader::buildHash¶
The git build hash the run was built from. EMPTY means MISSING — render it loudly and never invent a plausible value (§18.5, build_info.hpp).
function, declared at include/shulib/diag/blackbox_format.hpp:149.
BlackboxHeader::routineId¶
The routine id the run was started with (may be empty).
function, declared at include/shulib/diag/blackbox_format.hpp:151.
BlackboxHeader::alliance¶
Alliance as free text ("red"/"blue"/"skills"); may be empty.
function, declared at include/shulib/diag/blackbox_format.hpp:153.
BlackboxHeader::side¶
Side as free text ("left"/"right"); may be empty.
function, declared at include/shulib/diag/blackbox_format.hpp:155.
BlackboxHeader::portMap¶
The caller-authored port map; may be empty.
function, declared at include/shulib/diag/blackbox_format.hpp:157.
BlackboxHeader::buildHash_¶
Storage for buildHash() — written by the decoder, NUL-terminated.
field, declared at include/shulib/diag/blackbox_format.hpp:160.
BlackboxHeader::routineId_¶
Storage for routineId().
field, declared at include/shulib/diag/blackbox_format.hpp:162.
BlackboxHeader::alliance_¶
Storage for alliance().
field, declared at include/shulib/diag/blackbox_format.hpp:164.
BlackboxHeader::side_¶
Storage for side().
field, declared at include/shulib/diag/blackbox_format.hpp:166.
BlackboxHeader::portMap_¶
Storage for portMap().
field, declared at include/shulib/diag/blackbox_format.hpp:168.
class ByteWriter¶
Little-endian byte writer with a hard end: a write that would not fit writes NOTHING and latches overflow, so an undersized buffer can never corrupt neighbouring memory and can never half-write a field. Callers check ok().
class, declared at include/shulib/diag/blackbox_format.hpp:174.
ByteWriter::ByteWriter¶
Write into out, starting at offset 0.
function, declared at include/shulib/diag/blackbox_format.hpp:177.
ByteWriter::u8¶
Append one unsigned byte.
function, declared at include/shulib/diag/blackbox_format.hpp:180.
ByteWriter::boolean¶
Append a bool as 0x00 / 0x01.
function, declared at include/shulib/diag/blackbox_format.hpp:187.
ByteWriter::u16¶
Append a 16-bit unsigned value, little-endian.
function, declared at include/shulib/diag/blackbox_format.hpp:189.
ByteWriter::u32¶
Append a 32-bit unsigned value, little-endian.
function, declared at include/shulib/diag/blackbox_format.hpp:197.
ByteWriter::i32¶
Append a 32-bit signed value as two's complement, little-endian.
function, declared at include/shulib/diag/blackbox_format.hpp:206.
ByteWriter::f64¶
Append an IEEE-754 binary64 value, little-endian (bit pattern preserved, so a NaN or an infinity survives the trip exactly as it was recorded).
function, declared at include/shulib/diag/blackbox_format.hpp:209.
ByteWriter::text¶
Append fieldBytes of text: s truncated to fit, NUL-padded to the full width. Fixed width by design — a variable-length string would make every later offset depend on run-time content.
function, declared at include/shulib/diag/blackbox_format.hpp:222.
ByteWriter::zeros¶
Append n zero bytes (reserved space).
function, declared at include/shulib/diag/blackbox_format.hpp:232.
ByteWriter::offset¶
How many bytes have been appended.
function, declared at include/shulib/diag/blackbox_format.hpp:241.
ByteWriter::ok¶
False once any append did not fit (nothing was written for that append).
function, declared at include/shulib/diag/blackbox_format.hpp:243.
class ByteReader¶
Little-endian byte reader with a hard end: a read past the end yields zero and latches exhaustion, so a truncated or corrupt file can never read out of bounds and can never half-read a field. Callers check ok().
class, declared at include/shulib/diag/blackbox_format.hpp:262.
ByteReader::ByteReader¶
Read from in, starting at offset 0.
function, declared at include/shulib/diag/blackbox_format.hpp:265.
ByteReader::u8¶
Read one unsigned byte (0 past the end).
function, declared at include/shulib/diag/blackbox_format.hpp:268.
ByteReader::boolean¶
Read a bool: any nonzero byte is true.
function, declared at include/shulib/diag/blackbox_format.hpp:275.
ByteReader::u16¶
Read a 16-bit unsigned value, little-endian.
function, declared at include/shulib/diag/blackbox_format.hpp:277.
ByteReader::u32¶
Read a 32-bit unsigned value, little-endian.
function, declared at include/shulib/diag/blackbox_format.hpp:286.
ByteReader::i32¶
Read a 32-bit signed value (two's complement), little-endian.
function, declared at include/shulib/diag/blackbox_format.hpp:297.
ByteReader::f64¶
Read an IEEE-754 binary64 value, little-endian (bit pattern preserved).
function, declared at include/shulib/diag/blackbox_format.hpp:299.
ByteReader::text¶
Read fieldBytes of NUL-padded text into dst (capacity dstBytes, always NUL-terminated). Bytes beyond the destination are consumed and discarded, so the cursor stays aligned no matter how the caller sized its storage.
function, declared at include/shulib/diag/blackbox_format.hpp:314.
ByteReader::skip¶
Skip n bytes (reserved space).
function, declared at include/shulib/diag/blackbox_format.hpp:327.
ByteReader::offset¶
How many bytes have been consumed.
function, declared at include/shulib/diag/blackbox_format.hpp:333.
ByteReader::ok¶
False once any read ran past the end.
function, declared at include/shulib/diag/blackbox_format.hpp:335.
encodeHeader¶
[[nodiscard]] inline std::size_t encodeHeader(std::span<std::byte> out, const SessionInfo& info, double epochSeconds, std::uint32_t ringCapacity, std::uint32_t byteBudget) noexcept
Encode the 256-byte file header into out. Returns the bytes written (0 if out is too small). Provenance strings are copied in, truncated to their field widths — an EMPTY build hash stays empty, because MISSING must stay loud all the way to disk.
free function, declared at include/shulib/diag/blackbox_format.hpp:365.
decodeHeader¶
Decode a file header. Returns false if in is shorter than the header or the magic does not match; the VERSION is decoded but NOT judged here — BlackboxReader owns the refusal policy, and a caller inspecting a rejected file still wants to see what version it claims to be.
free function, declared at include/shulib/diag/blackbox_format.hpp:395.
encodeTick¶
[[nodiscard]] inline std::size_t encodeTick(std::span<std::byte> out, const DebugRecord& r) noexcept
Encode one DebugRecord. Returns the bytes written, or 0 if out was too small or the layout did not come out to exactly kTickPayloadBytes (a loud, testable failure rather than a silently short record).
free function, declared at include/shulib/diag/blackbox_format.hpp:440.
safeAngle¶
Rebuild an Angle from a decoded radian value WITHOUT trusting the file: a corrupt or truncated blackbox can contain any bit pattern, and math::Angle's factory rejects non-finite input by precondition. A decoder that throws on a corrupt file is a decoder you cannot use on the file you most need to read, so a non-finite heading decodes to zero and corrupt is raised for the caller to see.
free function, declared at include/shulib/diag/blackbox_format.hpp:500.
decodeTick¶
[[nodiscard]] inline bool decodeTick(std::span<const std::byte> in, DebugRecord& r, bool& corrupt) noexcept
Decode one DebugRecord. Returns false if the payload is not exactly kTickPayloadBytes. corrupt is set (never cleared) when a field could not be represented — today: a non-finite heading, which decodes to zero (safeAngle).
free function, declared at include/shulib/diag/blackbox_format.hpp:511.
encodeSummary¶
[[nodiscard]] inline std::size_t encodeSummary(std::span<std::byte> out, const RunSummary& s, std::uint32_t blackboxDropped) noexcept
Encode one RunSummary. blackboxDropped is the SINK's own drop count, passed in rather than read from the summary so the file always carries the writer's live figure even when the caller assembled the summary before the last drop. Returns the bytes written, or 0 on a layout/space failure.
free function, declared at include/shulib/diag/blackbox_format.hpp:592.
decodeSummary¶
[[nodiscard]] inline bool decodeSummary(std::span<const std::byte> in, RunSummary& s, std::uint32_t& blackboxDropped) noexcept
Decode one RunSummary; blackboxDropped receives the sink's own drop count. Returns false if the payload is not exactly kSummaryPayloadBytes.
free function, declared at include/shulib/diag/blackbox_format.hpp:623.
encodeTriage¶
[[nodiscard]] inline std::size_t encodeTriage(std::span<std::byte> out, const TriageInfo& info, const DebugRecord& faultTick) noexcept
Encode the D-7 triage block plus the complete record of the tick the fault fired on. Returns the bytes written, or 0 on a layout/space failure.
free function, declared at include/shulib/diag/blackbox_format.hpp:663.
decodeTriage¶
[[nodiscard]] inline bool decodeTriage(std::span<const std::byte> in, TriageInfo& info, DebugRecord& faultTick, bool& corrupt) noexcept
Decode a triage frame and the fault tick's record. Returns false if the payload is not exactly kTriagePayloadBytes.
free function, declared at include/shulib/diag/blackbox_format.hpp:688.
encodeEnd¶
Encode the graceful-end stamp. Its PRESENCE is the signal that the run closed cleanly; its absence is how a reader knows a file was cut short. Returns the bytes written, or 0 on a layout/space failure.
free function, declared at include/shulib/diag/blackbox_format.hpp:711.
decodeEnd¶
Decode the graceful-end stamp. Returns false if the payload is not exactly kEndPayloadBytes.
free function, declared at include/shulib/diag/blackbox_format.hpp:729.
encodeFrameHeader¶
[[nodiscard]] inline std::size_t encodeFrameHeader(std::span<std::byte> out, FrameType type, std::uint16_t payloadBytes) noexcept
Write a frame prefix {type, reserved, payloadBytes} into out. Returns the bytes written (kFrameHeaderBytes) or 0 if it did not fit.
free function, declared at include/shulib/diag/blackbox_format.hpp:747.
Design commentary, from the header¶
The header opens with the reasoning behind these shapes. It is reproduced here in full because a reference that only lists signatures teaches nobody why.
The header’s own reasoning — 52 lines, click to expand
The SHULIB BLACKBOX on-disk format, v1 — the binary record SdSink writes and
BlackboxReader reads (master plan §18; diagnostics-plan D-6/D-7; WS13, chunk E1).
── Why a format document lives in code ─────────────────────────────────────────────
A file outlives the program that wrote it. Six months from now the only thing that
can open a run from a competition day is a decoder that agrees with this layout, so
the layout is written down ONCE, here, with its version, and the encoder and the
decoder are both held to it BY A BYTE-EXACT GOLDEN TEST — not by their agreement
with each other. Two sides that share one bug agree perfectly; that is precisely the
failure a round trip cannot see, which is why test/blackbox_format_test.cpp pins the
literal bytes of the header and of one full tick record.
── The shape ───────────────────────────────────────────────────────────────────────
[ 256-byte file header ][ frame ][ frame ][ frame ] …
Every frame is { u8 type, u8 reserved, u16 payloadBytes } followed by payloadBytes
of payload. A reader that meets a frame type it does not know SKIPS it by its length
and counts it — that is what makes a v1 reader survive a v1.1 writer that appended a
new frame kind, without ever GUESSING at content it does not understand.
── Versioned from byte zero, and refusal over misreading ───────────────────────────
The header carries kFormatVersion. A decoder that mis-reads an old file is worse
than one that refuses it: a wrong number read confidently sends the 2am investigation
somewhere false, while a refusal sends it to the git history. So the reader refuses
any version it was not built for, and ALSO cross-checks the header's self-declared
record width against its own — that catches the specific human error of changing the
layout and forgetting to bump the version.
── Fixed width, and no narrowing ANYWHERE ──────────────────────────────────────────
Every field is fixed width and little-endian, written byte by byte (never a struct
memcpy: padding and ABI are not a file format). Every floating-point field is IEEE-754
binary64, including the per-wheel arrays and the tick-phase slots, where binary32
would have saved ~35% of the file. That trade was made deliberately: the chunk's
central promise is that a decoded record equals the encoded one FIELD BY FIELD, and
with narrowing that sentence would have quietly become "equals after a documented
rounding" — a weaker claim that also makes every future comparison of two runs
depend on rounding behaviour. Bytes are cheap on an SD card; a fuzzy record is not.
── What v1 does NOT carry, said plainly ────────────────────────────────────────────
The log() message channel. Variable-length text in a fixed-width record format is a
different problem with different trade-offs (bounded-vs-truncated strings, per-line
framing), and the terminal (A1) already owns text. The blackbox carries the per-tick
RECORD, the run SUMMARY, and the fault TRIAGE block. SdSink counts the messages it
was handed and writes that count into the end frame, so the omission is visible in
the file rather than silent — a reader can always see that N lines existed elsewhere.
── What H1 (F9, the SHUL/2 wire) inherits ──────────────────────────────────────────
This is a PERSISTENCE contract the moment a file exists, but it is deliberately NOT
the wire: SHUL/2 is streamed, sequenced and versioned on its own terms. What H1
should reuse is the FIELD ORDER of encodeTick() (it follows debug_record.hpp's own
declaration order, so a schema append lands at the end on both) and the refuse-don't-
misread rule. Nothing here freezes anything: F9 is H1's, and E1 freezes nothing.
Last updated 2026-08-15 — published from the main release branch, commit fa545ff. This site tracks releases, so between them it can lag the repository — that lag is deliberate, and it is what keeps the development log off the public site. For the current state of the code, see the repository.