Skip to content
Start free evaluation

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.
  • macOS or Linux and four terminals.
  • pdcomm-vserial on your PATH (see Install).
  • bin/pdcomm-modbus-headless from the Modbus SDK. The commands below find the SDK folder through the MODBUS_SDK environment variable.
  • The vserial/ folder of the examples. Run every command from this folder.
터미널 창
export MODBUS_SDK="$HOME/PDComm/pdcomm-modbus" # replace with your SDK folder
cd <examples folder>/vserial

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.

rs485-bus.json
{
"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"
}
  1. In terminal 1, start the lab:

    터미널 창
    pdcomm-vserial run --config rs485-bus.json
  2. When it’s ready, the lab prints its ports on one line. The path values 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}

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.

modbus-rtu/drive1.jsonl
{"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"}
  1. 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.

  2. 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"
modbus-rtu/master.jsonl
{"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"}
  1. 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.

  2. Type these lines into terminal 4, one at a time. pdu is the Modbus PDU in hexadecimal, and unit is 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 is 07 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"}}
  3. 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_response is expected:

    {"ok":true,"result":{"response":[6,0,1,0,100],"source":"wire","status":"success"}}
    {"ok":true,"result":{"response":null,"source":"wire","status":"no_response"}}
  4. 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"}}
  1. In terminal 1 (the lab), flag a parity error on the next character the master receives:

    {"command":"inject","node":"master","fault":{"error":"parity"}}
  2. 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}
  3. Send the same request again. This time the normal reply returns, because the fault applied to one character only.

  1. In terminal 1, unplug the cable of drive2:

    {"command":"attach","node":"drive2","attached":false}
  2. In terminal 4, a read from unit 2 now ends with {"error":"Timeout","ok":false}, because the unplugged device never receives the request.

  3. Plug the cable back in ("attached":true) and send the request again. [3,2,7,8] comes back.

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.

  1. 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.
  2. Type {"command":"stop"} in terminal 1. The lab removes the /tmp/pdcomm-* links and exits.
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.