Test Modbus RTU on a virtual RS-485 bus
In this tutorial you build a Modbus RTU bus without any RS-485 wiring. Two servers that play variable-frequency drives (drive1 and drive2) and one master client share a virtual two-wire bus. You read, write, and broadcast, and then see how a parity error and an unplugged cable affect the traffic.
- Connect three Modbus RTU devices to a virtual RS-485 bus.
- Confirm that a broadcast write (unit 0) reaches every server.
- Reproduce client timeouts with fault injection and an unplugged cable, and find the cause in the trace.
What you need
Section titled “What you need”- macOS or Linux and four terminals.
pdcomm-vserialon yourPATH(see Install).bin/pdcomm-modbus-headlessfrom the Modbus SDK. The commands below find the SDK folder through theMODBUS_SDKenvironment variable.- The
vserial/folder of the examples. Run every command from this folder.
export MODBUS_SDK="$HOME/PDComm/pdcomm-modbus" # replace with your SDK foldercd <examples folder>/vserialStep 1: Start the bus
Section titled “Step 1: Start the bus”The lab file sets three nodes to 19200 bit/s 8E1 and wires them to a two-wire bus. Each node opens as a pseudo terminal with a fixed link, and line events go to rs485-trace.jsonl.
{ "engine": { "pacing": "realistic", "nodes": [ {"name": "master", "port": {"line": {"baud": 19200, "data_bits": 8, "parity": "even", "stop_bits": "1"}}}, {"name": "drive1", "port": {"line": {"baud": 19200, "data_bits": 8, "parity": "even", "stop_bits": "1"}}}, {"name": "drive2", "port": {"line": {"baud": 19200, "data_bits": 8, "parity": "even", "stop_bits": "1"}}} ], "media": [{"kind": "two_wire", "nodes": ["master", "drive1", "drive2"]}] }, "ports": [ {"node": "master", "backend": {"type": "pty", "link": "/tmp/pdcomm-master"}}, {"node": "drive1", "backend": {"type": "pty", "link": "/tmp/pdcomm-drive1"}}, {"node": "drive2", "backend": {"type": "pty", "link": "/tmp/pdcomm-drive2"}} ], "trace": "rs485-trace.jsonl"}-
In terminal 1, start the lab:
터미널 창 pdcomm-vserial run --config rs485-bus.json -
When it’s ready, the lab prints its ports on one line. The
pathvalues differ from run to run.{"ports":[{"backend":"pty","link":"/tmp/pdcomm-master","node":"master","path":"/dev/ttys011"},{"backend":"pty","link":"/tmp/pdcomm-drive1","node":"drive1","path":"/dev/ttys016"},{"backend":"pty","link":"/tmp/pdcomm-drive2","node":"drive2","path":"/dev/ttys017"}],"ready":true}
Step 2: Start two servers
Section titled “Step 2: Start two servers”The headless simulator (pdcomm-modbus-headless) processes JSON actions from standard input, one per line. The server file configures an RTU server, puts a value in holding register 0, and opens the port.
{"action":"configure","config":{"transport":"rtu","role":"server","address":"","port":"/tmp/pdcomm-drive1","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000}}{"action":"data_set","table":"holding_registers","address":0,"values":[1500]}{"action":"connect"}-
In terminal 2, start the unit 1 server.
cat file -sends the file and then passes on what you type, and the server runs until its input ends.터미널 창 cat modbus-rtu/drive1.jsonl - | "$MODBUS_SDK/bin/pdcomm-modbus-headless"Three replies appear, and the last one contains
"connected":true. -
In terminal 3, start the unit 2 server, whose register 0 holds 1800:
modbus-rtu/drive2.jsonl {"action":"configure","config":{"transport":"rtu","role":"server","address":"","port":"/tmp/pdcomm-drive2","unit":2,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000}}{"action":"data_set","table":"holding_registers","address":0,"values":[1800]}{"action":"connect"}터미널 창 cat modbus-rtu/drive2.jsonl - | "$MODBUS_SDK/bin/pdcomm-modbus-headless"
Step 3: Read and write from the client
Section titled “Step 3: Read and write from the client”{"action":"configure","config":{"transport":"rtu","role":"client","address":"","port":"/tmp/pdcomm-master","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000}}{"action":"connect"}-
In terminal 4, start the client:
터미널 창 cat modbus-rtu/master.jsonl - | "$MODBUS_SDK/bin/pdcomm-modbus-headless"The client is ready when the second reply contains
"connected":true. -
Type these lines into terminal 4, one at a time.
pduis the Modbus PDU in hexadecimal, andunitis the device address.{"action":"request","unit":1,"pdu":"0300000001"}{"action":"request","unit":2,"pdu":"0300000001"}Function code 03 (read holding registers) reads register 0 of each server. 1500 is
05 DC(5, 220) and 1800 is07 08(7, 8):{"ok":true,"result":{"response":[3,2,5,220],"source":"wire","status":"success"}}{"ok":true,"result":{"response":[3,2,7,8],"source":"wire","status":"success"}} -
Write 100 to register 1 of unit 1 (function code 06), and broadcast 7 to register 2 through unit 0:
{"action":"request","unit":1,"pdu":"0600010064"}{"action":"request","unit":0,"pdu":"0600020007"}A write returns the request as its reply. A broadcast has no reply, so
no_responseis expected:{"ok":true,"result":{"response":[6,0,1,0,100],"source":"wire","status":"success"}}{"ok":true,"result":{"response":null,"source":"wire","status":"no_response"}} -
Read registers 0 to 2 of unit 2 to check that the broadcast arrived:
{"action":"request","unit":2,"pdu":"0300000003"}The last register holds 7:
{"ok":true,"result":{"response":[3,6,7,8,0,0,0,7],"source":"wire","status":"success"}}
Step 4: Reproduce a parity error
Section titled “Step 4: Reproduce a parity error”-
In terminal 1 (the lab), flag a parity error on the next character the master receives:
{"command":"inject","node":"master","fault":{"error":"parity"}} -
In terminal 4, read unit 1 again:
{"action":"request","unit":1,"pdu":"0300000001"}The first character of the reply is damaged, so the client rejects the reply and times out after one second:
{"error":"Timeout","ok":false} -
Send the same request again. This time the normal reply returns, because the fault applied to one character only.
Step 5: Unplug a cable
Section titled “Step 5: Unplug a cable”-
In terminal 1, unplug the cable of
drive2:{"command":"attach","node":"drive2","attached":false} -
In terminal 4, a read from unit 2 now ends with
{"error":"Timeout","ok":false}, because the unplugged device never receives the request. -
Plug the cable back in (
"attached":true) and send the request again.[3,2,7,8]comes back.
Step 6: Read the trace
Section titled “Step 6: Read the trace”Find the parity error from step 4 in rs485-trace.jsonl, in the folder where you started the lab:
grep '"error":"parity"' rs485-trace.jsonl{"t_ns":842903959,"node":"master","event":"rx","byte":1,"error":"parity"}node is master and byte is 1, the address character of unit 1, which confirms that the first character of the reply carried the error. Unplugging shows up as "event":"attached" lines.
Clean up
Section titled “Clean up”- Press Ctrl+D in terminals 4, 3, and 2 to end their input. Each headless simulator closes its port and exits; the client, which saw failed actions, exits with code 1.
- Type
{"command":"stop"}in terminal 1. The lab removes the/tmp/pdcomm-*links and exits.
Common errors
Section titled “Common errors”| Symptom | Cause and fix |
|---|---|
connect fails with Io(Other, "No such file or directory") |
The lab isn’t running or the link path differs. Check the port list in terminal 1. |
configure fails with Invalid endpoint configuration |
No parity ("none") with one stop bit. Use even parity or two stop bits. |
Every request ends with Timeout |
Check the server’s unit, speed, and parity; a mismatch garbles characters on the virtual bus just as on a real line. |
A lab command returns unknown node |
Use master, drive1, or drive2 as node. |
Next steps
Section titled “Next steps”- See the ASCII and SECS-I settings in Use virtual ports with Modbus RTU, ASCII, and SECS-I.
- Trace the same requests through the SDK log in Capture and read a diagnostic log.