Skip to content

modbusctl 3.1.0: record and replay every spec read function; capture commands replace mcap - #2

Merged
boeboe merged 7 commits into
mainfrom
feat/serve-all-functions
Oct 15, 2026
Merged

boeboe merged 7 commits into
mainfrom
feat/serve-all-functions

Conversation

@boeboe

@boeboe boeboe commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

modbusctl 3.1.0: record and replay every read function of the Modbus specification on go-modbus v1.7.1 and go-modbus-capture (MBCAP). Vendor and unknown function codes are not supported anywhere. The mcap command group becomes capture, and MCAP v1 references are removed.

Dependencies: go-modbus v1.7.1 (v1.7.1 removes RawRequestHandler, which this PR does not use) and go-modbus-capture v0.0.2. Its capture.CheckPDUs accepts only the spec read functions in every reader and writer; capfile.IsSpecRead builds on capture.IsReadFunction with the same FC08/FC43 rules. A file the library rejects for a sample's function code is still reported as capture_unsupported_function (exit 3), not as a corrupt file.

Record and replay

  • client record takes a plan (--blocks-file). Entries with function may use FC01–04, FC07, FC08 (data-returning sub-functions 0x00, 0x02, 0x0B–0x12), FC11, FC12, FC17, FC20, FC24 or FC43/14. Plain address blocks work as before. Anything else is refused before it is sent (internal/plan). Each request uses the retry loop of reads.
  • server static / server replay serve FC08, FC17, FC20 and FC24 through go-modbus v1.7.1's typed handlers, alongside FC01–04, FC07, FC11, FC12 and FC43. No RawRequestHandler: go-modbus answers every other function code, vendor and unknown ones included, with Illegal Function.
  • Loading rejects a capture holding a non-spec sample (capture_unsupported_function, exit 3), and the recorder never writes one.
  • --listen on both servers (default: every interface). The "🚀 Serving" line is printed once the listener is bound.

MCAP v1 removal

  • Commands: modbusctl mcap … → modbusctl capture … (decode, extract, strings, frequencies). info, verify and convert are removed because they duplicate mbcapctl; the README points to it. internal/mcap → internal/analysis.
  • JSON and error codes: mcap_output_path → capture_output_path; mcap_load_failed / mcap_write_failed → capture_load_failed / capture_write_failed.
  • Files: Docker volume /captures; docker-compose.yml, Makefile and .gitignore name .mbcap. ALGOS, CONTRIBUTING, SECURITY and docs updated. The README says once that 2.x .mcap files are not read.
  • Docs: the README links go-modbus-capture's FORMAT.md and mbcapctl instead of repeating them, and documents the plan format. RELEASE.md has a v3.1.0 entry with the CLI renames listed as breaking.

The intermittent internal/modbus failure

Root cause: a port race in the test helpers. They bound 127.0.0.1:0, closed it, and started the server on 0.0.0.0:N. Under load another test's socket took N in between. Then either the bind failed (address already in use), or, on macOS, the wildcard bind succeeded next to another test's 127.0.0.1:N listener: the readiness probe connected to that listener and the test client talked to the wrong device (unexpected EOF).

Evidence: -race -count=50 -cpu 1,2,8 on the old code under load gave 4 failures, all this race (two address already in use, two unexpected EOF).

Fix:

  • Test servers listen on 127.0.0.1 exactly (--listen).
  • Readiness is the server's own "Serving" line after the bind, not a probe connection.
  • Only a bind that fails with EADDRINUSE moves to another port.
  • TestCollServer_portTakenOnLoopback pins this down.

Tests

  • TestRoundTrip_everyFunction: record → file → replay for every supported function. Sources: a go-modbus v1.7.1 server, and mbtest over tcp, rtuovertcp and udp. Each file passes mbcapctl verify; static and replay servers answer byte for byte.
  • TestRoundTrip_textEncodings: mbcapctl convert to .mbcap.jsonl and .mbcap.csv serves the same.
  • TestChaosRoundTrip_everyFunction: busy, dropped, closed, truncated and late answers over tcp and rtuovertcp. Only valid answers the device actually sent are recorded.
  • TestParallelRoundTrip_everyFunction: four devices recorded at once, then 8 clients per recording against static and replay servers.
  • mbtest: RTU framing for every spec read function.
  • tcp+tls: not covered, because modbusctl's client has no TLS certificate settings (tcp+tls is not usable from the CLI today).
  • Checks: make check passes (fmt, staticcheck, golangci-lint 0 issues, vet, -race tests, coverage 96.4%).

