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: All constructs described in this reference – fields,
switch,group,unify,lookahead, value tables,<esc-decoder>, and the built-incalcfunctions (ByteSum/ByteXor) – work in the runtime XML interpreter without any code at all. Only two advanced constructs require registered C# code and are therefore not available in the pure XML workflow: the code-based<decoder>and customcalc function="…". For the vast majority of protocols you 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). |
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>
Integers with 8, 16, or 32 bits respectively. word/dword 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 (only word/dword). |
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}" />
<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). |
name, format, visible |
O | as for <byte>. |
<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. |
<const type="byte" value="0x02" name="STX" /> <!-- literal byte -->
<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>
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, 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. |
encoding |
O | – | ascii-hex: decode the input range from ASCII-hex (2 digits per byte) to binary before computing. |
Either type or function must be specified. The built-in types ByteSum,
ByteXor, LRC and CRC 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)).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> – custom decoder (code, advanced)
Includes a decoder registered via code. Decoders transform the raw stream (framing, stateful escape sequences) before the actual parsing.
| Attribute | M/O | Meaning |
|---|---|---|
function / name |
M | Name of the registered decoder. |
<decoder name="MyDecoder" />
Not available in the runtime XML workflow. A code-based
<decoder>requires an implementation registered on the server and is therefore available only in bundled system interpreters – not in an interpreter generated from XML at runtime. For declarative framing use<esc-decoder>instead (see below); for the vast majority of protocols that is sufficient.
<esc-decoder> – declarative escape decoder
Describes escape sequences purely in XML – without code. Children are <sequence>
(byte sequence → output + token) and <escape-mask> (bit operation on a byte).
<sequence> attributes: input (input bytes, space-separated), output
(output bytes), tokentype (resulting token).
<escape-mask> attributes: input, operation (ADD, SUB, INC, DEC,
NOT, AND, OR, XOR), mask (byte), tokentype.
<esc-decoder>
<sequence input="0x80" output="0x80" tokentype="framestart" />
<sequence input="0xC4" output="0xC4" tokentype="frameend" />
<escape-mask input="0x81" operation="OR" mask="0x80" tokentype="normalinput" />
</esc-decoder>
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. |
<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 | Number of bytes to look ahead from the current position. |
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)".
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. |
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 | Only supported value: value="label" inserts the label built so far. |
useRawValue |
O | true: use the raw field value instead of the value-table plain text. |
<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. |
<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. |
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>
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).
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="ByteOriented" />
<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
type="ByteSum" (sum) and type="ByteXor" (XOR) over the range from..to –
both without code, also in the runtime XML interpreter. More complex checksums
(CRC etc.) require a function registered on the server (function="name") and are
therefore only available in the bundled system interpreters.
Declarative vs. code decoder?
<esc-decoder>(declarative): for fixed escape sequences and simple bit operations (AND/OR/XOR/…). No code needed – works in the pure XML workflow.<decoder>(code): for stateful framing logic. Requires an implementation registered on the server and is therefore only available in the bundled system interpreters.
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.