Skip to content

Protocol testing

Automated send/expect test scripts for binary serial protocols. Each step sends data, waits for a response, and reports PASS/FAIL.

For interactive sending and CRC commands, see Serial Tools.

Protocol test scripts

Create .pro files in the per-config proto/ folder with send/expect sequences:

# example.pro
@timeout 1000ms
@frame_gap 50ms

label: Read registers
send: 01 03 00 00 00 0A C5 CD
expect: 01 03 14 ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **

label: Write register
send: 01 06 00 01 00 03 98 0B
expect: 01 06 00 01 00 03 98 0B
timeout: 500ms

# Text protocols work too
label: AT query
send: "AT+VERSION?\r"
expect: "V1." ** ** "\r"

Run with /proto.run example.pro. Each step reports PASS/FAIL.

Protocol test results

Script directives

  • @timeout <duration>: default expect timeout (default 1000ms)
  • @frame_gap <duration>: silence gap to detect frame end (default 50ms)
  • @strip_ansi: strip ANSI escape sequences from responses before matching
  • label: <text>: name for the next step
  • send: <hex or "text">: transmit raw bytes (no line ending appended)
  • expect: <pattern>: wait for response and match (** = any byte)
  • timeout: <duration>: per-step timeout override
  • delay: <duration>: fixed sleep
  • flush: <duration>: wait for serial silence, then discard received bytes
  • cmd: <text>: send a plain text command with config line ending

Packet visualizers

The proto debug screen uses pluggable visualizers to decode packet bytes into named columns. Built-in visualizers (Hex, Text, Modbus) ship with termapy. Add your own by dropping a .py file into termapy_cfg/<config>/viz/.

Interactive proto debug screen

Multiple visualizers can be active at once via the checklist. Enable "Show viz string" to display the raw format spec above each table.

Selecting visualizers in .pro files:

Use viz in the script header to limit which visualizers appear in the dropdown. Use viz in a [[test]] section to force that visualizer for the test:

viz = ["Modbus"]          # header: only show Hex, Text, Modbus in dropdown

[[test]]
name = "Read registers"
viz = "Modbus"            # force Modbus view for this test
send = "01 03 00 00 00 01 84 0A"
expect = "01 03 02 00 07 F9 86"

Format spec language

Format specs decode raw bytes into named, typed fields. One line defines your entire packet layout. Used in protocol testing (.pro files), data capture (/cap.struct, /cap.hex), and the proto debug screen.

Syntax

Each field: Name:TypeByteRange. Fields separated by spaces.

"ID:H1 Temp:U2-3 Signed:I4-5 Status:H6"

Given the bytes 01 00 C8 FF FE 0A, this decodes to:

  ID     = 01      (byte 1 as hex)
  Temp   = 200     (bytes 2-3 as unsigned int, big-endian)
  Signed = -2      (bytes 4-5 as signed int, big-endian)
  Status = 0A      (byte 6 as hex)

In protocol tests, termapy decodes both expected and actual bytes, then shows per-column pass/fail:

Expected: 01 00 C8 FF FE 0A  ->  ID:01  Temp:200   Signed:-2   Status:0A
Actual:   01 00 C9 FF FE 0A  ->  ID:01  Temp:201   Signed:-2   Status:0A
                                  match  MISMATCH   match       match

Type reference

Code Meaning Example Output
H Hex bytes H1, H3-4 0A, 01FF
U Unsigned integer U1, U3-4 10, 256
I Signed integer I1, I3-4 -1, +127
S ASCII string S5-12 Hello...
F IEEE 754 float F1-4 3.14
B Bit field B1.3, B1-2.7-9 1, 5
_ Padding (hidden) _:_3-4 (skipped)
crc* CRC verify CRC:crc16-modbus pass/fail

Integers support 1, 2, 3, 4, and 8 byte widths. Floats are 4-byte (F32) or 8-byte (F64). Byte indexing is 1-based. H7-* = wildcard to end of packet.

Endianness

Byte order in the spec IS the endianness - no separate flags needed:

  • U2-3 = bytes 2 then 3 = big-endian: 00 C8 = 200
  • U3-2 = bytes 3 then 2 = little-endian: C8 00 = 51200
  • I4-5 = big-endian signed: FF FE = -2
  • I5-4 = little-endian signed: FE FF = -257

You read the spec the same way you read the protocol datasheet. Modbus devices are big-endian (U2-3), x86-based devices are little-endian (U3-2).

CRC fields don't take byte indices -- they use the algorithm's natural wire order by default (low byte first for refout=True algorithms like Modbus, USB, CRC-32; high byte first for refout=False like XMODEM, CCITT-FALSE). Add _le or _be only when your protocol wires the CRC opposite to that natural order: CRC:crc16-modbus_be would mean "Modbus CRC, but the bytes go high-first on this particular wire."

Bit fields

Extract individual bits or bit ranges from bytes:

  • B4.0 - bit 0 of byte 4 (LSB)
  • B4.7 - bit 7 of byte 4 (MSB)
  • B4.5-7 - bits 5-7 of byte 4 (3-bit value)
  • B4-5.0-15 - 16-bit range across bytes 4-5

Example: a status byte where each bit means something:

"Temp:U1-2 Humid:U3 MotorOn:B4.0 AlarmHi:B4.1 AlarmLo:B4.2 Mode:B4.5-7"

Real-world examples

Modbus RTU response (read 2 holding registers):

"Slave:H1 Func:H2 Len:U3 Reg0:U4-5 Reg1:U6-7 CRC:crc16-modbus"

Decodes 01 03 04 00 C8 01 F4 XX XX to Slave:01 Func:03 Len:4 Reg0:200 Reg1:500 CRC:pass

GPS binary packet (mixed types):

"Sync:H1-2 MsgID:U3 Lat:F4-7 Lon:F8-11 Alt:F12-15 Sats:U16 _:_17 CRC:crc8-maxim"

Sensor with string ID and padding:

"Serial:S1-8 _:_9-10 Temp:U11-12 Humid:U13-14 CRC:crc16-xmodem"