Skip to content
Start free evaluation

Observe addresses and frames

This tutorial needs no network or serial port. You open one headless simulator, run PDUs against its data model (execute) and feed raw bytes to its frame decoders (fault). Every result reports source as local or synthetic: nothing goes on a wire.

  • Convert a device-manual address such as 40108 to a PDU address, then write and read it.
  • See how bit tables are packed and what an exception response looks like.
  • Decode the same request as TCP, RTU and ASCII frames, and reproduce CRC, LRC and RTU character-gap errors.
  • The PDComm Modbus SDK, with MODBUS_SDK pointing at it as in the quickstart
  • One terminal
터미널 창
"$MODBUS_SDK/bin/pdcomm-modbus-headless"

The headless simulator reads one JSON command per line from standard input and prints one reply line per command. It prints nothing until you send a command. A new session is a disconnected TCP server configuration with 1024 zeroed entries in each table.

Check what the build supports first:

{"action":"capabilities"}
{"ok":true,"result":{"ascii":true,"canopen":false,"diagnostic_logging":true,"diagnostics":[0,1,2,3,4,10,11,12,13,14,15,16,17,18,20],"functions":[1,2,3,4,5,6,7,8,11,12,15,16,17,20,21,22,23,24,43],"logging_schema_version":1,"mei13":"opaque_extension","project_version":1,"protocol":"modbus","rtu":true,"security":true,"tcp":true}}

If your SDK reports rtu or ascii as false, those transports fail with Transport is excluded in steps 6 and 7.

Suppose the device manual lists “40108: temperature setpoint”. The prefix 4 means holding registers, and 108 minus 1 is PDU address 107 (0x006B). Write 1234 (0x04D2) and read it back:

{"action":"execute","pdu":"06006B04D2"}
{"action":"execute","pdu":"03006B0001"}
{"ok":true,"result":{"response":[6,0,107,4,210],"source":"local","status":"success"}}
{"ok":true,"result":{"response":[3,2,4,210],"source":"local","status":"success"}}

Responses are decimal bytes: [3,2,4,210] is function 3, a byte count of 2 and the value 0x04D2 (1234).

Write 0xA5 to coils 0–7 in one request, then read the eight coils:

{"action":"execute","pdu":"0F0000000801A5"}
{"action":"execute","pdu":"0100000008"}
{"ok":true,"result":{"response":[15,0,0,0,8],"source":"local","status":"success"}}
{"ok":true,"result":{"response":[1,1,165],"source":"local","status":"success"}}

The least significant bit of 165 (0xA5) is coil 0, so coils 0, 2, 5 and 7 are on.

A client can’t write discrete inputs. In the simulator, data_set stands in for a changing device input:

{"action":"data_set","table":"discrete_inputs","address":0,"values":[1,0,1]}
{"action":"execute","pdu":"0200000003"}
{"ok":true,"result":{"written":3}}
{"ok":true,"result":{"response":[2,1,5],"source":"local","status":"success"}}

Read address 65535, which isn’t in the model:

{"action":"execute","pdu":"03FFFF0001"}
{"ok":true,"result":{"response":[131,2],"source":"local","status":"exception"}}

131 is 0x83 (function 3 plus 0x80), and 2 is Illegal Data Address. The command itself succeeded, so ok is true.

The fault command feeds raw bytes to a frame decoder and shows the result, without any I/O:

{"action":"fault","transport":"tcp","bytes":"000100000006010300000001"}
{"ok":true,"result":{"errors":[],"frames":[{"pdu":[3,0,0,0,1],"transaction":1,"unit":1}],"source":"synthetic"}}

The decoder removed the MBAP header (0001 0000 0006 01) and returned transaction 1, unit 1 and the PDU 03 0000 0001. Drop the last PDU byte and the frame never completes:

{"action":"fault","transport":"tcp","bytes":"0001000000060103000000"}
{"ok":true,"result":{"errors":["Incomplete"],"frames":[],"source":"synthetic"}}

Send the PDU 03 0000 000A as RTU. With the correct CRC C5 CD you get a frame; change one bit and you get a Checksum error:

{"action":"fault","transport":"rtu","bytes":"01030000000AC5CD"}
{"action":"fault","transport":"rtu","bytes":"01030000000AC5CC"}
{"ok":true,"result":{"errors":[],"frames":[{"pdu":[3,0,0,0,10],"unit":1}],"source":"synthetic"}}
{"ok":true,"result":{"errors":["Checksum"],"frames":[],"source":"synthetic"}}

For ASCII, pass the byte values of the characters :01030000000AF2 plus CR LF in hex:

{"action":"fault","transport":"ascii","bytes":"3A30313033303030303030304146320D0A"}
{"action":"fault","transport":"ascii","bytes":"3A30313033303030303030304146330D0A"}
{"action":"fault","transport":"ascii","bytes":"3A30313033303030303030306166320D0A"}
{"ok":true,"result":{"errors":[],"frames":[{"pdu":[3,0,0,0,10],"unit":1}],"source":"synthetic"}}
{"ok":true,"result":{"errors":["Checksum"],"frames":[],"source":"synthetic"}}
{"ok":true,"result":{"errors":["Encoding"],"frames":[],"source":"synthetic"}}
  • The second frame has its LRC changed from F2 to F3, so it fails with Checksum.
  • The third has A replaced by lowercase a (0x61), so it fails with Encoding: ASCII mode accepts uppercase hex digits only.

gap_us sets a virtual gap between the bytes fed to the RTU decoder; the default is 100 µs. At the configuration’s default 19200 bit/s and 8E1, t1.5 is 860 µs and t3.5 is 2006 µs.

{"action":"fault","transport":"rtu","bytes":"01030000000AC5CD","gap_us":1000}
{"action":"fault","transport":"rtu","bytes":"01030000000AC5CD","gap_us":3000}
{"ok":true,"result":{"errors":["Timing"],"frames":[],"source":"synthetic"}}
{"ok":true,"result":{"errors":["Length","Length","Length","Length","Length","Length","Length","Length"],"frames":[],"source":"synthetic"}}
  • 1000 µs exceeds t1.5, so the frame is dropped for a gap inside the frame (Timing).
  • 3000 µs exceeds t3.5, so every byte ends a frame. One-byte frames are too short, which gives eight Length errors.
{"action":"trace"}
{"action":"trace_clear"}

trace lists the execute commands as entries with source local; fault isn’t recorded. To finish, enter {"action":"shutdown"} or close the input (Ctrl+D on macOS and Linux, Ctrl+Z then Enter on Windows).

Output Cause and fix
{"error":"Invalid hex length","ok":false} The hex string has an odd number of characters. Write two characters per byte.
{"error":"Invalid hex","ok":false} The string contains a character that isn’t hexadecimal.
{"error":"key must be a string at line 1 column 2","ok":false} The line isn’t valid JSON. Put one complete JSON object on each line; a JSON object split over several lines is parsed line by line.
{"error":"Unknown service action","ok":false} The action name is wrong. See the JSON command reference.

If any command failed, the headless simulator exits with code 1 when it ends. It keeps processing the commands after an error.