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. A script is
TOML: a header of defaults, then one [[test]] table per send/expect
step. The Proto picker lists them newest first with size and age, and
/proto.rename renames one.
# example.pro
name = "Modbus smoke test"
timeout = "1000ms" # default expect timeout
frame_gap = "50ms" # silence that ends a frame
setup = ["/term.hex on"] # commands run before the first test
teardown = ["/term.hex off"]
[[test]]
name = "Read registers"
send = "01 03 00 00 00 0A C5 CD"
expect = "01 03 14 ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **"
[[test]]
name = "Write register"
send = "01 06 00 01 00 03 98 0B"
expect = "01 06 00 01 00 03 98 0B"
timeout = "500ms" # per-test override
# Text protocols work too: quote the text, \r \n escapes are honored
[[test]]
name = "AT query"
send = '"AT+VERSION?\r"'
expect = '"V1." ** ** "\r"'
send_fmt = "Title:AT_Command Command:S1-*" # optional inline format specs
expect_fmt = "Title:AT_Response Response:S1-*" # (see the format spec language below)
Run with /proto.run example.pro. Each test reports PASS/FAIL. Escape
stops a run after the current test (the script-level teardown is then
skipped). /proto.run example.pro --json returns the results document
described under Headless runs instead of the prose,
which is also what an MCP client receives.
Script keys¶
Header (defaults for every test):
name: script display nametimeout: default expect timeout (default 1000ms)frame_gap: silence gap that ends a frame (default 50ms)strip_ansi: strip ANSI escape sequences from responses before matching (a TOML boolean,true/false; a quoted string is refused at load)quiet: hide setup/teardown output (a TOML boolean too)setup/teardown: lists of commands run before the first test / after the lastviz: visualizer names allowed in the debug screen's dropdown (empty = all)send_fmt/expect_fmt: default inline format specs for TX / RX datajson_file: after/proto.run, write the results document to this file intest/beside the script. A filename template:{name}(the config),{proto_name}(the script's stem),{datetime},{date},{time}. The debug screen andtermapy --protowrite one whether or not the key is set; there it overrides theproto_results_templateconfig value
Per [[test]]:
name: test namesend: hex bytes (01 03 ...) or quoted text ('"AT\r"'); nothing is appendedexpect: pattern to match (**= any byte); hex or quoted texttimeout: per-test overridesetup/teardown: commands around this one testviz: force one visualizer for this test in the debug screensend_fmt/expect_fmt: inline format specs for this test
Legacy flat format¶
Older scripts use a colon-keyed flat format: header directives
@name <text>, @timeout <dur>, @frame_gap <dur>, @strip_ansi {on|off}
(bare is on), @quiet, @loud, then label:, send:, expect:, timeout:,
delay:, flush:, cmd: steps. It is still parsed when a file is not
valid TOML, but JSON result output (json_file, --json, --proto)
needs the TOML form.
Headless runs¶
termapy --proto smoke.pro my_device # no TUI; exit 0 when every test passed
termapy --proto smoke.pro --demo # against the demo device
--proto runs one TOML script against the config's port and exits:
0 when every test passed, 1 when any failed, 2 on a config, script, or
port error. One summary line goes to stdout; the results document goes
to proto/test/ in the config's data folder, named by the
proto_results_template config value unless the script sets
json_file. The document has four keys: meta (script, config, port,
baud rate, encoding, timestamp), summary (total, passed,
failed, elapsed_ms), tests (one record per test that ran:
index, name, passed, elapsed_ms, send_hex, expect_hex,
actual_hex, the text forms, any send_fmt/expect_fmt, and error
on a timeout), and source (the script text). The same document is
what /proto.run --json returns and what the debug screen's JSON
checkbox writes.
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/.
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 offer Modbus in the 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.
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 |
h |
Hex, per byte | h3-4 |
01 FF |
U |
Unsigned integer | U1, U3-4 |
10, 256 |
I |
Signed integer | I1, I3-4 |
-1, +127 |
S |
ASCII string | S1-5 |
Hello |
F |
IEEE 754 float | F1-4 |
3.14 |
B |
Bit field | B1.3, B1-2.7-9 |
1, 5 |
b |
Bit field, bits | b1.0-7 |
00000110 |
_ |
Padding (hidden) | _3-4 |
(skipped) |
crc* |
CRC verify | crc16-modbus |
pass/fail |
Lowercase shows the pieces: h spaces each byte (and a protocol diff marks
the mismatched bytes individually), b prints the bits as a binary string.
Only h and b have lowercase forms.
Integers support 1, 2, 3, 4, and 8 byte widths. Floats are 4-byte (F32) or
8-byte (F64). Byte indexing is 1-based: U0 is an error, not the first
byte. H7-* = wildcard to end of packet. A column that reaches past the
data shows ? rather than a value.
Endianness¶
Byte order in the spec IS the endianness - no separate flags needed:
U2-3= bytes 2 then 3 = big-endian:00 C8= 200U3-2= bytes 3 then 2 = little-endian:C8 00= 51200I4-5= big-endian signed:FF FE= -2I5-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."
The CRC covers every byte before it. When a protocol leaves bytes out (a
sync byte, say), give the range: CRC:crc16-modbus(2-7) computes over
bytes 2-7, 1-based and inclusive. In a protocol diff the CRC column is
match when the CRC the packet carries verifies over that range (and
agrees with the expect pattern wherever the pattern spells the CRC bytes
out instead of wildcarding them), mismatch otherwise.
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-5B.15- no byte range: bit 15 of a register word. Only a reader that knows the word binds it (the memory commands); in a packet spec it is an error
b takes the same references but prints the bits. An ascending range reads
like a datasheet, bit 7 leftmost (b4.0-7 on 06 gives 00000110); a
descending range reverses it, bit 0 leftmost (b4.7-0 gives 01100000).
Example: a status byte where each bit means something:
Real-world examples¶
Modbus RTU response (read 2 holding registers):
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):
Sensor with string ID and padding: