Headless simulator
pdcomm-simulator-headless is the SECS/GEM simulator without a window. It runs one scenario, writes one JSON object per line (JSON Lines) to standard output, and reports success or failure in its exit code, which makes it a good fit for automated tests and CI scripts.
Use it to:
- Run an internal host and equipment pair through one capability round trip.
- Start a single simulated equipment or host and connect your own program to it.
- Replay the profiles and scenarios of a project you saved in the GUI, and keep a trace file.
Before you begin
Section titled “Before you begin”- Set the
PDCOMM_SIMULATORvariable from Set up your environment. - The executable is
$Simulator\pdcomm-simulator-headless.exeon Windows and"$PDCOMM_SIMULATOR/pdcomm-simulator-headless"on Linux. On Linux, that file is a launcher script that sets the library paths and then runs the real program inbin/.
The commands on this page use a Linux or macOS shell. On Windows, run & "$Simulator\pdcomm-simulator-headless.exe" ... and read the exit code from $LASTEXITCODE.
Run a built-in scenario
Section titled “Run a built-in scenario”"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --scenario status-round-trip; echo "exit=$?"{"schema":1,"sequence":1,"scenario":"status-round-trip","scope":"pair","kind":"start","detail":"scenario-started","truncated":false}{"schema":1,"sequence":2,"scenario":"status-round-trip","scope":"equipment","kind":"endpoint","local_address":"127.0.0.1:49900","truncated":false}...{"schema":1,"sequence":7,"scenario":"status-round-trip","scope":"host","kind":"result","detail":"status-name","id":1001,"id_present":true,"value_present":false,"format":0,"preview":"","name":"EquipmentState",...}...{"schema":1,"sequence":10,"scenario":"status-round-trip","scope":"host","kind":"result","detail":"status-value","id":1001,"id_present":true,"value_present":true,"format":16,"preview":"IDLE",...}...{"schema":1,"sequence":15,"scenario":"status-round-trip","scope":"pair","kind":"summary","outcome":"passed","exit_code":0,"elapsed_ms":26,"records_emitted":15,"max_records":128,"truncated":false,...}exit=0Without --scenario, the simulator runs smoke. The port in the address and elapsed_ms change on every run.
Scenarios
Section titled “Scenarios”| Area | --scenario value |
What it checks |
|---|---|---|
| Basics | smoke |
Connection, Select and communication establishment |
status-round-trip |
Status variable names (S1F11) and values (S1F3) | |
data-report-round-trip |
Data variables and event reports | |
directory-round-trip |
Data and event directories (S1F21, S1F23) | |
| Queries, configuration, commands | report-query-round-trip |
Report queries |
remote-report-config-round-trip |
Host-side report definition, linking and enabling (S2F33, S2F35, S2F37) | |
typed-command-round-trip |
Remote commands | |
| Storage and constants | project-round-trip |
Saving and restoring a project document |
constants-snapshot-parity |
Equipment constant snapshots | |
| Virtual and declarative | secs1-virtual-pair |
The SECS-I virtual pair |
gem300-capability-profile-lifecycle |
The GEM300 capability catalog | |
| Formatted process programs | verification-report-round-trip, formatted-program-transfer-round-trip, formatted-program-store-reverse-round-trip, formatted-program-query-round-trip |
Verification reports, transfer, storage and queries |
formatted-program-selection-lifecycle, formatted-program-dry-run, formatted-program-execution-lifecycle |
Selection, structural analysis and execution | |
formatted-program-verification-request-round-trip, formatted-program-capability-round-trip |
Verification requests and capability queries | |
formatted-program-verification-negotiation-round-trip, formatted-program-verification-negotiation-reverse-round-trip, formatted-program-verification-negotiation-denied |
Verification negotiation | |
| Transport faults | transport-backpressure-recovery |
Write backpressure and recovery. Requires --transport-fault-profile write_backpressure. |
| External programs | external-peer |
Waits for S1F13 and S1F1 from an external host. See below. |
When the ...-denied scenario passes (exit_code 0), the rejection happened as intended and the state was preserved. Each scenario defines its own internal equipment catalog; don’t assume it matches the GUI’s default catalog.
Output format
Section titled “Output format”Every line has these fields:
| Field | Description |
|---|---|
schema |
Output format version, currently 1 |
sequence |
Line number, starting at 1 |
scenario |
The scenario you ran: the scenario ID in project mode, or invalid for argument errors |
scope |
Who produced the record: pair, host, equipment, secs1, project, transport, trace, cli and so on |
kind |
start, endpoint, request, result, error, summary and so on |
detail |
The step name, for example begin-communication or status-value |
truncated |
Whether a value on this line was shortened |
The last line always has "kind":"summary":
| Field | Description |
|---|---|
outcome |
passed, failed, timeout, interrupted, invalid-arguments (bad arguments) or invalid-input (bad input file) |
exit_code |
The same value as the process exit code |
elapsed_ms |
Run time in milliseconds |
records_emitted, max_records |
Lines written and the limit (128). At the limit, later records are dropped and truncated becomes true. |
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
| 0 | Passed |
| 2 | Bad arguments or a bad input file |
| 3 | The scenario failed (protocol error, failed request and so on) |
| 4 | Timed out (--timeout-ms) |
| 130 | Interrupted with Ctrl+C or SIGTERM |
An unknown scenario ends like this:
"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --scenario nope; echo "exit=$?"{"schema":1,"sequence":1,"scenario":"invalid","scope":"cli","kind":"error","detail":"scenario is unsupported","truncated":false}{"schema":1,"sequence":2,"scenario":"invalid","scope":"cli","kind":"summary","outcome":"invalid-arguments","exit_code":2,...}exit=2Options
Section titled “Options”| Option | Description |
|---|---|
--scenario <name> |
The scenario to run. Defaults to smoke. In project mode, a scenario ID from the project. |
--session-id <0-65535> |
The HSMS session ID. Defaults to 0. Match it to your peer when you connect an external program. |
--timeout-ms <100-60000> |
The scenario time limit. Defaults to 8000. |
--role host|equipment with --address <IPv4:port> |
Run a single role. Pass both options together; works with the smoke scenario only. |
--project <file> |
A project file saved from the GUI. Pass one of its scenario IDs in --scenario. |
--trace-output <file> |
In project mode, save the session trace to a file. |
--trace-input <file> |
Only validate a saved trace file. Can’t be combined with other options. |
--transport-fault-profile write_backpressure |
Inject write backpressure in the transport-backpressure-recovery scenario. |
There’s no --help option; any unknown option ends with exit code 2.
Test your own host program
Section titled “Test your own host program”The external-peer scenario starts a simulated equipment on a free port of 127.0.0.1 and waits until an external host sends S1F13 (establish communication) and S1F1 (Are You There) and then disconnects. Use it to test your host program automatically.
"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --scenario external-peer --session-id 7 --timeout-ms 20000The second line shows the equipment address:
{"schema":1,"sequence":2,"scenario":"external-peer","scope":"equipment","kind":"endpoint","local_address":"127.0.0.1:50092","truncated":false}Connect your host to that address. This example uses the tutorial C++ host; type establish, identity and quit:
"$PDCOMM_ROOT/build/pdcomm-tutorial" host active 127.0.0.1:50092When the host disconnects, the simulator reports what it saw and exits with code 0:
{"schema":1,"sequence":3,"scenario":"external-peer","scope":"equipment","kind":"result","detail":"s1f13","truncated":false}{"schema":1,"sequence":4,"scenario":"external-peer","scope":"equipment","kind":"result","detail":"s1f13-system-bytes=101","truncated":false}{"schema":1,"sequence":5,"scenario":"external-peer","scope":"equipment","kind":"result","detail":"s1f1","truncated":false}{"schema":1,"sequence":6,"scenario":"external-peer","scope":"equipment","kind":"result","detail":"s1f1-system-bytes=102","truncated":false}{"schema":1,"sequence":7,"scenario":"external-peer","scope":"equipment","kind":"result","detail":"disconnect","truncated":false}{"schema":1,"sequence":8,"scenario":"external-peer","scope":"pair","kind":"summary","outcome":"passed","exit_code":0,"elapsed_ms":2942,...}- Your host chooses the system bytes. The tutorial host starts at 100, so you see 101 and 102.
- If both messages don’t arrive in time, the run ends with
"outcome":"timeout"and exit code 4. The default limit is eight seconds, so raise--timeout-mswhen you type commands by hand. - With different session IDs, the equipment can’t process data messages. The tutorial examples use session ID 7.
Run a single role
Section titled “Run a single role”With --role and --address, the simulator runs only an equipment or only a host.
As an equipment, it waits on the address you give. It passes when a host establishes communication and then disconnects. This equipment registers status variable 1001 (EquipmentState, ASCII IDLE).
"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --role equipment --address 127.0.0.1:5001 --session-id 7 --timeout-ms 5000As a host, it connects to the equipment at the address, establishes communication and exits. Start the tutorial equipment (equipment passive 127.0.0.1:5006) first, then run:
"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --role host --address 127.0.0.1:5006 --session-id 7{"schema":1,"sequence":1,"scenario":"smoke","scope":"host","kind":"start","detail":"scenario-started","truncated":false}{"schema":1,"sequence":2,"scenario":"smoke","scope":"host","kind":"endpoint","local_address":"127.0.0.1:55752","truncated":false}{"schema":1,"sequence":3,"scenario":"smoke","scope":"host","kind":"request","detail":"begin-communication","truncated":false}{"schema":1,"sequence":4,"scenario":"smoke","scope":"host","kind":"summary","outcome":"passed","exit_code":0,...}The host role needs a nonzero port. Addresses must be a numeric IPv4 address with a port.
Replay a project scenario
Section titled “Replay a project scenario”Profiles and scenarios that you build in the GUI’s Scenarios & Profiles workspace are saved in the project file you write with File > Save Project As…. The headless simulator reads that file, starts an equipment session, applies the profile and runs the scenario steps.
Here’s a minimal project with the profile PROFILE-DEMO and the scenario count-cycle, which sets data variable 2001 to 7 and then clears it:
{ "kind": "pdcomm-simulator-project", "schema_version": 9, "equipment_alarms": null, "equipment_commands": null, "equipment_constants": null, "equipment_data_variables": null, "equipment_directories": null, "equipment_programs": null, "equipment_reports_events": null, "equipment_spooling": null, "profiles": [ { "profile_id": "PROFILE-DEMO", "description": "Tutorial profile", "variables": [ {"kind": "status", "id": 1001, "name": "State", "units": "", "format": "ascii", "min_count": 0, "max_count": 32, "initial_value": "IDLE"}, {"kind": "data", "id": 2001, "name": "GoodCount", "units": "count", "format": "u4", "min_count": 1, "max_count": 1, "initial_value": "0"} ] } ], "scenarios": [ { "scenario_id": "count-cycle", "description": "Set and clear GoodCount", "profile_id": "PROFILE-DEMO", "steps": [ {"kind": "set_data_variable", "variable_id": 2001, "value": "7"}, {"kind": "clear_data_variable", "variable_id": 2001, "value": ""} ] } ]}"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --project tutorial.pdcomm.json --scenario count-cycle --trace-output count-cycle.pdcomm-trace.json{"schema":1,"sequence":2,"scenario":"count-cycle","scope":"project","kind":"result","detail":"project-file-loaded","truncated":false}...{"schema":1,"sequence":8,"scenario":"count-cycle","scope":"project","kind":"result","detail":"project-step","step_index":0,"variable_id":2001,"action":"set-data-variable","passed":true,"truncated":false}{"schema":1,"sequence":9,"scenario":"count-cycle","scope":"project","kind":"result","detail":"project-step","step_index":1,"variable_id":2001,"action":"clear-data-variable","passed":true,"truncated":false}{"schema":1,"sequence":10,"scenario":"count-cycle","scope":"project","kind":"result","detail":"project-terminal","state":"passed","completed_steps":2,"total_steps":2,"truncated":false}{"schema":1,"sequence":11,"scenario":"count-cycle","scope":"project","kind":"summary","outcome":"passed","exit_code":0,...}Open the saved trace in the GUI with File > Open Trace…. To only validate the file, use --trace-input:
"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --trace-input count-cycle.pdcomm-trace.json{"schema":1,"sequence":1,"scenario":"trace-validation","scope":"trace","kind":"summary","outcome":"passed","exit_code":0,...}Reproduce write backpressure
Section titled “Reproduce write backpressure”transport-backpressure-recovery fills the send buffer on purpose and checks the recovery:
"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --scenario transport-backpressure-recovery --transport-fault-profile write_backpressureThe detail values go through transport-fault-armed, first-send-accepted, second-send-limit-exceeded, transport-fault-released, transport-recovered and retry-send-accepted, and the run ends with "outcome":"passed". The second send is rejected at the limit; after the fault is released, the same request goes through. In your own program, when you get Backpressure, process the received events first and retry a bounded number of times.
Run it in CI
Section titled “Run it in CI”Check the exit code, and keep the output so you can see why a run failed:
set -emkdir -p resultsfor scenario in smoke status-round-trip data-report-round-trip directory-round-trip; do "$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --scenario "$scenario" > "results/$scenario.jsonl"done