# Modbus CLI utility for client and server testing

**URL:** <https://forum.inductiveautomation.com/t/modbus-cli-utility-for-client-and-server-testing/111882>\
**Category:** General Discussion\
**Tags:** modbus\
**Created:** [November 2, 2025, 11:48pm UTC](https://forum.inductiveautomation.com/t/modbus-cli-utility-for-client-and-server-testing/111882 "2025-11-02T23:48:41Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![Kevin.Herron](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/kevin.herron/32/7259_2.png) [@Kevin.Herron](https://forum.inductiveautomation.com/u/Kevin.Herron)\
**Post date:** [November 2, 2025, 11:48pm UTC](https://forum.inductiveautomation.com/t/modbus-cli-utility-for-client-and-server-testing/111882/1 "2025-11-02T23:48:41Z")

</div>

I started working on a little Modbus CLI utility: [GitHub - kevinherron/modbus-cli: Native CLI utility for reading, writing, and scanning Modbus TCP](https://github.com/kevinherron/modbus-cli)

Ripped from the README, some quick examples:

## Quick Examples

**Start a test server:**

```bash
$ modbus server
Modbus server started on 0.0.0.0:502

```

The server initializes with 65536 holding registers, each pre-populated with its address as a value.  
Leave this running in one terminal while trying the client examples below in another.

**Read holding registers:**

```bash
$ modbus client localhost rhr 0 10
Hostname: localhost:502, Unit ID: 1
→ ReadHoldingRegistersRequest[address=0, quantity=10]
← ReadHoldingRegistersResponse[registers=0000000100020003000400050006000700080009]
Offset (hex)	Bytes (hex)
-------------------------
00000000	00 00 00 01 00 02 00 03 00 04 00 05 00 06 00 07
00000010	00 08 00 09 .. .. .. .. .. .. .. .. .. .. .. ..

```

**Get JSON output for automation:**

```bash
$ modbus --format=json client localhost rhr 0 10
{"timestamp":"2025-11-02T23:07:57.618695Z","type":"info","message":"Hostname: localhost:502, Unit ID: 1"}
{"timestamp":"2025-11-02T23:07:57.627904Z","type":"register_table","start_address":0,"quantity":10,"data":[0,0,0,1,0,2,0,3,0,4,0,5,0,6,0,7,0,8,0,9]}

```

**Write then read back a register:**

```bash
$ modbus --format=json client localhost wsr 100 42
{"timestamp":"2025-11-02T23:08:09.316542Z","type":"protocol","direction":"received","function_code":6,"pdu":"060064002a"}

$ modbus --format=json client localhost rhr 100 1
{"timestamp":"2025-11-02T23:08:09.332309Z","type":"register_table","start_address":100,"quantity":1,"data":[0,42]}

```

**Scan a range of registers:**

```bash
$ modbus client localhost scan 0 50 --size=10
Hostname: localhost:502, Unit ID: 1
Address Values (hex, 2 bytes each)
-------------------------------------------------------
0000 0000 0001 0002 0003 0004 0005 0006 0007
0008 0008 0009 000A 000B 000C 000D 000E 000F
0010 0010 0011 0012 0013 0014 0015 0016 0017
0018 0018 0019 001A 001B 001C 001D 001E 001F
0020 0020 0021 0022 0023 0024 0025 0026 0027
0028 0028 0029 002A 002B 002C 002D 002E 002F
0030 0030 0031

```

**Poll and filter JSON output with jq:**

```bash
$ modbus --format=json client localhost rhr 0 10 -c 5 | jq -c 'select(.type == "register_table") | {timestamp, data}'
{"timestamp":"2025-11-02T23:12:37.072923Z","data":[0,0,0,1,0,2,0,3,0,4,0,5,0,6,0,7,0,8,0,9]}
{"timestamp":"2025-11-02T23:12:38.075708Z","data":[0,0,0,1,0,2,0,3,0,4,0,5,0,6,0,7,0,8,0,9]}
{"timestamp":"2025-11-02T23:12:39.081328Z","data":[0,0,0,1,0,2,0,3,0,4,0,5,0,6,0,7,0,8,0,9]}
{"timestamp":"2025-11-02T23:12:40.086801Z","data":[0,0,0,1,0,2,0,3,0,4,0,5,0,6,0,7,0,8,0,9]}
{"timestamp":"2025-11-02T23:12:41.088714Z","data":[0,0,0,1,0,2,0,3,0,4,0,5,0,6,0,7,0,8,0,9]}

```

It's still at version 0.1-SNAPSHOT. There are binaries you might be able to download [here](https://github.com/kevinherron/modbus-cli/actions/runs/19019603044), not sure about the permissions though.

If anybody wants to play with it and give me some feedback about the output format or missing features that would be helpful.

Already I'm pretty sure I want the data output in the examples above to be an array of 16-bit register values instead of bytes, and then to include a "raw\_data" field that has the raw hex-encoded bytes similar to the "pdu" field in the protocol messages (`"pdu":"060064002a"`).

The default server behavior is kind of a placeholder as well. Haven't thought too much about that side of it yet.

---

<div class="post-metadata">

**Author:** ![Kevin.Herron](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/kevin.herron/32/7259_2.png) [@Kevin.Herron](https://forum.inductiveautomation.com/u/Kevin.Herron)\
**Post date:** [February 16, 2026, 6:25pm UTC](https://forum.inductiveautomation.com/t/modbus-cli-utility-for-client-and-server-testing/111882/2 "2026-02-16T18:25:50Z")

</div>

I added Modbus RTU / Serial support this morning.

> **[Release Modbus CLI v0.2 · kevinherron/modbus-cli](https://github.com/kevinherron/modbus-cli/releases/tag/v0.2)**
>
> What's Changed
> 
> Add Linux ARM64 native image builds to CI workflows by @Copilot in #10
> Add Modbus RTU serial support to client and server commands by @kevinherron in #11
> 
> Full Changelog: v0.1...v0.2

Server:

```python
kevin@kevin-x1 ~> modbus server rtu:/dev/ttyUSB0
Modbus RTU server started on /dev/ttyUSB0

```

Client:

```python
kevin@kevin-x1 ~> modbus client rtu:/dev/ttyUSB1 rhr 0 10
Serial Port: /dev/ttyUSB1, Unit ID: 1
→ ReadHoldingRegistersRequest[address=0, quantity=10]
← ReadHoldingRegistersResponse[registers=0000000100020003000400050006000700080009]
Offset (hex) Bytes (hex)
-------------------------
00000000 00 00 00 01 00 02 00 03 00 04 00 05 00 06 00 07
00000010 00 08 00 09 .. .. .. .. .. .. .. .. .. .. .. ..

```
