MBCAP is a file format for Modbus captures: what a device answered, and when, stored so that a
virtual device can later serve exactly that data to a Modbus client, at the original speed, slowed down
or sped up. go-modbus-capture is its Go library and the mbcapctl command-line tool.
Record a site once, then test SCADA screens, gateways, historians or analytics against it as often as you like, without touching the real devices.
- Every read function: coils, registers, diagnostics, event logs, file records, FIFO queues, device identification (FC43), all stored as raw request and response PDUs. Only the read functions of the Modbus specification: vendor and unknown function codes are rejected.
- Whole sites in one file: several devices, each with several acquisition streams, that a consumer can untangle or split.
- Compact and safe: about 28 bytes per sample for a typical meter, append-only, checksummed, readable after a crash.
- Lossless text twins: JSON Lines and CSV versions that convert back to the identical file.
- No decoding, no surprises: values stay raw; nothing is interpolated or invented.
This repository owns the format and the tooling for capture files. Recording from devices and replaying
captures are done by modbusctl, built on this library.
A capture holds four kinds of records:
| Record | Is | Example |
|---|---|---|
| Header | the capture: tick size, wall-clock start, producer, flags, tags, end | recorded on 2026-10-14 at 13:14:24 UTC, 1 µs ticks |
| Target | a device: an endpoint plus a unit id | Inverter A at 192.168.2.201:502, unit 21 |
| Stream | one way a target's data was acquired | "poll every second", "identity probe" |
| Sample | one answered request: stream, time, request PDU, response PDU | at 1.00045 s, 03 1000 0002 → 03 04 0001 01a7 |
Here is a complete capture, in its JSON Lines encoding:
{"t":"header","version":"0.1","tick_ns":1000,"wall_start":"2026-10-14T13:14:24.701149Z","producer":"modbusctl 3.0.0"}
{"t":"target","id":1,"label":"Inverter A","transport":"tcp","host":"192.168.2.201","port":502,"unit":21}
{"t":"stream","id":1,"target":1,"role":"poll","label":"holding 4096 x2"}
{"t":"stream","id":2,"target":1,"role":"identity"}
{"t":"s","s":1,"at":0,"req":"0310000002","resp":"0304000101a7"}
{"t":"s","s":1,"at":1000450,"req":"0310000002","resp":"0304000101a7"}
{"t":"s","s":1,"at":2001100,"req":"0310000002","resp":"0304000101a8"}
{"t":"s","s":1,"at":3000900,"req":"0310000002","resp":"0304000101a8"}
{"t":"s","s":2,"at":3500000,"req":"2b0e0100","resp":"2b0e0101000003000441434d45010258310203312e30"}
{"t":"end","at":4000800}The poll stream reads two holding registers at address 4096 (03 1000 0002) once a second. The device
answers 0x0001, 0x01a7, and from the third poll 0x0001, 0x01a8. The identity stream holds one
Read Device Identification answer: vendor ACME, product X1, revision 1.0.
Three rules give the data its meaning:
- Sample and hold. A value holds from its sample until the next sample of the same address or
request. Register 4097 is
0x01a7from 0 s and0x01a8from 2.0011 s until the end at 4.0008 s. Nothing is interpolated. - Only what was recorded exists. An address or function without a sample is absent; a replayer decides how to answer it.
- Metadata is advisory, values are not. A replayer may serve the inverter on another address or unit id. It may never change a recorded value or time; edits produce a new file.
Times are integer ticks since the start (here 1 µs), taken from a monotonic clock, so a capture replays at exactly its original pace, or at any multiple of it. More in Concepts.
| Encoding | Extension | Lossless | For |
|---|---|---|---|
| MBCAP binary | .mbcap |
yes | recording and archiving: compact, append-only, crash-safe, checksummed |
| JSON Lines | .mbcap.jsonl |
yes | reading, diffing, scripting, test fixtures |
| Long CSV | .mbcap.csv |
yes | spreadsheets and dataframes |
| Wide CSV | .wide.csv |
view only | charting raw values per address over time |
The binary encoding stores each distinct request once and lets samples refer to it, codes times as small deltas, and stores each response raw, as "same as before", or as only the 16-bit words that changed. Samples are grouped in independently decodable, checksummed, deflate-compressed blocks, with an index at the end:
magic │ header │ targets │ streams │ request templates │ data block │ data block │ … │ index │ footer
On a synthetic typical meter that is about 28 bytes per sample, against 192 for the format it replaces. A file cut short by a crash reads up to its last complete block. More in Encodings and the format specification.
Install the CLI:
go install github.com/otfabric/go-modbus-capture/cmd/mbcapctl@latestLook inside a capture:
mbcapctl info capture.mbcapConvert it, losslessly:
mbcapctl convert capture.mbcap capture.mbcap.jsonlKeep only the FC03 samples of the first ten minutes:
mbcapctl filter capture.mbcap first-ten.mbcap --fc 3 --to 10mmbcapctl also verifies, dumps, merges, splits, trims and repairs captures; see the
CLI reference. It never opens a Modbus connection.
Read a capture in Go:
r, err := mbcap.NewReader(f) // or jsonl.NewReader, mbcsv.NewReader
if err != nil {
return err
}
for {
rec, err := r.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if s, ok := rec.(capture.Sample); ok {
fmt.Printf("%v: % x -> % x\n", r.Header().Duration(s.At), s.Request, s.Response)
}
}Write one with mbcap.NewWriter, convert with capture.Copy, edit with the edit package: see the
library guide.
| Package | Responsibility |
|---|---|
capture |
the model, the Reader and Writer interfaces, and the rules every file follows |
mbcap |
binary reader and writer, index access, repair |
jsonl |
JSON Lines reader and writer |
mbcsv |
long CSV reader and writer, wide CSV view |
edit |
filter, trim, split and merge, each writing a new capture with provenance |
cmd/mbcapctl |
the CLI |
Standard library only, no dependencies; Go 1.23 or later. Every reader checks every rule and is fuzzed against hostile input.
The format is draft 0.1: implemented, tested and pinned by golden files, but not frozen. It becomes 1.0 once its coding is calibrated on real captures; files written now will not be readable by 1.0. See the release notes.
| Page | |
|---|---|
| Getting started | install, inspect, convert, read from Go |
| Concepts | targets, streams, samples, time, sample and hold |
| Encodings | binary, JSON Lines, CSV, with examples |
| mbcapctl | every command and flag |
| Go library | reading, writing, converting, editing |
| Format specification | the normative specification |
| All documentation | including project internals |
make checkmake test runs the tests with the race detector, make fuzz every fuzz target, make bench the
benchmarks, and make golden regenerates the derived golden files in testdata/ from their hand-written
JSON Lines sources.
MIT, see LICENSE.