The Comm Script system is a communication-oriented script interpreter built to drive any ICommDriver-derived transport (serial port, TCP socket, USB HID, etc.) through a terse, line-oriented scripting language. Each script line describes a directed data exchange — send, receive, or a timed delay — with rich data-type annotations for both sides of the exchange.
Unlike the Core Script system, Comm scripts have no plugin machinery, no IF/GOTO flow control, and no variable macros. They focus exclusively on protocol-level send/receive sequencing with precise data-type validation.
┌──────────────────────────────────────────────────────────────────────────┐
│ CommScriptClient<TDriver> │
│ Template facade: wires the pipeline to a concrete driver instance, │
│ exposes execute(). │
└────────────────────────────────┬─────────────────────────────────────────┘
│ owns
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ CommScriptRunner<CommCommandsType, TDriver> │
│ Extends ScriptRunner; holds a typed reference to the comm interpreter │
│ for potential post-run access (e.g. getLastReceived()). │
│ Inherits: read → validate → interpret │
└───────┬──────────────────────┬──────────────────────────────┬────────────┘
│ │ │
owns │ owns │ owns │
▼ ▼ ▼
┌──────────────┐ ┌───────────────────────┐ ┌──────────────────────────┐
│ ScriptReader │ │ CommScriptValidator │ │ CommScriptInterpreter │
│ (shared) │ │ │ │ <TDriver> │
│ │ │ Line-by-line: │ │ │
│ Same reader │ │ • expand $MACROs │ │ Iterates vCommands[], │
│ as Core: │ │ • if MACRO := style, │ │ dispatches each to: │
│ comments, │ │ store in mapMacros │ │ │
│ blank lines,│ │ • else validate as │ │ CommScriptCommand- │
│ block │ │ CommCommand via │ │ Interpreter<TDriver> │
│ comments │ │ CommScriptCommand- │ │ │
│ │ │ Validator │ │ Applies inter-command │
└──────────────┘ │ • push to vCommands │ │ delay (m_szDelay) │
└───────────┬───────────┘ └──────────────┬───────────┘
│ │
▼ ▼
┌───────────────────────┐ ┌──────────────────────────┐
│ CommScriptCommand- │ │ CommScriptCommand- │
│ Validator │ │ Interpreter<TDriver> │
│ │ │ │
│ ItemParser: │ │ executeSend() │
│ 1. parseDirection() │ │ convertToData() │
│ 2. splitFields() │ │ driver->tout_write() │
│ 3. getTokenType() ×2 │ │ │
│ 4. evaluateAndValidate│ │ executeReceive() │
│ │ │ receiveAndMatchRegex() │
│ Returns CommCommand │ │ receiveUntilToken() │
│ {direction, │ │ receiveExactSize() │
│ values{v1,v2}, │ │ receiveUntilDelimiter() │
│ tokens{t1,t2}} │ │ receiveAndCompare() │
└───────────────────────┘ │ receiveToFile() │
│ sendFile() │
│ │
│ driver->tout_read() │
│ driver->tout_write() │
└──────────────────────────┘
Comm script file (.txt)
│
▼
ScriptReader::readScript() ← shared with Core Script
─────────────────────────────────────
• Strips #-comments, blank lines, --- ... !-- blocks
• Returns vector<string> of clean lines
│
▼
CommScriptValidator::validateScript()
─────────────────────────────────────
For each line:
1. replaceMacros($NAME) ← expand already-defined constant macros
2. if matches NAME := value → store in mapMacros{}, continue
3. else CommScriptCommandValidator::validateCommand()
→ ItemParser::parse()
a. parseDirection() → SEND_RECV / RECV_SEND / DELAY
b. splitFields() → field1, field2 (pipe-aware quoting)
c. getTokenType(f1) → CommCommandTokenType (+ validation)
d. getTokenType(f2) → CommCommandTokenType (+ validation)
e. evaluateAndValidate() → semantic direction/type rules
→ CommCommand{ direction, values{f1,f2}, tokens{t1,t2} }
→ push to vCommands[]
│
│ CommCommandsType { vCommands[], mapMacros{} }
▼
CommScriptInterpreter::interpretScript()
─────────────────────────────────────────
For each CommCommand in vCommands[]:
CommScriptCommandInterpreter::interpretCommand(command)
┌─ SEND_RECV: executeSend(values.first, tokens.first)
│ executeReceive(values.second, tokens.second)
├─ RECV_SEND: executeReceive(values.first, tokens.first)
│ executeReceive(values.second, tokens.second)
└─ DELAY: delay_us / delay_ms / delay_seconds
delay_ms(m_szDelay) ← configurable inter-command pause
CommCommandsType
├── mapMacros : unordered_map<string,string>
│ "MACRO_NAME" → "literal value" (constant macros, $-substituted)
│
└── vCommands : vector<CommCommand>
CommCommand {
direction : CommCommandDirection { SEND_RECV | RECV_SEND | DELAY | INVALID }
values : pair<string,string> { field1_value, field2_value }
tokens : pair<CommCommandTokenType, CommCommandTokenType>
}
Every non-blank, non-comment line is one of three forms:
MACRO_NAME := literal value
Identical in syntax to Core Script constant macros. The value is substituted
into subsequent lines via $MACRO_NAME at validation time.
<direction> <expr1> [| <expr2>]
| Symbol | Direction | Meaning |
|---|---|---|
> |
SEND_RECV | Send expr1, then optionally receive/validate expr2 |
< |
RECV_SEND | Receive/validate expr1, then optionally send expr2 |
- The pipe
|separator is mandatory only when both sides are present. - A pipe inside a quoted expression (
"...") is preserved and not treated as a separator.
! <value> <unit>
| Unit | Meaning |
|---|---|
us |
Microseconds |
ms |
Milliseconds |
s |
Seconds |
Each expression expr1 or expr2 is annotated with an optional prefix decorator that determines how the value is encoded/decoded. The decorator is a single uppercase letter immediately followed by "...".
| Decorator | Token Type | Description | Valid for |
|---|---|---|---|
| (none — quotes) | STRING_DELIMITED |
Quoted literal string: "hello world" |
Send & Receive |
"" |
STRING_DELIMITED_EMPTY |
Empty quoted string | Send only |
| (none — no quotes) | STRING_RAW |
Unquoted plain token: hello |
Send & Receive |
H"…" |
HEXSTREAM |
Hex byte stream: H"4A6F686E00" |
Send & Receive |
R"…" |
REGEX |
Regex pattern for matching received data: R"OK.*\r\n" |
Receive only |
F"…" |
FILENAME |
File path (must exist and be non-empty): F"firmware.bin" |
Send & Receive |
T"…" |
TOKEN_STRING |
Receive until string token found: T"OK" |
Receive only |
X"…" |
TOKEN_HEXSTREAM |
Receive until hex-byte sequence found: X"CAFE00FF" |
Receive only |
L"…" |
LINE |
Read/compare a newline-terminated line: L"OK" |
Send & Receive |
S"…" |
SIZEOF |
Receive exactly N bytes: S"256" |
Receive only |
Note on
F"…"file format:
- Send file:
F"path/file.bin"orF"path/file.bin,chunksize"(default chunk = 1024 bytes)- Receive to file:
F"out.bin"orF"out.bin,expected_size"orF"out.bin,expected_size,chunksize"
The evaluateAndValidate() step enforces direction-aware rules:
| Rule | Detail |
|---|---|
Cannot send TOKEN_STRING, TOKEN_HEXSTREAM, SIZEOF, REGEX, EMPTY |
These are receive-only concepts |
Cannot receive EMPTY or STRING_DELIMITED_EMPTY |
Receiving nothing is not meaningful |
Both expressions cannot be EMPTY |
At least one side must carry data |
DELAY value must be a numeric string convertible to size_t |
Non-numeric values are rejected at validation time |
File type (FILENAME) validated at parse time |
File must exist on disk and be non-empty |
Hex streams (HEXSTREAM, TOKEN_HEXSTREAM) validated for hex format |
Non-hex characters are rejected |
Size value (SIZEOF) validated as a positive numeric |
Must parse as a valid size_t |
Regex patterns (REGEX) must be non-empty |
Empty pattern is rejected |
# Line comment — entire line ignored
---
Block comment
All lines until closing marker are skipped
!--
> "hello" # inline comment after command
PROMPT := login:
BAUD := 115200
CMD_TIMEOUT := 5000
FW_PATH := /opt/fw/image.bin
Macros are expanded with $ prefix:
> "$PROMPT"
> "hello\r\n" # send delimited string
> H"0D0A" # send raw bytes (CRLF)
> raw_command # send unquoted raw string
> L"AT" # send line (string + newline)
> F"firmware.bin" # send file in 1024-byte chunks
> F"firmware.bin,512" # send file in 512-byte chunks
> "AT\r\n" | T"OK" # send string, wait for token
> H"DEADBEEF" | H"CAFE0001" # send hex, expect exact hex response
> "GET / HTTP/1.0" | R"HTTP/1\.[01].*" # send string, match regex
> "READ_TEMP\r\n" | L"25.3" # send, receive matching line
> "DUMP\r\n" | S"128" # send, receive exactly 128 bytes
> F"image.bin" | T"DONE" # send file, wait for completion token
< T"login:" # wait for string token
< X"FF00CAFE" # wait for hex-byte sequence
< R"[0-9]{1,3}\.[0-9]{1,3}" # receive and match regex
< "expected response" # receive and compare exact string
< H"4F4B0D0A" # receive and compare exact bytes
< S"64" # receive exactly 64 bytes
< L"READY" # receive until newline, compare to "READY"
< F"received.bin,4096" # receive 4096 bytes, write to file
< T"login:" | "admin\r\n" # wait for prompt, send username
< T"Password:" | "secret\r\n" # wait for password prompt, send it
< "OK" | "NEXT\r\n" # receive exact confirmation, send next command
< L"READY" | H"01020304" # wait for line, send binary header
! 100 ms # wait 100 milliseconds
! 500 us # wait 500 microseconds
! 2 s # wait 2 seconds
| Token Type | Driver Read Mode | Behavior |
|---|---|---|
STRING_DELIMITED, STRING_RAW, HEXSTREAM |
Exact |
Read up to maxRecvSize bytes, compare with expected |
REGEX |
Exact |
Read up to maxRecvSize bytes, apply std::regex_match |
TOKEN_STRING |
UntilToken |
Read until the exact string sequence is found in the stream |
TOKEN_HEXSTREAM |
UntilToken |
Read until the exact byte sequence is found in the stream |
LINE |
UntilDelimiter('\n') |
Read until newline; optionally compare content |
SIZEOF |
Exact |
Read exactly N bytes; verify count matches |
FILENAME |
Exact (chunked) |
Write received chunks to a file; stop at expected size |
# =============================================================================
# Example: Embedded device boot & firmware update via serial
# =============================================================================
/*
Scenario:
1. Device boots and prints a login prompt.
2. We authenticate, then check running firmware version.
3. If version is old, we send the new firmware binary.
4. Confirm flash success and reboot.
*/
# Constant macros
USER := admin
PASS := secret123
FW_BIN := /opt/firmware/v3.1.bin
# ---------------------------------------------------------------------------
# Phase 1: Login sequence
# ---------------------------------------------------------------------------
# Wait for device boot banner (regex: any version string)
< R".*Boot v[0-9]+\.[0-9]+.*"
# Wait for login prompt, send credentials
< T"login:" | "$USER\r\n"
< T"Password:" | "$PASS\r\n"
# Expect the shell prompt (exact string)
< "$ "
# ---------------------------------------------------------------------------
# Phase 2: Check current firmware version
# ---------------------------------------------------------------------------
# Send version query command
> "fw_version\r\n" | L"v2.9.0"
# Small pause before next command
! 200 ms
# ---------------------------------------------------------------------------
# Phase 3: Initiate firmware update
# ---------------------------------------------------------------------------
# Send the update start command
> "fw_update start\r\n" | T"READY"
# Delay for device to prepare its flash buffer
! 500 ms
# Transfer the firmware binary (default 1024-byte chunks)
> F"$FW_BIN" | T"DONE"
# ---------------------------------------------------------------------------
# Phase 4: Verify and reboot
# ---------------------------------------------------------------------------
! 1 s
# Query version after flashing — expect the new version
> "fw_version\r\n" | L"v3.1.0"
# Send reboot command, device will stop responding
> "reboot\r\n"
# Wait for device to come back up (boot banner with new version)
! 3 s
< R".*Boot v3\.1\.0.*"
# ---------------------------------------------------------------------------
# Phase 5: Binary data exchange example
# ---------------------------------------------------------------------------
# Send a binary probe packet and receive exact 8-byte ACK
> H"AA5501000000FF" | H"AA5500000000FF"
# Receive a variable-length certificate (exact byte count)
> "get_cert\r\n" | S"512"
# Save a full memory dump (receive 65536 bytes) to file
> "mem_dump\r\n" | F"/tmp/memdump.bin,65536"
# Send a pre-built binary image file in 256-byte chunks, wait for token
> F"/tmp/payload.bin,256" | X"DEADBEEF"
CommScriptInterpreter (and by extension CommScriptClient) accepts the following parameters at construction:
| Parameter | Default | Description |
|---|---|---|
shpDriver |
— | Shared pointer to the concrete ICommDriver implementation |
szMaxRecvSize |
4096 | Maximum receive buffer size in bytes |
u32DefaultTimeout |
5000 | Default I/O timeout in milliseconds |
szDelay |
0 | Inter-command delay in milliseconds |
The Comm Script system is fully driver-agnostic. The concrete driver must implement:
// Non-blocking write with timeout
WriteResult tout_write(uint32_t timeout_ms, std::span<const uint8_t> data);
// Flexible read with timeout and read options
ReadResult tout_read(uint32_t timeout_ms,
std::span<uint8_t> buffer,
ReadOptions options);
bool is_open() const;ReadOptions selects the read strategy:
ReadMode::Exact— fill buffer exactlyReadMode::UntilToken— search for a byte pattern; setsfound_terminatorReadMode::UntilDelimiter— read until a single-byte delimiter (e.g.\n)
| Component | File | Role |
|---|---|---|
CommScriptClient<TDriver> |
uCommScriptClient.hpp |
Facade: wires pipeline to a driver, exposes execute() |
CommScriptRunner<TScriptEntries, TDriver> |
uCommScriptRunner.hpp |
Extends ScriptRunner; holds typed interpreter reference |
ScriptReader |
uScriptReader.hpp |
Shared file reader (same as Core Script) |
CommScriptValidator |
uCommScriptValidator.hpp |
Validates & builds CommCommandsType IR |
CommScriptCommandValidator |
uCommScriptCommandValidator.hpp |
Parses direction, splits fields, classifies token types, semantic rules |
CommScriptInterpreter<TDriver> |
uCommScriptInterpreter.hpp |
Iterates commands, applies inter-command delay |
CommScriptCommandInterpreter<TDriver> |
uCommScriptCommandInterpreter.hpp |
Executes individual send/receive/delay commands against driver |
CommScriptDataTypes |
uCommScriptDataTypes.hpp |
CommCommand, CommCommandDirection, CommCommandTokenType enums & helpers |