TrIML – Trace Interpreter Markup Language
TrIML is the XML language used in Trace to describe protocol interpreters. An interpreter reads a byte stream (e.g. from a serial interface), recognizes frames of a particular protocol within it, and returns them as structured, named fields – including plain-text labels for values (enums), checksum validation, and nested structures.
The major advantage: a new protocol can be described purely declaratively in XML
– without a single line of C# code. The evaluation is handled by the shared
XmlMessageParser (engine), which interprets the XML definition at runtime.
Who needs this documentation? Anyone who wants to make their own protocol readable in Trace without touching the server code. The reference describes every XML element and attribute; the tutorial walks through building an interpreter using a concrete example.
What an interpreter does
The parser works as a recursive top-down matcher over the byte stream:
- Bytes are fed into the parser piece by piece.
- The parser tries to overlay the message structure described in the XML onto the stream (fields consume bytes, constants expect exact values, branches choose a path depending on the value read).
- If the structure matches completely, a match is produced – a recognized frame with a label and hierarchical field descriptions.
- If it does not match, the parser resets (backtracking) and tries the next possible frame start.
Frame-oriented vs. byte-oriented
In the root element, the type attribute sets the interpretation mode:
| Mode | Meaning |
|---|---|
FrameOriented |
The protocol has clearly delimited frames with start/end markers (e.g. STX … ETX). The most common case. |
ByteOriented |
There is no fixed frame delimitation; the stream is interpreted continuously. |
Tokens and decoders
The parser does not work directly on the raw bytes, but on a token stream.
By default – without a custom decoder – each byte becomes a NormalInput token
that carries its value. Fields such as <byte>, <word>, and literal
<const value="…"> consume exactly such NormalInput tokens. This is the normal
case and requires no decoder.
In addition, the engine knows special tokens that convey a meaning rather than just a value:
| Token | Meaning |
|---|---|
framestart |
Start of a frame (explicit re-sync point) |
frameend |
End of a frame |
specialvalue |
protocol-specific special character |
eof |
End of the input stream |
invalid |
invalid input character |
These special tokens do not arise on their own – they are produced from the
byte stream by a decoder. A <const token="frameend" /> matches such a
token; without a decoder that produces frameend tokens, it never applies.
This leads to two ways of handling frame boundaries:
As literal bytes – the simplest way, no decoder needed:
<const type="byte" value="0x02" name="STX" /> <!-- Start --> … <const type="byte" value="0x03" name="ETX" /> <!-- End -->Works immediately, even in the pure runtime XML interpreter. For getting started and for most protocols, this is the right choice.
As frame tokens via an
<esc-decoder>– when the stream may start in the middle of a frame and needs to re-synchronize cleanly, or when escape/masking logic is required. The decoder assigns token types to the boundary bytes; afterwards<const token="…">matches these tokens:<esc-decoder> <sequence input="0x02" output="0x02" tokentype="framestart" /> <sequence input="0x03" output="0x03" tokentype="frameend" /> </esc-decoder> … <const token="framestart" name="STX" /> <const token="frameend" name="ETX" />The
<esc-decoder>is purely declarative and likewise works without code (details in the element reference, section<esc-decoder>).
Rule of thumb: Start with literal bytes (way 1). Reach for frame tokens (way 2) only when you really need robust re-sync or escape logic.
The skeleton of a TrIML file
Every interpreter definition has the same outer frame:
<?xml version="1.0" encoding="utf-8"?>
<Trace-Interpreter-Definition>
<interpreter name="My-Interpreter" type="FrameOriented" />
<definition>
<!-- Optional: global value tables (enums) -->
<valuedescriptor type="byte" name="type-descriptor">
<descriptor value="0x01" name="Status" label="Status message" />
<descriptor value="0x02" name="Command" label="Command" />
</valuedescriptor>
<!-- Optional: reusable structs, decoders … -->
<!-- Required: at least one message -->
<message>
<const type="byte" value="0x02" name="STX" />
<byte name="Type" valuedescriptor="type-descriptor" />
<byte name="Length" />
<array sizeRef="Length" name="Payload" />
<const type="byte" value="0x03" name="ETX" />
<match label="Frame" />
</message>
</definition>
</Trace-Interpreter-Definition>
The two mandatory building blocks directly under the root:
<interpreter>– metadata: name and mode.<definition>– contains all global definitions (value tables, structs, decoders) and at least one<message>with the actual frame description.
How an interpreter is loaded
You work exclusively with the XML definition – there is nothing to compile and no DLL to build. Trace creates an interpreter instance from your XML at runtime, which you can assign to a port.
Two ways:
- Via the TraceUI (recommended): The Interpreter Assistant offers an XML editor with live preview. You write the definition, run it against the currently visible bytes, and immediately see which frames are recognized. Once it fits, the interpreter is created and can be assigned to a port.
- Via the REST API: The XML is passed in a request, and the server creates an ephemeral interpreter (living in the running process):
POST /api/interpreters/from-xml
{ "name": "MiniBus", "xml": "<Trace-Interpreter-Definition> … </Trace-Interpreter-Definition>" }
The name must match the interpreter/@name attribute in the XML.
Risk-free live test:
POST /api/interpreters/dryrunwith{ "xml": …, "bytes": [ … ] }parses the bytes with a fresh instance and returns the recognized matches – without changing the server state. This is exactly what the assistant uses for its preview, and it is well suited for automated tests of your sample vectors.
Further endpoints: GET /api/interpreters (list), GET /api/interpreters/{name}
(read back the XML), PUT /api/interpreters/{name} (replace the XML – the
running ports are hot-swapped), DELETE /api/interpreters/{name}.
Purely declarative: Every construct described in this reference works in the runtime XML interpreter without any code at all – fields,
switch,group,unify,lookahead, value tables, the built-incalcfunctions (ByteSum,ByteXor,LRC,WordSum, parameterisedCRC), and the full<decoder>, including modes and the<frame>window framing for protocols without delimiters.Exactly one path still requires registered C# code and is therefore not available in the pure XML workflow:
calc function="…"and a<decoder name="X"/>without children. Both are legacy; new definitions do not need them.
Next steps
- Tutorial – build a small interpreter from scratch.
- Element reference – every tag and attribute in detail.
- Patterns & recipes – checksums, variable length, bit fields, ASCII numbers.
- Examples – complete definitions for public protocols (Modbus RTU, NMEA 0183).
Tutorial: An Interpreter in 7 Steps
We will build an interpreter step by step for a small, made-up protocol – a typical status telegram of the kind found in building or plant automation. Each step introduces exactly one new TrIML concept.
Our example protocol "MiniBus":
STX Type Addr(2 B) Length Payload(Length B) Checksum ETX
0x02 .. .. .. .. ... .. 0x03
STX=0x02,ETX=0x03delimit the telegram.Typeis a byte with a fixed meaning (Status / Command / Error).Addris a 16-bit address (big-endian).Lengthspecifies the number of payload bytes that follow.Checksumis the XOR sum over Type..Payload.
Step 1 – The skeleton
Every definition begins with the root, <interpreter> and <definition> along with a
<message>:
<?xml version="1.0" encoding="utf-8"?>
<Trace-Interpreter-Definition>
<interpreter name="MiniBus" type="FrameOriented" />
<definition>
<message>
<!-- to follow -->
</message>
</definition>
</Trace-Interpreter-Definition>
Step 2 – Detecting the frame (const)
The telegram begins with STX (0x02) and ends with ETX (0x03). Both are
fixed bytes that we expect as literal constants:
<message>
<const type="byte" value="0x02" name="STX" />
<!-- content follows -->
<const type="byte" value="0x03" name="ETX" />
</message>
A literal <const value="…"> matches exactly this byte. That is enough for
telegram detection and works without a decoder.
Optional: If you need explicit re-sync (the stream may start in the middle of a telegram), you can turn STX/ETX into
framestart/frameendtokens via an<esc-decoder>and match on them with<const token="…">– see Tokens and Decoders.
Step 3 – Reading fixed fields (byte, word, bigendian)
Between the frame markers come the data fields. A single byte is read by <byte>,
a 16-bit word by <word>. Since the address is transmitted big-endian, we set
bigendian="yes":
<const type="byte" value="0x02" name="STX" />
<byte name="Type" />
<word name="Address" bigendian="yes" format="0x{0:X4}" />
<const type="byte" value="0x03" name="ETX" />
The format attribute determines how the value is displayed in the UI – here as
a 4-digit hex value (format is a .NET String.Format pattern, and the value
lands on {0}).
Step 4 – Naming values (valuedescriptor)
Type is an enumeration value. Instead of a bare number, we want plain text.
To do this, we define a value table at the top of the <definition> and reference
it on the field:
<definition>
<valuedescriptor type="byte" name="type-descriptor">
<descriptor value="0x01" name="Status" label="Status message" />
<descriptor value="0x02" name="Command" label="Command" />
<descriptor value="0x03" name="Error" label="Error message" />
</valuedescriptor>
<message>
<const type="byte" value="0x02" name="STX" />
<byte name="Type" valuedescriptor="type-descriptor" />
<word name="Address" bigendian="yes" format="0x{0:X4}" />
...
When the parser reads the value 0x02 for Type, the UI displays "Command".
Step 5 – Variable length (sizeRef)
The Length byte determines how many payload bytes follow. That is exactly what
sizeRef is for: an <array> whose size comes at runtime from a previously
read field:
<byte name="Length" />
<array sizeRef="Length" name="Payload" />
sizeRef="Length" references the field by name. (If the length were fixed,
size="8" would be written directly there instead.)
Step 6 – Computing and validating the checksum (calc + validate)
The checksum is the XOR sum over the bytes from Type to Payload. We
compute it with <calc> and then compare it against the transmitted
byte. from/to are byte offsets within the telegram (0 = first byte after the
frame start; the engine counts from the first consumed content byte):
<byte name="Length" />
<array sizeRef="Length" name="Payload" />
<!-- XOR over Type(0) .. end of payload -->
<calc name="ExpectedChecksum" type="ByteXor" from="0" to="-1" resultType="byte" />
<!-- expects the next byte == computed value; on mismatch a frame error -->
<const ref="ExpectedChecksum" validate="true" name="Checksum" />
validate="true" is the decisive part here: if the checksum does not match, the
telegram is marked as faulty (visible in the stream), but the parse process
continues. Without validate, a mismatch would discard the path entirely.
Note on
to: Depending on the engine version, end bounds can be specified absolutely or relatively (negative = from the current end). When in doubt, check the result against real data with thedryrunendpoint.
Step 7 – Shaping the output (match)
Finally, <match> produces the visible telegram label. Static text and
field values are stated together in the format string; the <param> children
provide the values for {0}, {1}, … – here the fields Type
and Address defined in Step 3/4:
<match format="MiniBus {0} to {1}">
<param ref="Type" /> <!-- {0} -->
<param ref="Address" /> <!-- {1} -->
</match>
Result in the frame list, for example: MiniBus Command to 0x00A3 (Type is
resolved to "Command" via its value table).
labelorformat– not both. If both are set on a<match>,formatwins and thelabelis discarded. So write static text directly into theformatstring.
The complete definition
<?xml version="1.0" encoding="utf-8"?>
<Trace-Interpreter-Definition>
<interpreter name="MiniBus" type="FrameOriented" />
<definition>
<valuedescriptor type="byte" name="type-descriptor">
<descriptor value="0x01" name="Status" label="Status message" />
<descriptor value="0x02" name="Command" label="Command" />
<descriptor value="0x03" name="Error" label="Error message" />
</valuedescriptor>
<message>
<const type="byte" value="0x02" name="STX" />
<byte name="Type" valuedescriptor="type-descriptor" />
<word name="Address" bigendian="yes" format="0x{0:X4}" />
<byte name="Length" />
<array sizeRef="Length" name="Payload" />
<calc name="ExpectedChecksum" type="ByteXor" from="0" to="-1" resultType="byte" />
<const ref="ExpectedChecksum" validate="true" name="Checksum" />
<const type="byte" value="0x03" name="ETX" />
<match format="MiniBus {0} to {1}">
<param ref="Type" />
<param ref="Address" />
</match>
</message>
</definition>
</Trace-Interpreter-Definition>
More constructs – branches (switch/case), repetitions (group),
bitfields, ASCII numbers, and custom decoders – can be found in the
Element Reference and under Patterns & Recipes.
Element reference
Complete reference of all TrIML elements and their attributes. The described
meanings correspond to the behavior of the XmlMessageParser (engine); where XSD and
engine differ, the engine behavior documented here applies.
Notation of the attribute tables: M = mandatory, O = optional. "Default" gives the value the engine assumes when the attribute is missing.
Document structure
<Trace-Interpreter-Definition>
Root element. Contains exactly one <interpreter> and exactly one <definition>.
<interpreter>
Interpreter metadata. Not evaluated at parse time, but read during registration.
| Attribute | M/O | Values | Meaning |
|---|---|---|---|
name |
M | any | Display name of the interpreter. |
type |
M | FrameOriented, ByteOriented |
Interpretation mode (see Overview). With ByteOriented, a Flush() from outside closes the open message – the way to express protocols with no frame end in the grammar. |
frameGapUs |
O | integer (µs) | Timing-based framing for delimiter-less protocols (see below). 0/absent = off. |
Timing-based framing (frameGapUs)
Some protocols (e.g. Modbus RTU) have no delimiter bytes – frames are
separated only by idle gaps on the line (≥ 3.5 character times). With
frameGapUs the engine measures the gap between consecutive bytes from the
monotonic microsecond tick each byte carries (ByteInfo.Tick) and, when the gap
reaches the threshold, injects a framestart token. The grammar anchors each
frame with a leading <const token="framestart"/>; the very first byte of the
stream always starts a frame.
<interpreter name="Modbus-RTU" type="FrameOriented" frameGapUs="4000" />
…
<message>
<const token="framestart" name="SOF" />
<field type="byte" name="Address" />
…
</message>
Resolution caveat. Real sub-millisecond gap framing requires capture hardware that timestamps per byte in microseconds. Commodity USB-serial adapters deliver bytes in batches, so within a batch the per-byte timing is lost and the practical floor stays around 1 ms (which still covers Modbus RTU up to ~38400 baud). For higher rates, structural framing + CRC re-sync is the robust complement.
Last frame on an idle bus. Because a frame is closed when the next
framestart(i.e. the next gap) is seen, the last frame before the line goes quiet would otherwise wait for the next telegram. The runtime's idle flush handles this: after the bus has been silent for 2×frameGapUs, it injects a syntheticframestartso the last frame closes cleanly (CRC validated) without a following byte. This only applies to gap-framed interpreters; length-prefixed and delimiter-framed ones are never force-flushed (a legitimately paused frame must not be cut off).
<definition>
Container for all definitions. Direct children: <valuedescriptor>, <struct>
(reusable), <decoder>, <esc-decoder>, and at least one
<message>. Elements named via name within the <definition> can be
referenced via ref.
<message>
Entry point of parsing – the top-level frame structure. May contain all content elements (fields, constants, groups, switch, …).
| Attribute | M/O | Meaning |
|---|---|---|
name |
O | Identifier of the message. |
visible |
O | Visibility in the output (yes/no). |
Field types
Fields consume bytes from the stream. There are two interchangeable notations:
the generic <field type="…"> and the short forms <byte>, <word>,
<dword>, <array>, <string>, <var>.
<byte>, <word>, <dword>, <qword>, <float>, <double>
Integers with 8, 16, 32, or 64 bits, plus IEEE-754 single (4 bytes) and double
(8 bytes) precision. All multi-byte types are little-endian by default; with
bigendian="yes" big-endian.
| Attribute | M/O | Default | Meaning |
|---|---|---|---|
name |
O | – | Field name (for labels and ref/sizeRef/switch ref). |
nameRef |
O | – | Take the field name dynamically from another (string) field. |
bigendian |
O | no |
yes/true → big-endian (every type except byte). |
signed |
O | no |
yes → read the raw value as two's complement. Only on byte, word, dword, qword; anywhere else the definition is rejected at load time. |
format |
O | – | .NET String.Format pattern for display, value on {0}. |
valuedescriptor |
O | – | Reference to a value table (plain text for the value). |
mask |
O | – | Bitmask applied to the value before display/comparison. |
scale |
O | – | Affine scaling: displayed value = raw · scale + offset. Accepts a decimal or a fraction like 100/255. |
offset |
O | 0 |
Additive offset for scale (e.g. -40 for °C). |
unit |
O | – | Unit string appended to the displayed value (e.g. rpm, km/h). |
match |
O | – | Validation pattern, see Match patterns. |
visible |
O | yes |
no/false hides the field in the output. |
<word name="Address" bigendian="yes" format="0x{0:X4}" />
<byte name="Status" valuedescriptor="status-descriptor" />
Affine scaling turns raw integers into physical values. The displayed value is
raw · scale + offset, optionally with a unit. scale may be a fraction
(100/255). Parsing is culture-invariant (. as the decimal separator). Example
(OBD-II PIDs):
<word name="EngineRPM" bigendian="yes" scale="0.25" unit="rpm" format="{0:F0}" />
<byte name="CoolantTemp" scale="1" offset="-40" unit="C" format="{0:F0}" />
<byte name="Throttle" scale="100/255" unit="%" format="{0:F1}" />
The sign (signed="yes") is applied before scaling — which is exactly
what telemetry needs:
<qword name="Timestamp" unit="us" />
<dword name="Latitude" signed="yes" scale="1e-7" unit="deg" format="{0:F7}" />
<float name="Roll" unit="rad" format="{0:F3}" />
Three things that would otherwise surprise you:
signedis a display attribute.match,switch/caseandvaluedescriptorstill compare the unsigned raw value.- Without
scale,qworddoes not go throughdouble. A 64-bit timestamp has more digits than the double mantissa, so the raw value is formatted unconverted. - On
float/doublethe attribute has no meaning and is rejected — the format carries the sign itself.
<array>
Fixed- or variable-length byte sequence.
| Attribute | M/O | Meaning |
|---|---|---|
size |
conditional | Fixed length in bytes. |
sizeRef |
conditional | Length dynamically from a previously read field (by name). |
name, format, visible, valuedescriptor |
O | as for <byte>. |
Exactly one of size/sizeRef must be specified.
<array size="4" name="SerialNumber" />
<array sizeRef="Length" name="Payload" />
<string>
Text field. With size of fixed length, without size up to the null terminator.
| Attribute | M/O | Meaning |
|---|---|---|
size |
O | Fixed byte length; if missing, read up to 0x00. |
sizeRef |
O | Length dynamically from a field. |
encoding |
O | Encoding name, resolved via Encoding.GetEncoding(name) (e.g. utf-8, iso-8859-1, ascii). |
nonprintable |
O | How bytes with no printable glyph appear: raw (default), hex (<02>) or names (<STX>). See below. |
name, format, visible |
O | as for <byte>. |
nonprintable – making control characters visible
| Value | Result |
|---|---|
raw |
Default. Bytes are decoded through the encoding; control characters stay control characters. |
hex |
Everything without a printable glyph as a hex pair: <02>, <0D>, <80>. |
names |
Control characters by their ASCII name: <STX>, <CR>. From 0x7F upwards a hex pair anyway – there are no names for those. |
From
hexon, the value is no longer trimmed. Withoutnonprintablea string value runs through aTrimthat strips spaces, tabs and line breaks at both ends. The two belong together: whoever wants control characters visible does not want the indentation to disappear quietly.
Only for
type="string". On a numeric field the engine rejects the attribute at load time rather than accepting it without effect.
<string size="16" encoding="iso-8859-1" name="DeviceName" />
<string name="Text" /> <!-- null-terminated -->
<field>
Generic notation; type selects the concrete field type. All attributes of
the respective short form apply accordingly.
| Attribute | M/O | Default | Meaning |
|---|---|---|---|
type |
O | byte |
byte, word, dword, array, string, var, ascii-byte, ascii-word, ascii-dword, ascii-digits. |
ref |
O | – | References a previously defined field/var/const and adopts it (with overridable attributes). |
<field type="dword" name="Timestamp" bigendian="yes" />
<var> – hidden field
Like <field>, but visible="false" by default. Typical usage: read a
value once and split it into bit groups via multiple <field ref="…" mask="…">.
<var type="byte" name="flags" />
<field ref="flags" mask="0x0F" name="Lower 4 bits" />
<field ref="flags" mask="0xF0" name="Upper 4 bits" format="{0:X1}" />
With a value attribute, <var> becomes a zero-width literal holder: it
consumes no bytes from the wire and instead carries a fixed, referenceable
value (decimal or 0x…). Use it to feed a per-message constant into a shared
calculation – e.g. MAVLink's CRC_EXTRA, picked up by a common
<calc … appendValueRef="…"> (see <calc>).
<var value="50" name="CrcExtra" /> <!-- not on the wire, referenceable by name -->
ASCII number fields
<ascii-byte>, <ascii-word>, <ascii-dword> read a number written as
ASCII digits and return the numeric value. <ascii-digits> reads
a plain digit sequence of fixed length and requires size.
<ascii-word name="Temperature" /> <!-- e.g. "0235" -> 235 -->
<ascii-digits size="3" name="Channel" /> <!-- exactly 3 digits -->
Constants
<const>
Either expects a fixed byte value in the stream or marks a frame boundary (token) without consuming bytes.
| Attribute | M/O | Default | Meaning |
|---|---|---|---|
type |
conditional | – | Data type of value (byte/word/dword). Mandatory if not a frame token. |
value |
conditional | – | Expected value (0x.. or decimal). Mandatory if not a frame token. |
token |
O | – | Marks a stream position: framestart, frameend, eof, invalid, specialvalue, normalinput. |
ref |
O | – | Reference to a previously defined const/calc result – expects its value at this position. |
mask |
O | – | Bitmask on the comparison. |
validate |
O | false |
true: a deviation marks the frame as faulty, parsing continues. false: a deviation discards the path. |
valuedescriptor |
O | – | Plain-text table for the value. |
format |
O | – | Display pattern for the value. Allowed without a placeholder too: format="Ack" on <const value="0x41" name="Acknowledge"> renders Acknowledge: Ack instead of Acknowledge: 65. That is exactly what it is for — for a constant the raw value says nothing. |
consume |
O | true |
no: the node reads its token like any other, but its byte does not belong to the frame's raw data. For buses whose separator sits between records rather than inside one. Only allowed on <const> — on a data field it would be a value the display shows and the raw data does not contain. |
bigendian |
O | false |
yes: the value sits on the wire with its most significant byte first. It affects the display – without it the word is read in host order and a big-endian checksum shows up byte-swapped (wire BC E6, display 0xE6BC). The comparison stays correct either way: the expected value is turned along with it – from a <calc> (wire order) as well as from a field (whose bytes are already turned). Combined with type="ascii-*" the attribute is rejected: it is not implemented there. |
<const type="byte" value="0x02" name="STX" /> <!-- literal byte -->
<const type="byte" value="0x41" name="Acknowledge" format="Ack" />
<const ref="ExpectedChecksum" validate="true" name="Checksum" />
ASCII-hex checksums (type="ascii-byte"). When the value travels over the wire
as ASCII-hex digits (two hex characters per byte, e.g. NMEA *47) but the reference
was computed as a raw byte via <calc>, add type="ascii-byte" (or ascii-word /
ascii-dword) to the validating ConstRef. It then reads two characters per byte,
decodes them, and compares against the referenced raw value. format controls how
the validated value is shown.
<calc name="ChecksumXor" type="ByteXor" from="1" to="-1" resultType="byte" />
<const type="byte" value="0x2A" name="*" />
<const ref="ChecksumXor" validate="true" type="ascii-byte" name="Checksum" format="{0:X2}" />
The token values at a glance:
token |
Meaning |
|---|---|
framestart |
Frame start (re-sync point). |
frameend |
Frame end. |
eof |
End of the input stream. |
specialvalue |
Special token (supplied by the decoder). |
normalinput |
Normal input byte (rarely needed explicitly). |
invalid |
Invalid input token. |
Important: The special tokens (
framestart,frameend,specialvalue, …) arise only through a decoder. Without a decoder the stream consists exclusively ofNormalInputbytes, and a<const token="…">never matches. For simple frame boundaries use literal bytes (<const value="0x02" />); for token-based framing declare an<esc-decoder>. See Tokens and decoders.
Control flow
<group> – repetition, option, alternative
The type attribute controls the semantics:
type |
Meaning |
|---|---|
* |
0..n repetitions of the children (as long as they match). |
+ |
1..n repetitions (at least once). |
? |
Optional – 0 or 1 occurrence. |
OR |
Alternatives: each child <struct> is a variant; the first matching one wins. |
<!-- 0..n records until end of frame -->
<group type="*">
<byte name="Value" />
</group>
<!-- either a short ENQ or a full frame -->
<group type="OR">
<struct name="ENQ"> ... </struct>
<struct name="Full"> ... </struct>
</group>
<struct> – nested struct
Groups fields into a named subunit. Can be placed inline or – as a
direct child of <definition> – defined reusably and included via ref.
| Attribute | M/O | Meaning |
|---|---|---|
name |
O | Identifier / label of the struct. |
ref |
O | Includes a struct defined in <definition>. |
size |
O | Exact byte length of the struct. |
maxsize |
O | Upper bound of the byte length (not exact). |
sizeRef / maxsizeRef |
O | size/maxsize dynamically from a field. |
nameRef |
O | Name dynamically from a field. |
visible |
O | Visibility. |
<struct name="Time">
<byte name="Hour" format="{0:D2}" />
<byte name="Minute" format="{0:D2}" />
<byte name="Second" format="{0:D2}" />
</struct>
<switch> / <case> / <default>
Branches depending on the value of a previously read field. <switch ref="…">
names the field (or calc/var/unify result); each <case> covers a
value or a pattern, <default> catches the rest.
| Element | Attribute | Meaning |
|---|---|---|
switch |
ref (M) |
Field whose value is compared. |
switch |
mask (O) |
Bitmask applied to the value before comparison. |
case |
value (O) |
Exact comparison value. |
case |
match (O) |
Pattern instead of single value, see Match patterns. |
default |
– | Fallback when no case matches. |
Per <case>, either value or match must be specified.
<byte name="Type" />
<switch ref="Type">
<case value="0x01">
<word name="Status" />
</case>
<case match="(0x42,0x44)"> <!-- 0x42 or 0x44 -->
<array size="6" name="Data" />
</case>
<default>
<match label="Unknown type" />
</default>
</switch>
When nothing matches
Two cases, and they mean different things:
- No
<case>matches and there is no<default>→ the branch fails. Inside a<group type="OR">the next alternative gets its turn; inside a repetition (*,+) the repetition ends and the message continues with what it has read so far; otherwise the message fails and its bytes come out as an error frame. - The value in
refhas not been read on this path at all → the branch fails as well.<default>does not step in: the default branch means "the value matched nocase", not "there was no value".
A ref that does not exist in the definition at all is a different matter: it
is rejected at load time (Undefined node "…" referenced in …).
The value is scoped to one frame. A <switch> cannot see what a previous
frame read at the same place.
Computation & decoding
<calc> – checksums / computations
Computes a value over a byte range and registers it under name,
so that a subsequent <const ref="…" validate="true"> or <switch ref="…">
can access it.
| Attribute | M/O | Default | Meaning |
|---|---|---|---|
name |
M | – | Name of the result (for ref). Use a name different from any <const>'s display name, otherwise the ref resolves to itself. |
type |
O | – | Built-in function: ByteSum, ByteXor, LRC, WordSum, or CRC. |
function |
O | – | Name of a custom function registered via code. |
from |
M | – | Start byte offset. |
to |
M | – | End byte offset (inclusive; negative = relative to the current position). |
startValue |
O | 0 |
Start value of the accumulator (ignored for CRC – use init). |
resultType |
O | byte |
Result type: byte/word/dword/qword. |
encoding |
O | – | ascii-hex: decode the input range from ASCII-hex (2 digits per byte) to binary before computing. |
byteOrder |
O | see below | Order of a multi-byte result: little or big. |
Either type or function must be specified. The built-in types work
everywhere – including in the runtime XML interpreter. function="…", in
contrast, references a function registered on the server and is available only
in bundled system interpreters.
LRCis the Modbus-ASCII longitudinal redundancy check: a byte-sum mod 256, then two's complement (LRC = (byte)(-sum)).WordSumis the 16-bit byte sum (RDM: "the sum of all preceding bytes"). UnlikeByteSumit keeps the carry byte and yields two bytes;resultType="word"belongs with it.
byteOrderhas a different default per type, deliberately:littleforCRC,bigforWordSum. Each follows what the respective protocols put on the wire. If you would rather not rely on that, write it out.
encoding="ascii-hex"is needed when the bytes the checksum covers travel as ASCII-hex on the wire (e.g. Modbus ASCII), because the checksum is defined over the decoded bytes, not the ASCII characters.
<calc name="Checksum" type="ByteSum" from="0" to="9" resultType="byte" />
<const ref="Checksum" validate="true" />
<!-- Modbus ASCII: LRC over the decoded Address+Function+Data bytes -->
<calc name="LrcCalc" type="LRC" from="1" to="-1" resultType="byte" encoding="ascii-hex" />
<const ref="LrcCalc" validate="true" type="ascii-byte" name="LRC" format="{0:X2}" />
type="CRC" – generic parameterised CRC
One configurable CRC (Rocksoft/Williams model) covers the common reflected and
non-reflected variants used by Modbus RTU, MAVLink, CRSF, DNP3 and others. Add
these attributes alongside from/to:
| Attribute | M/O | Default | Meaning |
|---|---|---|---|
width |
O | 16 |
Result width in bits: 8, 16, or 32. |
poly |
O | 0 |
Generator polynomial in normal form (decimal or 0x…). Modbus = 0x8005, not 0xA001. |
init |
O | 0 |
Initial register value. |
reflectIn |
O | false |
Reflect each input byte before processing. |
reflectOut |
O | false |
Reflect the final result. |
xorOut |
O | 0 |
Final XOR applied to the result. |
byteOrder |
O | little |
Wire byte order of the multi-byte result: little (low byte first) or big. |
appendValue |
O | – | Constant byte folded into the CRC after the [from..to] range (before reflectOut/xorOut). For MAVLink's CRC_EXTRA (a per-message byte that is not on the wire). |
appendValueRef |
O | – | Like appendValue, but the byte is taken at runtime from a referenced field/var/const. Lets one shared <calc> pull its CRC_EXTRA from a per-message <var value=… name=…> (DRY instead of one calc per message). |
The polynomial is the normal-form value. The reflected Modbus polynomial
0xA001is expressed aspoly="0x8005"together withreflectIn/reflectOut.
Common parameter sets (reveng catalogue):
| Protocol | width | poly | init | reflectIn/Out | xorOut |
|---|---|---|---|---|---|
| CRC-16/MODBUS | 16 | 0x8005 |
0xFFFF |
true | 0 |
| CRC-8/DVB-S2 (CRSF) | 8 | 0xD5 |
0x00 |
false | 0 |
| CRC-16/MCRF4XX (MAVLink) | 16 | 0x1021 |
0xFFFF |
true | 0 |
| CRC-16/DNP | 16 | 0x3D65 |
0x0000 |
true | 0xFFFF |
<!-- Modbus RTU: CRC16/MODBUS over Address..last data byte, low byte first -->
<calc name="CrcCalc" type="CRC" width="16" poly="0x8005" init="0xFFFF"
reflectIn="true" reflectOut="true" xorOut="0x0000" byteOrder="little"
resultType="word" from="1" to="-1" />
<const ref="CrcCalc" validate="true" name="CRC" />
<!-- MAVLink: CRC16/MCRF4XX + per-message CRC_EXTRA appended (50 = HEARTBEAT) -->
<calc name="Crc" type="CRC" width="16" poly="0x1021" init="0xFFFF"
reflectIn="true" reflectOut="true" byteOrder="little"
resultType="word" appendValue="50" from="1" to="-1" />
<const ref="Crc" validate="true" name="CRC" />
<!-- DRY variant: one shared calc, the CRC_EXTRA comes per message from a var -->
<var value="50" name="CrcExtra" /> <!-- inside HEARTBEAT; SYS_STATUS uses 124, … -->
<calc name="Crc" type="CRC" width="16" poly="0x1021" init="0xFFFF"
reflectIn="true" reflectOut="true" byteOrder="little"
resultType="word" appendValueRef="CrcExtra" from="1" to="-1" />
<const ref="Crc" validate="true" name="CRC" />
<decoder> – the byte tokenizer
A decoder sits in front of the grammar. It receives the raw stream byte by
byte and turns it into a stream of tokens: framestart, frameend,
specialvalue, normalinput, invalidinput. On the way it may drop bytes,
replace them, merge them, split them, and pull in values from the side channel
(LineStatus/ModemStatus).
Only then does the grammar take over: <const token="framestart"/> consumes
exactly the token the decoder produced.
The decoder is fully declarative – no line of code required.
<decoder>
<escape-mask input="0xAC" operation="XOR" mask="0x20" tokentype="normalinput"/>
<sequence input="0xAA" tokentype="framestart"/>
<sequence input="0xAB" tokentype="frameend"/>
</decoder>
<esc-decoder> is a legacy name for the same thing and remains valid; it builds
the same engine.
There are two shapes, and they are mutually exclusive:
| Shape | For |
|---|---|
Rule chain (<sequence>, <escape-mask>, <when>, <map>, <merge>, <counted>, <mode>) |
Protocols with delimiters, escapes, modes. Every byte is classified once and for good. |
Window (<frame>) |
Protocols without delimiters, where a frame is only recognisable because the header fields are plausible and the checksum matches. Decisions are taken back across many bytes. |
Both in one file would give the same rule set two meanings; the mix is rejected at load time.
Attributes on <decoder>
| Attribute | M/O | Meaning |
|---|---|---|
name |
O | Label, for readability only. |
mode |
O | Start mode. Defaults to the first declared <mode>. |
modeScope |
O | frame (default) or stream – how far a mode reaches, see modes. |
dedup |
O | pairs: every byte appears twice on the wire, one copy is dropped. |
merge |
O | Merging of two bytes into one value (short form without <mode>). |
onInvalid |
O | Token type for bytes that are not a valid digit in merge mode. |
All six belong to the rule chain. Together with <frame> they are rejected.
Rules
Rules live either directly inside <decoder> (a single, implicit mode) or
inside <mode> groups.
<sequence> – byte sequence to token type
| Attribute | M/O | Meaning |
|---|---|---|
input |
M | One or more bytes, space-separated ("0x1B 0x5B"). |
tokentype |
M | Token type of the match. |
output |
O | Replacement bytes. Without it the input byte passes through. |
enter |
O | Mode to switch to. |
collapse |
O | next: the match produces no token – instead the next byte carries both its value and this token type. For protocols where ETX and checksum together form the frame end. |
resetMerge |
O | no: the match leaves a half nibble pair standing. Default yes. |
<escape-mask> – escape prefix that transforms the following byte
| Attribute | M/O | Meaning |
|---|---|---|
input |
M | The escape prefix. |
operation |
M | ADD, SUB, INC, DEC, NOT, AND, OR, XOR (case-insensitive). |
mask |
M | Operand of the operation. |
tokentype |
M | Token type of the escaped byte. |
enter |
O | Mode to switch to. |
resetMerge |
O | no: leaves a half nibble pair standing. Default yes. |
<when> / <otherwise> – predicate on byte or side channel
Some buses do not carry the frame boundary in the byte value but in a line state – for instance the 9th bit of a 9-bit transmission.
| Attribute | M/O | Meaning |
|---|---|---|
source |
M¹ | value, linestatus or modemstatus. |
mask |
O | Bit mask (default 0xFF). |
value |
O | Expected value after masking. Without it: the mask itself, i.e. "all mask bits set". |
max |
O | Upper bound – turns the comparison into a value range. |
tokentype |
O | Token type. Omitted when <emit> children are given. |
enter |
O | Mode to switch to. |
¹ Mandatory in the short form – see <condition> below.
<!-- The 9th bit marks the address byte and thus the start of the frame -->
<decoder>
<when source="linestatus" mask="0x40" tokentype="framestart"/>
</decoder>
<otherwise tokentype="…"/> sets the token type for everything no rule matched.
tokentype="notoken" explicitly means: this byte produces nothing – the way
to discard unknown line states.
<condition> – several conditions, combined with AND
Sometimes only the combination of byte value and side channel carries the
meaning. Then <condition> children hold the conditions; all of them must
hold for the rule to fire.
| Attribute | M/O | Meaning |
|---|---|---|
source |
M | value, linestatus or modemstatus. |
mask |
O | Bit mask (default 0xFF). |
value |
O | Expected value after masking. |
max |
O | Upper bound – value range. |
<!-- RAF500: the frame starts at 0xB4 WITH the 9th bit set. Without the
conjunction, every data byte holding 0xB4 would start a frame. -->
<when tokentype="specialvalue">
<condition source="linestatus" mask="0x40"/>
<condition source="value" value="0xB4"/>
</when>
Two rules go with it:
- Short form or children, not both. A
sourceon the<when>itself next to a<condition>child is rejected – otherwise it would stay open whether the attributes are another condition or a default for the children. - No condition at all is rejected. A
<when>withoutsourceand without<condition>would always match and shadow every rule after it. If that is what you want, write<otherwise>– and say so.
<emit> – one input byte, several tokens
| Attribute | M/O | Meaning |
|---|---|---|
source |
M | value, linestatus or modemstatus. |
mask |
O | Bit mask applied to the value read. |
tokentype |
M | Token type. |
<emit> sits exclusively as a child of a <when> – the fan-out belongs to
the condition that triggers it:
<!-- The masked line state becomes a data value itself -->
<when source="linestatus" mask="0x1F" value="0x00" max="0x08">
<emit source="linestatus" mask="0x1F" tokentype="specialvalue"/>
<emit source="value" tokentype="normalinput"/>
</when>
<map> – fixed value translation
For line codes where the transmitted symbol is not the payload value.
| Attribute | M/O | Meaning |
|---|---|---|
onMiss |
O | Replacement value for unmapped symbols. Without it the byte is left unchanged. |
onMissToken |
O | Token type for unmapped symbols. |
<map onMiss="0xFF" onMissToken="specialvalue">
<entry from="0x55" to="0x00"/>
<entry from="0x56" to="0x01"/>
</map>
<counted> – length-driven region
For protocols whose frame length is carried inside the frame.
| Attribute | M/O | Meaning |
|---|---|---|
startToken |
O | Token type that opens the region (default framestart). |
lengthAt |
O | Position of the length byte within the region; 0 is the opening byte (default 1). |
adjust |
O | Addend applied to the length value read, may be negative. |
endToken |
O | Token type of the last byte in the region (default frameend). |
<!-- 0xDD, then the length byte; the frame ends after (length-2) data bytes -->
<decoder>
<sequence input="0xDD" tokentype="framestart"/>
<counted lengthAt="1" adjust="-2" endToken="frameend"/>
</decoder>
<frame> – framing without delimiters
Some protocols have neither a start nor an end character. A frame is only recognisable there because the header fields are plausible and the checksum matches. If either fails, it was not a frame — the decoder starts over one byte later.
That is what <frame> is for. It buffers a window, checks the frame at the head
of the window, and rewinds on failure.
<decoder>
<frame minSize="4" maxSize="60">
<at index="0" match="(0x01-0x3F,0xC0-0xFF)"/> <!-- address -->
<at index="1" match="(0x00-0x3C)" length="total"/> <!-- total length -->
<at index="2" match="(0x01,0x02,0xC0,0xC1)"/> <!-- command -->
<checksum type="bytesum"/>
</frame>
</decoder>
Attribute on <frame> |
M/O | Meaning |
|---|---|---|
size |
O | Fixed frame size for protocols without a length field. Excludes <at length="total">. |
minSize |
O | Smallest possible frame size. Acts as a lower bound on the length read, not just as a start threshold. Defaults to size, otherwise 1. |
maxSize |
O | Hard upper bound. A length field claiming more discards the candidate. |
startToken |
O | Token before the frame (default framestart). |
endToken |
O | Token after the frame (default frameend). |
<at> – predicate on a fixed position
Counted from the start of the frame, not from the stream.
| Attribute | M/O | Meaning |
|---|---|---|
index |
M | 0-based position within the frame. |
match |
O | Accepted values, see match patterns – the same notation as on <byte match=…>. |
length |
O | total: this field carries the total size of the frame. |
size |
O | Width of the length field in bytes (1–4, default 1). Only with length="total". |
byteOrder |
O | Order of the length field: big (default) or little. Only with length="total". |
adjust |
O | Addend applied to the length value read, may be negative. Only with length="total". |
<checksum> – the checksum as a framing condition
Here the checksum is not an error indicator but part of the recognition: if
it does not match, it was not a frame. (Flagging a recognised frame as broken is
still <calc> together with <const validate="true">.)
| Attribute | M/O | Meaning |
|---|---|---|
type |
M | bytesum, bytexor, lrc, wordsum, or crc. |
at |
O | Position of the checksum within the frame; negative counts from the end. Default: at the very end. |
from |
O | First covered byte (default 0). |
to |
O | Last covered byte, inclusive. Default: the byte before the checksum. |
size |
O | Number of checksum bytes. Derived from width for crc and not settable there. |
byteOrder |
O | little (default) or big. |
width, poly, init, xorOut, reflectIn, reflectOut |
O | Only for type="crc"; meaning as for <calc type="CRC">. On other types they are rejected. |
from/to are needed as soon as the checksum does not cover everything before
it — MAVLink computes from byte 1, leaving the start character out:
<frame size="5">
<at index="0" match="(0xFE)"/>
<checksum type="bytesum" from="1"/>
</frame>
Rejected at load time – each of these would otherwise leave a port silent: it accepts bytes, checks, discards, and never reports anything. Missing length source;
sizeagainstminSize/maxSize; an<at>position outside the largest possible frame; a frame size above the window limit;type="crc"withoutpoly; CRC attributes on other types; a misspelledbyteOrder;adjust/size/byteOrderwithoutlength="total".
Modes
Many protocols switch their reading within one telegram: a header in raw bytes,
then ASCII hex, with a text section in between. A <mode> bundles the rules
that currently apply.
<decoder mode="asciihex" modeScope="stream">
<mode name="asciihex" merge="ascii-hex" onInvalid="invalidinput">
<sequence input="0x3A" tokentype="framestart"/>
<sequence input="0x0D" tokentype="frameend"/>
<sequence input="0x02" tokentype="specialvalue" enter="text"/>
</mode>
<mode name="text">
<sequence input="0x03" tokentype="specialvalue" enter="asciihex"/>
</mode>
</decoder>
merge – two bytes make one value
| Value | Meaning |
|---|---|
none |
No merging (default). |
ascii-hex |
Strict hex digits 0-9 A-F a-f. Anything else is invalid, see onInvalid. |
ascii-hex-upper |
Like ascii-hex but upper case only. a–f are invalid. |
low-nibble |
byte & 0x0F – the lower nibble, e.g. for streams whose values already came through <map>. |
ascii-digit-legacy |
(byte - 0x30) & 0x0F. Decodes A–F incorrectly. It exists only to reproduce existing protocols unchanged – never choose it for a new definition. |
<after> – mode switch after a fixed length
| Attribute | M/O | Meaning |
|---|---|---|
bytes |
O* | Number of wire bytes. |
values |
O* | Number of merged values (with ascii-hex that is two wire bytes each). |
enter |
M | Target mode. |
apply |
O | deferred (default) or immediate – see below. |
* Exactly one of bytes/values must be set.
modeScope says how far a mode reaches:
frame(default): within one telegram only. At the frame boundary the mode falls back to the start mode. Right when a telegram always begins the same way.stream: across frame boundaries. Needed when a switch (into a text section, say) still applies to the next telegram.
Not every combination is admissible. With
frame, every mode needs a path back to the start mode – otherwise no token would ever be produced there. The engine checks this on load and rejects the definition rather than leaving the port silent.
apply on <after> says whether the mode ends with this byte
(immediate) or after it (deferred, default). For classifying the next
byte both are equivalent; the difference is whether a frame may be cut here.
Evaluation order
The order in the document is free – the evaluation is fixed:
1. dedup drop the byte?
2. escape-mask open or resolve an escape
3. sequence / when control character or predicate, possibly a mode switch
4. map only for bytes that 1-3 did not match
5. merge only for bytes that 1-3 did not match
6. emit / collapse token output
7. counted / after counters
Two rules follow from this:
- A control character is never translated or merged.
mapandmergeapply exclusively to bytes that no rule in steps 1-3 matched. - A matched control character resets the merge cadence – by default. This is
a property of the rule, not of the decoder: with
resetMerge="no"a half nibble pair survives the match. Both forms occur in real protocols. - Inside a running
<counted>region the length governs, not the byte. Sequences and predicates do not apply there – otherwise a payload byte that happens to look like a delimiter would tear open its own frame.
Collecting & look-ahead
<unify> – combine bytes
Collects the bytes read by the child elements and combines them into a value
of type type. With name the result becomes referenceable (e.g. in
a switch); without name it serves as an anonymous collector.
| Attribute | M/O | Meaning |
|---|---|---|
type |
M | Target type (byte/word/dword/array/string). |
name |
O | Name of the result. |
format |
O | Display format. Without it a string is quoted; format="{0}" drops the quotes. |
valuedescriptor |
O | Value table for the unified value – a unified field is a field. With type="string" the table compares strings (value='"3"'). |
nonprintable |
O | As for <string>; only with type="string". |
visible |
O | no/false hides the result in the output. |
<unify type="array" name="Data">
<group type="*">
<lookahead distance="1" result="failed">
<const type="byte" value="0x03" /> <!-- while not ETX -->
</lookahead>
<byte />
</group>
</unify>
<lookahead> – speculative look-ahead
Checks whether a pattern matches starting at a certain distance, without consuming bytes. This allows reading, for example, variable data ranges up to a terminator.
| Attribute | M/O | Values | Meaning |
|---|---|---|---|
distance |
M | integer ≥ 1 | Tokens from the read head, the tested one included: 1 tests the token under the head, 3 skips two and tests the third. |
result |
M | matched, failed (accepted = legacy alias of matched) |
Desired result: with matched the look-ahead succeeds when the children match; with failed when they do not match (negation). |
The example under <unify> uses result="failed" to express "read bytes as long as
the next byte is not the end byte (ETX)".
Careful with
distancegreater than 1 inside a repetition. A guard withdistance="3"ends the repetition two tokens before the pattern – no matter whether the bytes in between belong to the pattern at all. If a checksum follows the payload, a checksum byte may happen to equal the pattern: the guard then fires in the middle of the payload, the field is cut short and the frame falls apart. And it fires from every position in its window:distance="3"spans two bytes, so a two-byte checksum is fully covered. Measured on the eComm interpreter: out of 600 telegrams with a brim-full text part, four failed – exactly the four whose CRC carried a0x01byte anywhere, i.e. roughly one in 128.Inside a fixed-length region (
<struct sizeRef="…">) the guard is unnecessary anyway: the size check already caps the repetition at the announced length – no field reads past it.
The guard in front of an optional field
The most common use: a field is sometimes present and sometimes not, followed by a fixed tail (checksum, line ending). Read forwards you cannot tell whether the characters under the read head belong to the optional field or already to the checksum. The guard looks past it:
<group type="?">
<lookahead distance="3" result="failed"><const token="frameend" /></lookahead>
<ascii-byte name="Mode" />
</group>
<ascii-byte name="Checksum" />
<const token="frameend" visible="false" />
If the line ending already sits at position 3, nothing but the checksum is left — so the group is skipped. Any number of such groups may follow one another; each one decides for itself.
Two traps, both silent.
- The token type must match.
<const type="byte" value="0x0D" />will never match aframeendtoken — the engine compares the type first. The guard would never fire, the group would eat the checksum, and the frame would vanish without a trace. Use<const token="frameend" />.distancecounts the tested token. For a two-character field in front of a two-character checksum the answer is3, not2. A field of n characters additionally needs the guards5,7, …n+1, or it misfires on a partial remainder.
Output
<match>
Produces the visible frame label. There are two mutually exclusive ways to determine the label:
label– a static text.format– a .NETString.Formatpattern into which the values of the<param>children ({0},{1}, …) are inserted in their order.
| Attribute | M/O | Default | Meaning |
|---|---|---|---|
label |
O | – | Static label. |
format |
O | – | Format pattern over the <param> values. |
add-label |
O | – | Like label, but appending (implicitly sets append="true"). |
add-format |
O | – | Like format, but appending. |
add-ref |
O | – | Shorthand: appends a single field as a parameter (appending). |
append |
O | false |
true: append to the existing output instead of replacing it. |
usespace |
O | true |
false: no separating space when appending. |
error |
O | false |
true: marks the frame as faulty (FrameError), so the UI colours it accordingly. Meant for catch-all messages: an alternative that deliberately accepts an unusable frame so it becomes fully visible. A frame the grammar rejects outright is reported too — but only with the bytes the engine discarded while resetting, and not at all while the parser is still waiting for more input. The catch-all branch stakes out the frame boundaries itself and reports the bytes that were on the wire. Before this, only <const validate="true"> could set the flag, and only as a side effect of a value comparison. |
Limitation: the mark is a parser-level pending flag, consumed only when the frame completes. If the
<match error='true'/>sits in a branch that later fails while a different branch succeeds, the successful frame carries the mark. Harmless in practice as long as the error match is the last child behind the closing token — which is its natural place.
A catch-all branch that keeps broken frames visible:
<group type="OR">
<struct ref="Frame body" visible="false" />
<struct ref="Unreadable frame" visible="false" />
</group>
...
<struct name="Unreadable frame" visible="false">
<group type="*">
<lookahead distance="1" result="failed"><const token="frameend" /></lookahead>
<byte visible="false" />
</group>
<const token="frameend" visible="false" />
<match label="Frame Error!" error="true" />
</struct>
Do not use
labelandformattogether. If both are set on the same<match>,formatwins and overrides thelabel. For static text plus values, write the text directly into the format string:<match format="Status: {0}"><param ref="Value" /></match>.
<param> attributes (children of <match>, in the order {0}, {1}, …):
| Attribute | M/O | Meaning |
|---|---|---|
ref |
O | Field whose value is inserted – the plain text from the value table, otherwise the formatted field value. |
value |
O | A source instead of a field – see the table below. |
useRawValue |
O | true: use the raw field value instead of the value-table plain text. |
value knows three sources; anything else is rejected at load time (a typo
would otherwise leave an empty placeholder in every label):
value |
Meaning |
|---|---|
label |
the label of the frame as built so far |
frametext |
the printable dump of the frame: control characters as <SOH>, printable ASCII as characters, anything above as <XX> |
framehex |
the same dump in hex ("41 42 "), for binary protocols |
The dump ends at the read head – at the bytes consumed up to this <match>.
That is the capability, not its limit: it lets you print a prefix and format the
rest differently.
<!-- ESPA: <SOH>1<STX>1<US>1234<ETX><7B>
The dump comes BEFORE the checksum field so the checksum shows up as a
hex pair rather than as a character. -->
<const type="byte" value="0x03" name="ETX" visible="no" />
<match format="{0}"><param value="frametext" /></match>
<byte name="Checksum" format="{0:X2}" visible="no" />
<match format="{0}<{1}>">
<param value="label" />
<param ref="Checksum" />
</match>
<byte name="Address" format="{0}" />
<byte name="Status" valuedescriptor="status-descriptor" />
<match format="Adr {0} = {1}">
<param ref="Address" />
<param ref="Status" />
</match>
Value tables (enums, bitfields, flags)
<valuedescriptor> / <descriptor>
Maps values to plain text. type determines the kind of mapping.
<valuedescriptor> attribute |
M/O | Meaning |
|---|---|---|
type |
M | byte/word/dword (value mapping), string/array (text mapping), bitfield, flags. |
name |
M | Identifier (for valuedescriptor="…" on fields). |
hideWhenEmpty |
O | type='flags' only: if no bit is set, the referencing field's description is suppressed entirely (behaves like visible='false'). Without the attribute the field stays visible even for value 0. Default: false. |
render |
O | inline (default): the matching descriptor's name is appended in parentheses — Mode: 0 (without). child: the meaning becomes a sub-description with the same offset and length — Mode: 0 with the child without protocol extension. |
<descriptor> attribute |
M/O | Meaning |
|---|---|---|
value |
O | Value to match. |
mask |
O | Bitmask (for bitfield/flags). |
name |
M | internal identifier. |
label |
O | displayed text. |
valuedescriptor |
O | recursive reference to another table. |
default |
O | yes: applies when no other descriptor matches — the default: branch. No value. |
Simple enum:
<valuedescriptor type="byte" name="status-descriptor">
<descriptor value="0x00" name="OK" label="Ready" />
<descriptor value="0x01" name="Busy" label="Busy" />
<descriptor value="0xFF" name="Fault" label="Fault" />
</valuedescriptor>
Flags/bitfield (mask instead of single value):
<valuedescriptor type="flags" name="alarm-flags">
<descriptor mask="0x01" name="FEUER" label="Fire alarm" />
<descriptor mask="0x02" name="STOERUNG" label="Fault" />
<descriptor mask="0x04" name="ABSCHALT" label="Shutdown" />
</valuedescriptor>
render and default apply to type='flags' as well: render="child" puts the
resolved bits on their own line below the raw value instead of in parentheses
behind it, and a <descriptor default="yes" name="0" /> supplies the text for
"no bit set". Together they reproduce what Enum.ToString() does on a [Flags]
enum — the shape hand-written interpreters use for their status words.
Order: the engine emits matches in reverse document order. To reproduce the ascending order of
Enum.ToString(), write the highest bit first.
Hide empty flag fields with hideWhenEmpty (type='flags' only): bytes with
no bit set then produce no description line at all. Handy for long bitmaps
(e.g. a Consolidated Heartbeat) where only the set addresses matter:
<valuedescriptor type="flags" name="sys-flags-0" hideWhenEmpty="true">
<descriptor value="0x01" name="System 0" />
<descriptor value="0x02" name="System 1" />
<!-- ... -->
</valuedescriptor>
For value 0x00 the field's line is dropped entirely; for 0x06 it shows
Systeme 0-7: 6 (System 1, System 2).
Wide bitfields: bits instead of a mask
A mask goes through the number parser and stops at 32 bits there. For
bitfields that reach further or straddle byte boundaries, use the bit
position:
<!-- CRSF/S.BUS: 16 channels of 11 bits, packed across 22 bytes -->
<valuedescriptor type="bitfield" name="rc-channels">
<descriptor name="Channel 1" bits="0..10" />
<descriptor name="Channel 2" bits="11..21" />
<descriptor name="Channel 3" bits="22..32" />
<!-- ... -->
</valuedescriptor>
| Notation | Meaning |
|---|---|
bits="11..21" |
Bits 11 through 21 inclusive. |
bits="7" |
Exactly bit 7. |
- Bit 0 is the least significant bit of the value. With
bigendian="yes"the field has already reversed its bytes — counting happens on the value, not in wire order. - At most 64 bits wide; beyond that the value could no longer be displayed, and silent truncation would be worse than an error.
- Only under
type="bitfield"ortype="flags". Under a value table the descriptor value is a comparison value, not a mask;bits="0..2"would silently mean "value == 7" there. bitsandmask/valueare mutually exclusive.
Match patterns (pattern matching)
The attributes match (on <field>, <const>) and match (on <case>) allow
patterns instead of fixed single values. Optionally, ~ introduces a negation.
Character sets [ … ]
For character-based matching, with ranges via -:
[0-9] a decimal digit
[0-9a-fA-F] a hex digit
~[ \t\r\n] any byte except whitespace
Value sets ( … )
Comma-separated values and ranges (hex or decimal):
(0x02) only 0x02
(0x41,0x43) 0x41 or 0x43
(0x10-0x1F) range 0x10 .. 0x1F
(0x01-0x10,0x20) mixed: range plus single value
~(0x00) anything except 0x00
Examples in use:
<field match="[0-9]" name="Digit" />
<const match="(0x0A,0x0D)" />
<case match="(0x10-0x1F)"> ... </case>
<case match="~(0xFF)"> ... </case>
Static vs. dynamic attributes (…Ref)
Several attributes exist in a static and a "reference" variant. The reference variant resolves the value at parse time from another field:
| Static | Dynamic | Effect |
|---|---|---|
size="8" |
sizeRef="LenField" |
fixed vs. length read from a field |
maxsize="32" |
maxsizeRef="MaxField" |
fixed vs. dynamic upper bound |
name="X" |
nameRef="NameField" |
fixed vs. dynamic field name |
This is how, for example, a length-prefixed payload field is expressed:
<byte name="Length" />
<array sizeRef="Length" name="Payload" />
Patterns & recipes
Recurring tasks and their idiomatic TrIML solution.
Frame delimiters with STX/ETX
The standard case of a frame-oriented protocol: expecting fixed start/end bytes as literal constants (no decoder required).
<const type="byte" value="0x02" name="STX" />
<!-- content -->
<const type="byte" value="0x03" name="ETX" />
If the stream needs to re-synchronize robustly in the middle of a frame, the
boundary bytes can instead be turned into framestart/frameend tokens via an
<esc-decoder> and matched with <const token="…"> — see
Tokens and decoders and the <esc-decoder> section of the
element reference.
Length-prefixed payload
A length byte, followed by exactly that many data bytes — via sizeRef:
<byte name="Length" />
<array sizeRef="Length" name="Payload" />
Compute and validate a checksum
calc produces the expected value, const ref=… validate="true" compares it
with the transmitted byte. On mismatch the frame is flagged as faulty, but
parsing continues:
<calc name="ExpectedCRC" type="ByteXor" from="0" to="-1" resultType="byte" />
<const ref="ExpectedCRC" validate="true" name="Checksum" />
Frame-type-dependent structure (switch)
A type byte determines the further layout:
<byte name="Type" valuedescriptor="type-descriptor" />
<switch ref="Type">
<case value="0x01"><word name="Status" /></case>
<case value="0x02"><array size="6" name="Data" /></case>
<default><match label="Unknown" /></default>
</switch>
Multiple frame forms (group type="OR")
When a stream contains different frame forms that cannot be distinguished by a
single type byte (e.g. a short polling byte vs. a full frame), OR
alternatives separate the cases. The first matching <struct> wins —
discrimination is often done via a match pattern on the first field:
<group type="OR">
<struct>
<const type="byte" value="0x02" name="SOT" />
<byte name="Length" /> ...
</struct>
<struct>
<byte match="(0xA0,0xB0)" name="Polling" />
<match label="Polling" />
</struct>
</group>
Variable-length data up to a terminator (unify + lookahead)
"Read bytes as long as the next byte is not the end byte" — the typical
solution for payload without a length field. The lookahead inspects the next
byte without consuming it:
<unify type="array" name="Data">
<group type="*">
<lookahead distance="1" result="failed">
<const type="byte" value="0x03" /> <!-- while not ETX -->
</lookahead>
<byte />
</group>
</unify>
Split a byte into bit groups (var + mask)
Read the value once, hidden, then expose it multiple times, masked:
<var type="byte" name="ctrl" />
<field ref="ctrl" mask="0x07" name="Mode" />
<field ref="ctrl" mask="0x38" name="Channel" />
<field ref="ctrl" mask="0xC0" name="Priority" />
Flags as plain text (bitfield table)
<valuedescriptor type="flags" name="alarm-flags">
<descriptor mask="0x01" name="FEUER" label="Fire alarm" />
<descriptor mask="0x02" name="STOERUNG" label="Fault" />
</valuedescriptor>
...
<byte name="Alarms" valuedescriptor="alarm-flags" />
ASCII-encoded numbers
Protocols that transmit numbers as text (e.g. "0235") are read with the
ASCII field types:
<ascii-word name="Temperature" /> <!-- "0235" -> 235 -->
<ascii-digits size="3" name="Channel" /> <!-- exactly 3 digits -->
Build the label incrementally (add-ref / add-label)
Instead of a single match at the end, the label can be extended field by
field. add-ref appends a field value, add-label a fixed text — both
implicitly set append="true":
<byte name="Receiver" format="{0:X2}" />
<match add-ref="Receiver" />
<byte name="Sender" format="{0:X2}" />
<match add-ref="Sender" />
...
<const type="byte" value="0x03" name="End" />
<match add-label="<ETX>" />
Escape / framing logic
- Declaratively via
<esc-decoder>— fixed byte sequences and simple bit operations. Works in the runtime XML interpreter too (see Examples). - In code via a
<decoder>— for stateful framing logic (e.g. doubling an escape byte in the data stream). Requires a registered implementation and is therefore only possible in the system interpreters shipped with Trace, not in the pure XML workflow.
Examples
The following examples describe publicly known protocols and show
the TrIML constructs working together. They are intended as learning templates –
always test your own definition with POST /api/interpreters/dryrun (or the
live preview in the Interpreter Assistant) against real sample data.
Minimal: continuous byte dump
The smallest sensible skeleton: after a literal start byte, output each following
byte individually as hex. Demonstrates a literal frame byte and a
group type="*".
<Trace-Interpreter-Definition>
<interpreter name="ByteDump" type="FrameOriented" />
<definition>
<message>
<const type="byte" value="0x7E" name="SOF" />
<match label="Frame" />
<group type="*">
<byte name="b" format="{0:X2}" />
<match add-ref="b" />
</group>
</message>
</definition>
</Trace-Interpreter-Definition>
Modbus RTU
Modbus RTU is binary and has no start/end bytes – telegrams are separated by a transmission pause (3.5 character times). The structure:
Address(1) Function(1) Data(N) CRC16(2, little-endian)
The example shows valuedescriptor for the function codes, switch/case for
the function-dependent data part, and bigendian for the 16-bit registers
(Modbus transmits register values big-endian).
<Trace-Interpreter-Definition>
<interpreter name="Modbus-RTU" type="FrameOriented" frameGapUs="4000" />
<definition>
<valuedescriptor type="byte" name="funccode">
<descriptor value="0x01" name="ReadCoils" label="Read Coils" />
<descriptor value="0x03" name="ReadHoldingRegs" label="Read Holding Registers" />
<descriptor value="0x06" name="WriteSingleReg" label="Write Single Register" />
<descriptor value="0x10" name="WriteMultipleRegs" label="Write Multiple Registers" />
</valuedescriptor>
<message>
<byte name="Address" format="{0}" />
<byte name="Function" valuedescriptor="funccode" />
<switch ref="Function">
<case value="0x03"> <!-- Read Holding Registers (request) -->
<word name="StartAddress" bigendian="yes" format="0x{0:X4}" />
<word name="Count" bigendian="yes" />
</case>
<case value="0x06"> <!-- Write Single Register -->
<word name="Register" bigendian="yes" format="0x{0:X4}" />
<word name="Value" bigendian="yes" format="0x{0:X4}" />
</case>
<default>
<!-- add further function codes here -->
</default>
</switch>
<word name="CRC" format="0x{0:X4}" /> <!-- CRC-16/Modbus, little-endian -->
<match format="Modbus Addr {0} {1}">
<param ref="Address" />
<param ref="Function" />
</match>
</message>
</definition>
</Trace-Interpreter-Definition>
Practical notes on Modbus RTU:
- Framing by timing: Since there are no frame bytes, clean telegram separation depends on the transmission pause between frames.
- Request vs. response: The same function code has different layouts in request and response. If both directions are on one port, you need additional logic (e.g. separate ports/interpreters per direction).
- CRC-16/Modbus cannot be checked with the built-in
calctypes (ByteSum/ByteXor) – they use a different polynomial. Here the CRC is only read as a field, not validated.
NMEA 0183
NMEA 0183 (used by GPS among others) is text-based: a sentence begins with $,
ends before the checksum with *, and closes with <CR><LF>. The example shows
literal frame bytes, collecting a variable-length text body with unify +
lookahead (up to the * byte), and a string field.
$ GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47 <CR><LF>
<Trace-Interpreter-Definition>
<interpreter name="NMEA-0183" type="FrameOriented" />
<definition>
<message>
<const type="byte" value="0x24" name="$" />
<!-- collect the sentence body until the next byte is '*' (0x2A) -->
<unify type="string" name="Sentence">
<group type="*">
<lookahead distance="1" result="failed">
<const type="byte" value="0x2A" />
</lookahead>
<byte />
</group>
</unify>
<!-- XOR over all bytes between '$' and '*' (index 1 .. last byte read) -->
<calc name="ChecksumXor" type="ByteXor" from="1" to="-1" resultType="byte" />
<const type="byte" value="0x2A" name="*" />
<!-- The checksum travels as 2 ASCII-hex digits. type="ascii-byte" on the
validating ConstRef decodes them and compares against ChecksumXor;
a mismatch marks the frame as FrameError instead of discarding it.
format="{0:X2}" shows the checksum as 2-digit hex. -->
<const ref="ChecksumXor" validate="true" type="ascii-byte" name="Checksum" format="{0:X2}" />
<const type="byte" value="0x0D" name="CR" />
<const type="byte" value="0x0A" name="LF" />
<match format="NMEA {0}">
<param ref="Sentence" />
</match>
</message>
</definition>
</Trace-Interpreter-Definition>
On the NMEA checksum: It is the XOR of all characters between
$and*, transmitted as two ASCII-hex digits.calc type="ByteXor"computes the raw byte, and<const ref="ChecksumXor" validate="true" type="ascii-byte">decodes the two wire characters and validates them against it. Thetype="ascii-byte"switch on a validating ConstRef is what bridges "computed raw byte" and "ASCII-hex on the wire" (see the element reference,const/validate).
Testing your own definition
- Write the XML in the Interpreter Assistant of the TraceUI.
- Test it against recorded or live incoming bytes via the preview (
dryrun). - Compare fields and labels against the expected values until detection is correct.
- Create the interpreter and assign it to the port.
You can find further public protocol interpreters that we provide as examples in the repository under the respective named example definitions.
FAQ & troubleshooting
How do I test a new definition?
You don't need a DLL or a build – the XML is loaded at runtime:
- Write the XML in the Interpreter Assistant of the TraceUI (or via
POST /api/interpreters/dryrunwith{ "xml": …, "bytes": [ … ] }). - Run it against a small, known byte sequence –
dryrunreturns the recognized matches without changing the server state. - Compare the recognized fields against the expected values and adjust the XML
until it's correct. Then create it via
POST /api/interpreters/from-xmland assign it to a port.
My frame isn't recognized at all – why?
- The frame boundaries are wrong. Check the start byte (
<const value="…">). If you're working withtoken="framestart"/frameend": these tokens are produced only by a decoder – without an<esc-decoder>(or a code<decoder>) they never match. See Tokens and decoders. - A
constwithoutvalidateaborts the path. If an expected fixed value doesn't match exactly, the entire path is discarded. For checksums always setvalidate="true", so that only the frame is marked as faulty. size/sizeReftoo large. If a field demands more bytes than the frame provides, the match fails.sizeRefmust point to the correct length field.
value vs. match – which one?
value="0x02"expects exactly this value.match="(0x02,0x06)"ormatch="(0x10-0x1F)"allows multiple values/ranges.match="[0-9]"matches character-based (sets with[ ]).- A leading
~negates:~(0x00)= "anything except 0x00".
For <case> the rule is: either value or match, not both.
Why doesn't my field appear in the output?
visible="false"(or a<var>) deliberately hides fields.- Fields without a
namecan't be referenced and, depending on the context, don't appear on their own. - The visible label is produced only by a
<match>– without amatchelement the frame designation is missing.
How do I build the label incrementally?
With add-ref (append a field value) and add-label (append text). Both
implicitly set append="true". Alternatively, a single <match> at the end with
multiple <param> children and a format pattern.
Big-Endian / Little-Endian?
word/dword are little-endian by default. For big-endian, set
bigendian="yes" on the field.
How do I format values (hex, leading zeros, units)?
format is a .NET String.Format pattern; the value is at {0}:
| Pattern | Output for 47 |
|---|---|
{0:X2} |
2F |
0x{0:X4} |
0x002F |
{0:D3} |
047 |
{0} °C |
47 °C |
Built-in calc functions
ByteSum, ByteXor, LRC, WordSum and the parameterised CRC – all without
code, also in the runtime XML interpreter. function="name" references a function
registered on the server and exists for the bundled system interpreters; new
definitions do not need it.
Does <decoder> require code?
No. It is fully declarative and comes in two mutually exclusive shapes:
- Rule chain –
<sequence>,<escape-mask>,<when>,<map>,<merge>,<counted>and<mode>. For protocols with delimiters, escapes and shifting readings. Every byte is classified once and for good. - Window –
<frame>. For protocols without delimiters, where a frame is only recognisable because the header fields are plausible and the checksum matches. The decoder buffers, checks, and rewinds on failure.
<esc-decoder> is a legacy name for the rule chain and builds the same engine.
A <decoder name="X"/> without children is the one remaining code
reference: it points at an implementation hooked in via RegisterDecoder. That
path is legacy and goes away with the migration.
Note: XSD defaults vs. engine behavior
The XSD lists default values for some attributes that the engine does not always handle identically. The engine behavior is authoritative. Verified points:
match/@usespace– defaulttrue(separator space between parameters);falseturns it off.match/@append– defaultfalse;add-label/add-format/add-refset it implicitly totrue.lookahead/@result– canonicallymatched/failed;acceptedis a legacy alias formatched.
When in doubt, check the result via dryrun against real data.
Where do I find templates?
The examples page provides complete definitions for publicly known protocols (Modbus RTU, NMEA 0183) as well as a minimal skeleton. They cover the most important constructs and can be copied as a starting point for your own definitions.