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 |
JSON lines
Section titled “JSON lines”- 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
pollabout every 2 ms, so servers and gateways keep answering. - If
pollfails, it writesEndpoint 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
shutdownor EOF doesn’t clear the error.
JSON command reference lists the commands and their members.
Interactive use
Section titled “Interactive use”"$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/nullThis 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.
Scenarios
Section titled “Scenarios”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:
[ {"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 matchto 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
expectthat isn’t an object fails withExpected 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
executechecks and clientrequestchecks.
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:
[ {"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.jsonBefore a scenario writes to a real device, confirm in the manual that the address is safe to write.
Project files
Section titled “Project files”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.jsonTo 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.jsonlecho '{"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. kindmust bepdcomm.modbus.projectandversion1; otherwise you getUnsupported 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
securityare stored as paths. Fix the paths before using a project on another computer. Key material never goes into a project.
Next steps
Section titled “Next steps”- Replay a GUI project headless in Build a test workflow with the simulators.
- To use the same commands from AI tools, see The MCP server.