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:

  1. Bytes are fed into the parser piece by piece.
  2. 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).
  3. If the structure matches completely, a match is produced – a recognized frame with a label and hierarchical field descriptions.
  4. 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:

  1. 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.

  2. 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/dryrun with { "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-in calc functions (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 custom calc 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 = 0x03 delimit the telegram.
  • Type is a byte with a fixed meaning (Status / Command / Error).
  • Addr is a 16-bit address (big-endian).
  • Length specifies the number of payload bytes that follow.
  • Checksum is 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/frameend tokens 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 the dryrun endpoint.

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).

label or format – not both. If both are set on a <match>, format wins and the label is discarded. So write static text directly into the format string.

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 frameGapUs, it injects a synthetic framestart so 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 of NormalInput bytes, 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.

  • LRC is 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 0xA001 is expressed as poly="0x8005" together with reflectIn/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 .NET String.Format pattern 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 label and format together. If both are set on the same <match>, format wins and overrides the label. 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="&lt;ETX&gt;" />

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 calc types (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. The type="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

  1. Write the XML in the Interpreter Assistant of the TraceUI.
  2. Test it against recorded or live incoming bytes via the preview (dryrun).
  3. Compare fields and labels against the expected values until detection is correct.
  4. 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:

  1. Write the XML in the Interpreter Assistant of the TraceUI (or via POST /api/interpreters/dryrun with { "xml": …, "bytes": [ … ] }).
  2. Run it against a small, known byte sequence – dryrun returns the recognized matches without changing the server state.
  3. Compare the recognized fields against the expected values and adjust the XML until it's correct. Then create it via POST /api/interpreters/from-xml and 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 with token="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 const without validate aborts the path. If an expected fixed value doesn't match exactly, the entire path is discarded. For checksums always set validate="true", so that only the frame is marked as faulty.
  • size/sizeRef too large. If a field demands more bytes than the frame provides, the match fails. sizeRef must point to the correct length field.

value vs. match – which one?

  • value="0x02" expects exactly this value.
  • match="(0x02,0x06)" or match="(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 name can't be referenced and, depending on the context, don't appear on their own.
  • The visible label is produced only by a <match> – without a match element 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 – default true (separator space between parameters); false turns it off.
  • match/@append – default false; add-label/add-format/add-ref set it implicitly to true.
  • lookahead/@result – canonically matched / failed; accepted is a legacy alias for matched.

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.