🤖 Generated with Claude Code

boeboe and others added 2 commits October 15, 2026 02:10
go-modbus v1.7.0 dispatches every function code and hands the ones a
handler does not decode to an optional RawRequestHandler. The capture
handler implements it: FC07, FC08, FC0B, FC0C, FC11, FC12, FC17, FC20,
FC24, other FC43 MEI types and vendor functions are answered with the
response recorded for exactly that request PDU, byte for byte. The
typed FC07/0B/0C handlers go, as the raw path covers them. FC21, FC22
and FC23 writes are refused with Illegal Function.

E2E tests record FC08, FC17, FC20 and FC24 from a go-modbus server and
check the static and replay servers answer them byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Per the owner, vendor and unknown function codes are not supported.
Drop the RawRequestHandler and serve FC08 (data-returning
sub-functions 0x00, 0x02, 0x0B-0x12), FC17, FC20 and FC24 through
go-modbus v1.7.0's DiagnosticsHandler, ReportServerIDHandler,
ReadFileRecordHandler and FIFOQueueHandler, with the data taken from
the recorded response PDU. FC07/0B/0C/43 keep their typed handlers.
go-modbus answers every other function code with Illegal Function.

capfile.IsSpecRead defines the supported reads. Load rejects a capture
holding any other sample (capture_unsupported_function, exit 3), and
the recorder refuses to write one. Tests cover refusal on load and in
the recorder, Illegal Function for vendor, unknown, FC43/0D, FC08
state-changing and FC21-23 requests on a live server, and vendor
--function values in read, scan and record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@boeboe boeboe changed the title Serve every recorded function on the static and replay servers (go-modbus v1.7.0) Serve FC08, FC17, FC20 and FC24 from captures; refuse vendor and unknown functions (go-modbus v1.7.0) Oct 15, 2026
boeboe and others added 2 commits October 15, 2026 03:03
Port race (the internal/modbus failure seen once): test helpers picked
a port by binding 127.0.0.1:0 and closing it, and the server then bound
0.0.0.0:N. Under load another test's socket took N in between: either
EADDRINUSE, or on macOS the wildcard bind succeeded next to another
test's 127.0.0.1:N listener, the probe connected to that listener and
the test client talked to the wrong device (unexpected EOF). The
servers get --listen; they report "Serving" only after the listener is
bound; test helpers bind 127.0.0.1 exactly, wait for that report and
move to another port only on EADDRINUSE.

Recording: the --blocks-file plan (internal/plan) may name any read
function of the specification: FC01-04 blocks as before, FC07, FC08
data-returning sub-functions, FC11, FC12, FC17, FC20, FC24 and FC43/14.
Anything else is refused before it is sent. client record sends each
entry with the retry loop of reads (exchange).

Tests: record -> file -> replay round trips for every function from a
go-modbus server and mbtest devices over tcp, rtuovertcp and udp, with
mbcapctl verify and convert; chaos (busy, drop, close, truncated,
late) and parallel variants. mbtest frames every spec read over RTU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `modbusctl mcap` becomes `modbusctl capture` (decode, extract,
  strings, frequencies). info, verify and convert are removed: they
  duplicate mbcapctl of go-modbus-capture, which the docs point to.
  internal/mcap becomes internal/analysis.
- JSON summaries say capture_output_path; error codes
  capture_load_failed and capture_write_failed.
- Docker volume /captures; compose, Makefile and .gitignore name .mbcap.
- README: capture files section links go-modbus-capture (FORMAT.md,
  mbcapctl) instead of repeating it; the plan file format of client
  record; --listen. RELEASE.md: 3.1.0 with the CLI renames as breaking.
- client record examples gave intervals in seconds; they are ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@boeboe boeboe changed the title Serve FC08, FC17, FC20 and FC24 from captures; refuse vendor and unknown functions (go-modbus v1.7.0) modbusctl 3.1.0: record and replay every spec read function; capture commands replace mcap Oct 15, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.0.2's readers and writers accept only the read functions of the
specification (capture.CheckPDUs). IsSpecRead now builds on
capture.IsReadFunction with the same FC08 and FC43 rules. A load that
the library rejects for a sample's function code is still reported as
ErrUnsupportedFunction (capture_unsupported_function, exit 3) rather
than a corrupt file. The test for it writes the bad samples as JSON
Lines by hand, since the writers no longer produce them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@boeboe
boeboe merged commit b821bfb into main Oct 15, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant