Read registers from any Modbus device over TCP or RS-485 – read-only, raw words always shown.
A universal, read-only Modbus reader – address, register and type in, value out. No device knowledge in the tool, no built-in register map; it works with any Modbus device. Meant for checking register maps from manuals or community reverse engineering against the real device.
Speaks Modbus TCP and Modbus RTU (serial). It calls no write function of the Modbus library at all – a tool without a write path cannot write by accident.
modbusread <target> <address> <type> [flags]
Address in decimal (42082) or hex (0xA462), type raw, uint16, int16, uint32,
int32, float32, float64 or string.
The target decides the transport, without an extra flag:
| Input | Transport |
|---|---|
192.168.1.50, plc.local:1502 |
Modbus TCP (port defaults to 502) |
/dev/ttyUSB0, /dev/tty.usbserial-…, COM3 |
Modbus RTU over the serial line |
rtu://…, tcp://…, udp://…, rtuovertcp://…, rtuoverudp://… |
explicit, overrides the detection |
tcp+tls:// is rejected outright rather than silently ignored – the library could do
it, but it needs certificates this tool does not manage, and offering it untested would
be a promise with nothing behind it.
$ modbusread 192.168.1.50 42082 uint16
addr raw value
42082 0x0064 100
# A float with swapped words (high word in the second register)
$ modbusread 192.168.1.50 40574 float32 --word-order low
# Dump a range to find unknown registers
$ modbusread 192.168.1.50 40520 raw --count 120 --out hex
# Find out which register reacts to a change on the device
$ modbusread 192.168.1.50 40520 raw --count 120 --interval 1s --on-changeThe addresses in these examples come from the EcoFlow PowerOcean, the device the tool was first written for; its register map lives in womat/ecoflow.
Important: addresses are never converted – they go on the wire exactly as typed (0-based). Many register maps are documented 1-based, some are not, and often it is not clear which. Converting is therefore left to the human, so the tool does not hide an assumption.
With a serial target the line parameters come in – --baud (19200), --databits (8),
--parity (none) and --stopbits. For the latter, 0 follows the Modbus rule: two stop
bits without parity, one with. They have to match the device exactly, otherwise you get
garbage or nothing at all. A bus typically has several devices on it, so --unit is no
longer a formality there:
$ modbusread /dev/ttyUSB0 40069 uint16 --baud 9600 --parity even --unit 3On a TCP target these flags are rejected rather than ignored – a baud rate that silently has no effect sends you off debugging the wiring.
More flags: --unit, --fc holding|input, --byte-order, --timeout, --json,
--samples. modbusread --help shows everything. --samples and --on-change belong to
--interval and are rejected without it, for the same reason as the serial flags.
The raw words are always printed, even when a value was decoded – when reverse
engineering a register map, the raw value matters more than the interpretation. A
register that could not be read shows - there (null in --json) next to the error,
never 0x0000: a zero would look like something the device sent. A float that is NaN or
infinite is null in --json, since JSON has no number for it; its bits are in raw.
The exit code is non-zero when not a single value could be read – a uint32 with one of
its two registers missing counts as not read. In a range, one readable value is enough.
Ctrl-C ends a read after the request in flight. A device that does not answer makes every
request wait for --timeout, so that is at most one timeout, not one per register.
Connecting over the network is the exception: the Modbus library waits up to 5 s for it
whatever --timeout says, and Ctrl-C takes effect only after that.
With a Go toolchain:
go install github.com/womat/modbusread@latest
For machines without Go – such as a Raspberry Pi next to the device – ready-made
binaries are under Releases. They are
statically linked (CGO_ENABLED=0), so there is nothing to install: unpack and run.
VERSION=0.8.0 # without the v - see the releases page for the latest
ARCH=linux_arm64 # see the table below
curl -LO "https://github.com/womat/modbusread/releases/download/v$VERSION/modbusread_${VERSION}_$ARCH.tar.gz"
tar -xzf "modbusread_${VERSION}_$ARCH.tar.gz" modbusread
./modbusread --version| Machine | ARCH |
|---|---|
| Raspberry Pi 3/4/5/Zero 2 W with a 64-bit OS | linux_arm64 |
| Raspberry Pi 2/3/4/5/Zero 2 W with a 32-bit OS | linux_armv7 |
| Raspberry Pi 1 and Zero (1st gen) | linux_armv6 |
| ordinary Linux PC/server, NAS | linux_amd64 |
| Mac with Apple Silicon | darwin_arm64 |
| Mac with Intel | darwin_amd64 |
| Windows | windows_amd64 (.zip) |
The download can be checked against the checksums.txt of the same release:
curl -LO "https://github.com/womat/modbusread/releases/download/v$VERSION/checksums.txt"
sha256sum -c checksums.txt --ignore-missingEvery release on the releases page carries
archives for Linux (including all Raspberry Pi architectures), macOS and Windows with the
binary, README.md and LICENSE, plus a checksums.txt and a changelog. Versions follow
semantic versioning.
modbusread --version reports the release a binary was built from. A local build reports
the commit instead, with +dirty when the working tree had changes – which is how the two
are told apart.
Up to v0.6.0 modbusread lived in womat/ecoflow as
cmd/modbusread; those releases stay there, and
go install github.com/womat/ecoflow/cmd/modbusread@v0.6.0 still works. From v0.7.0 on
it is released here. The version numbers continue.
Building from source needs Go and make; make help lists the targets:
make test # go test -race ./... - no hardware: the integration tests start a Modbus server
make lint # gofmt, go fix -diff, go mod tidy -diff, go vet, golangci-lint, govulncheck
make build # ./modbusread for this machine
make snapshot # all release archives into ./dist, without publishing (needs goreleaser)
A release is a tag vX.Y.Z on main, made with make release TAG=vX.Y.Z; the workflow
tests the tagged commit again and GoReleaser publishes the archives.
modbusread is released under the MIT License – see LICENSE for the full text.
The source tree contains no third-party code, but a compiled binary statically links the modules below. Their terms apply to anyone distributing that binary, not to the sources here.
| Module | License |
|---|---|
github.com/simonvetter/modbus |
MIT |
github.com/goburrow/serial |
MIT |
Both are permissive.