Skip to content
Start free evaluation

Headless runs and scenarios

pdcomm-modbus-headless is the Modbus simulator without a window. It takes the same service commands as the GUI on standard input. Use it to run servers and clients in CI, on remote machines and from scripts.

Command Behavior Exit code
pdcomm-modbus-headless Reads JSON commands line by line from standard input. 0 when every command succeeded, 1 when any failed
pdcomm-modbus-headless --scenario <file> Runs the steps of a scenario file and compares expectations. 0 on pass, 1 on a mismatch or an error
Any other arguments Prints Usage: pdcomm-modbus-headless [--scenario path] on standard error. 2
  • Put one JSON object on each line, at most 1 MiB. A JSON object split across lines fails, because each line is parsed separately.
  • Each command produces one reply line on standard output: {"ok":true,"result":...} on success, {"ok":false,"error":"..."} on failure.
  • A JSON syntax error also gets a reply, such as {"ok":false,"error":"key must be a string at line 1 column 2"}.
  • {"action":"shutdown"} ends the program without a reply, as does closing the input (EOF).
  • While waiting for input, the program runs poll about every 2 ms, so servers and gateways keep answering.
  • If poll fails, it writes Endpoint poll failed: <error> to standard error and disconnects.
  • Standard output carries replies only. The diagnostic log goes to a file or to standard error.
  • After an error, later commands still run, but the exit code is 1. Ending with shutdown or EOF doesn’t clear the error.

JSON command reference lists the commands and their members.

터미널 창
"$MODBUS_SDK/bin/pdcomm-modbus-headless"

Paste commands into the terminal one line at a time. When it acts as a server, keep the terminal open: closing the input stops the server too.

You can prepare commands in a file or produce them with another program. The headless simulator writes all replies and exits at the end of the input.

터미널 창
printf '%s\n' '{"action":"execute","pdu":"060000002A"}' '{"action":"execute","pdu":"0300000001"}' | "$MODBUS_SDK/bin/pdcomm-modbus-headless"
{"ok":true,"result":{"response":[6,0,0,0,42],"source":"local","status":"success"}}
{"ok":true,"result":{"response":[3,2,0,42],"source":"local","status":"success"}}

To keep a server up for a while, close the input later:

터미널 창
(printf '%s\n' '{"action":"configure","config":{"transport":"tcp","role":"server","address":"127.0.0.1:1502","port":"","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000}}' '{"action":"connect"}'; sleep 4) | "$MODBUS_SDK/bin/pdcomm-modbus-headless" > /dev/null

This server answers on 127.0.0.1:1502 for four seconds, then exits. For a long-running server, use a wrapper that keeps the input pipe open, like the example peer.py.

A scenario file is a JSON array of steps, up to 1 MiB. Each step has:

Member Required Description
command Yes The command object to run
expect No Members to compare with the result. Only the listed members are compared, exactly.

The example scenario checks the build’s features, then a write, a read and an exception:

examples/modbus-tutorial/scenario.json
[
{"command":{"action":"capabilities"},"expect":{"protocol":"modbus","canopen":false}},
{"command":{"action":"execute","pdu":"060000002A"},"expect":{"response":[6,0,0,0,42],"status":"success"}},
{"command":{"action":"execute","pdu":"0300000001"},"expect":{"response":[3,2,0,42],"status":"success"}},
{"command":{"action":"execute","pdu":"0300000000"},"expect":{"response":[131,3],"status":"exception"}}
]
터미널 창
"$MODBUS_SDK/bin/pdcomm-modbus-headless" --scenario "$MODBUS_EXAMPLES/modbus-tutorial/scenario.json"
{"result":{"ascii":true,"canopen":false,...,"protocol":"modbus","rtu":true,"security":true,"tcp":true},"step":1}
{"result":{"response":[6,0,0,0,42],"source":"local","status":"success"},"step":2}
{"result":{"response":[3,2,0,42],"source":"local","status":"success"},"step":3}
{"result":{"response":[131,3],"source":"local","status":"exception"},"step":4}
  • Each step prints {"step":N,"result":...}.
  • On a mismatch it writes Scenario step N field <member> did not match to standard error and exits with code 1 at once.
  • When a command fails, it writes the error message (such as Connect a client before sending a request) and exits with code 1.
  • An expect that isn’t an object fails with Expected fields must be an object.
  • A scenario starts a new session and never polls. A server it connects doesn’t answer requests, so use scenarios for local execute checks and client request checks.

A client scenario can also check a running device or server. This one writes to and reads from a server on 127.0.0.1:1502:

device-check.json
[
{"command":{"action":"configure","config":{"transport":"tcp","role":"client","address":"127.0.0.1:1502","port":"","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000}}},
{"command":{"action":"connect"},"expect":{"connected":true}},
{"command":{"action":"request","pdu":"060000002A"},"expect":{"status":"success"}},
{"command":{"action":"request","pdu":"0300000001"},"expect":{"response":[3,2,0,42],"source":"wire"}}
]
터미널 창
"$MODBUS_SDK/bin/pdcomm-modbus-headless" --scenario device-check.json

Before a scenario writes to a real device, confirm in the manual that the address is safe to write.

A project is a JSON file with the configuration and the data model. The GUI’s Project menu, the headless simulator, the MCP server and the SDK share the format.

{
"kind": "pdcomm.modbus.project",
"version": 1,
"config": {"transport": "tcp", "role": "server", "address": "127.0.0.1:1502", ...},
"model": {"coils": {...}, "discrete_inputs": {...}, "holding_registers": {...}, "input_registers": {...},
"files": {...}, "fifos": {...}, "objects": {...}, "server_id": [1, 255],
"exception_status": 0, "individual_access": true}
}

Save only the result of project_export, not the whole {"ok":true,...} reply:

터미널 창
printf '%s\n' '{"action":"data_set","table":"holding_registers","address":0,"values":[42,7]}' '{"action":"project_export"}' | "$MODBUS_SDK/bin/pdcomm-modbus-headless" | tail -n 1 | jq '.result' > plant.modbus.json

To load it, put the file into the project member. Build the JSON with a JSON tool such as jq, not with string replacement.

터미널 창
jq -c '{action:"project_import",project:.}' plant.modbus.json > replay.jsonl
echo '{"action":"execute","pdu":"0300000002"}' >> replay.jsonl
"$MODBUS_SDK/bin/pdcomm-modbus-headless" < replay.jsonl
{"ok":true,"result":{"loaded":true}}
{"ok":true,"result":{"response":[3,4,0,42,0,7],"source":"local","status":"success"}}

Loading rules:

  • Load only while disconnected; otherwise you get Disconnect before loading a project.
  • kind must be pdcomm.modbus.project and version 1; otherwise you get Unsupported Modbus project kind or version.
  • Unknown or missing members fail (unknown field ..., missing field ...).
  • The configuration and the model are both validated before anything changes; on failure the current state stays as it was.
  • Certificates and keys in security are stored as paths. Fix the paths before using a project on another computer. Key material never goes into a project.