Implementations

Logb is a format. The specification is the normative document, and an implementation is a claim to conform to it — where the two disagree, the specification wins and the implementation is the bug.

One implementation exists today, in Go, together with a set of command-line tools, two importers, and a browser-based viewer. If you write another, in any language, it belongs on this page: open an issue or a pull request on the repository.

Writing one

A conforming reader is roughly a thousand lines plus a decompressor. There is no XML, no external schema registry, and no runtime dependency that must still exist in 2050. The things worth knowing before you start are on the conformance page — in particular that the bit-numbering vectors in §6.2, not the prose around them, are what your extractor has to reproduce.

A writer is smaller still, and needs only a byte sink: nothing points forward, so it never seeks back to patch a field it already emitted. That is what makes the format practical on an embedded logger.

Reference implementation — Go

`github.com/rveen/logb` — MIT, covering the specification as well as the code.

go get github.com/rveen/logb

A reader that needs only an `io.Reader`, a writer that needs only an `io.Writer`, bit extraction and the full conversion set, plus `Resync` — design rule 3 as an API: hand it a byte slice starting anywhere and it scans forward to the next sync frame and returns a reader positioned there.

API reference is on pkg.go.dev.

Command-line tools

go run ./cmd/logbgen -o /tmp/x.logb       # write the example file
go run ./cmd/logbdump /tmp/x.logb         # frames, schemas, records
go run ./cmd/logbdump -resync /tmp/x.logb # resynchronise, from the CLI

`logbdump` is the quickest way to watch the crash-safety claim hold up. Cut a file in half and read what is left:

head -c 7000 /tmp/x.logb > /tmp/cut.logb
go run ./cmd/logbdump /tmp/cut.logb       # 302 records, TRUNCATED

No repair step, no recovery mode — the truncated file is simply a shorter valid file.

Importers

Two importers exist, one for each of the format's parents. Both are packages you can use directly, not just commands.

go run ./cmd/raw2logb testdata/test.raw            # a SPICE raw file
go run ./cmd/mdf2logb testdata/mdf/ex2-obd.mf4     # an MDF4 recording
go run ./cmd/mdf2logb -dbc testdata/obd2.dbc testdata/mdf/ex2-obd.mf4

`raw2logb` applies the mapping in §11: LTspice's binary raw file in, Logb out, with the header as metadata, the variable type column as units, a sibling `.net` netlist as an attachment, and a `.step` sweep's run boundaries made explicit instead of left to be guessed at.

`mdf2logb` does the same for the other parent. One MDF channel group becomes one stream, the master channel becomes the axis, and every other channel becomes a field at the bit offset it already had — the record is copied as it stands, because Logb numbers bits the way MDF does. Conversions become §7 conversions, invalidation bits become §6.2 guarded fields, and a payload MDF stored out of line comes back inline where §6.4 says a bus payload belongs. Whatever cannot be carried across is printed rather than dropped in silence. The MDF 4.x reader is its own package and depends on nothing.

A bus recording contains no signals. A CAN log is frames — an identifier and eight bytes; `EngineSpeed` is not in it and never was. What those bytes mean lives in a DBC database, so `mdf2logb -dbc` reads one and writes a stream per message beside the raw frames:

OBD2_Response  +186.929250s  PID=12  Service="Mode01Response"  EngineSpeed=2368rpm
OBD2_Response  +186.948900s  PID=13  Service="Mode01Response"  VehicleSpeed=46km/h

The frames stay, and so does the database: it is embedded as an attachment with its SHA-256 in the metadata. A decoded signal is an interpretation, the frame bytes are the evidence for it, and the DBC is the citation. See CAN, DBC and bit ordering for why the Motorola case is an offset transform here and nothing more.

The viewer

`logbview` opens a file in your browser — a signal tree, plots, drag-to-zoom, a record table and a frame map. It is a separate Go module so that the frontend bundle it embeds stays out of the core library's file set.

go run github.com/rveen/logb/viewer/cmd/logbview@latest recording.logb

It is described, with screenshots, on the viewer page.

Writing and reading a stream

The schema declares the record layout, the axis, and the stream's identity. The UUID says which logical stream this is across segments and files.

func schema() *logb.Schema {
	return &logb.Schema{
		UUID:       uuid.NewSHA1(uuid.NameSpaceOID, []byte("example/EngineData")),
		Name:       "EngineData",
		RecordBits: 24, // EngineSpeed(16) + CoolantTemp(8)

// One record every 10 ms, derived from the record index: the axis costs // zero bytes per record. AxisExp is the tick size as a power of ten, so // -9 means one tick is a nanosecond. AxisKind: logb.AxisTime, AxisMode: logb.AxisImplicit, AxisExp: -9, AxisUnit: "s", AxisStep: logb.TickVal(10_000_000),

// Raw is what the bus produced; Conv derives the physical value. Fields: []logb.Field{ {Name: "EngineSpeed", BitOffset: 0, BitWidth: 16, Type: logb.TypeUint, Unit: "rpm", Conv: logb.Linear{A: 0, B: 0.25}}, {Name: "CoolantTemp", BitOffset: 16, BitWidth: 8, Type: logb.TypeUint, Unit: "degC", Conv: logb.Linear{A: -40, B: 1}}, }, } }


Writing needs only an `io.Writer`. A segment restates its schemas, which is what lets a reader start there:

lw, _ := logb.NewWriter(w)
lw.AddStream(s)
lw.BeginSegment(1773480413000000000) // wall clock, ns
lw.WriteMeta("vehicle.vin", "WVWZZZ1JZXW000001")
lw.WriteData(s, logb.TickVal(0), 0, n, rec)
lw.Close() // writes the index and end frames — both optional by construction

Reading yields one batch per DATA frame:

r, _ := logb.NewReader(bytes.NewReader(data))
for {
	b, err := r.Next()
	if err == io.EOF {
		break
	}
	for i := 0; i < int(b.Count); i++ {
		axis, _ := b.Axis(i)     // b.Raw(i, f) gives the stored bits instead
		rpm, _ := b.Value(i, 0)  // conversion applied
		degC, _ := b.Value(i, 1)
		_, _, _ = axis, rpm, degC
	}
}

// Truncated means the scan stopped at damage rather than at a clean end. // Every batch returned before it was set is intact and trustworthy. if r.Truncated { fmt.Println("file is truncated; batches above are still intact") }


Recovering a cut file is one call:

r, off, err := logb.Resync(data[7000:])

The full worked example — multiple segments, mixed byte orders, variable-length fields, compression — is in `internal/example`.

Other languages

None yet. The specification is complete enough to implement from, the conformance vectors will tell you whether you got the hard part right, and there is nobody to ask for permission.


MD5 signature: 04d3be86bc0cc1c9c97eb5a8c3b30775