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: Every construct described in this reference works in the runtime XML interpreter without any code at all – fields, switch, group, unify, lookahead, value tables, the built-in calc functions (ByteSum, ByteXor, LRC, WordSum, parameterised CRC), and the full <decoder>, including modes and the <frame> window framing for protocols without delimiters.

Exactly one path still requires registered C# code and is therefore not available in the pure XML workflow: calc function="…" and a <decoder name="X"/> without children. Both are legacy; new definitions do not need them.

Next steps

  • Tutorial – build a small interpreter from scratch.
  • Element reference – every tag and attribute in detail.
  • Patterns & recipes – checksums, variable length, bit fields, ASCII numbers.
  • Examples – complete definitions for public protocols (Modbus RTU, NMEA 0183).

Tutorial: An Interpreter in 7 Steps

We will build an interpreter step by step for a small, made-up protocol – a typical status telegram of the kind found in building or plant automation. Each step introduces exactly one new TrIML concept.

Our example protocol "MiniBus":

 STX  Type  Addr(2 B)  Length  Payload(Length B)  Checksum  ETX
 0x02  ..    .. ..       ..       ...                 ..      0x03
  • STX = 0x02, ETX = 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). With ByteOriented, a Flush() from outside closes the open message – the way to express protocols with no frame end in the grammar.
frameGapUs O integer (µs) Timing-based framing for delimiter-less protocols (see below). 0/absent = off.

Timing-based framing (frameGapUs)

Some protocols (e.g. Modbus RTU) have no delimiter bytes – frames are separated only by idle gaps on the line (≥ 3.5 character times). With frameGapUs the engine measures the gap between consecutive bytes from the monotonic microsecond tick each byte carries (ByteInfo.Tick) and, when the gap reaches the threshold, injects a framestart token. The grammar anchors each frame with a leading <const token="framestart"/>; the very first byte of the stream always starts a frame.

<interpreter name="Modbus-RTU" type="FrameOriented" frameGapUs="4000" />
…
<message>
  <const token="framestart" name="SOF" />
  <field type="byte" name="Address" />
  …
</message>

Resolution caveat. Real sub-millisecond gap framing requires capture hardware that timestamps per byte in microseconds. Commodity USB-serial adapters deliver bytes in batches, so within a batch the per-byte timing is lost and the practical floor stays around 1 ms (which still covers Modbus RTU up to ~38400 baud). For higher rates, structural framing + CRC re-sync is the robust complement.

Last frame on an idle bus. Because a frame is closed when the next framestart (i.e. the next gap) is seen, the last frame before the line goes quiet would otherwise wait for the next telegram. The runtime's idle flush handles this: after the bus has been silent for 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>, <qword>, <float>, <double>

Integers with 8, 16, 32, or 64 bits, plus IEEE-754 single (4 bytes) and double (8 bytes) precision. All multi-byte types are little-endian by default; with bigendian="yes" big-endian.

Attribute M/O Default Meaning
name O Field name (for labels and ref/sizeRef/switch ref).
nameRef O Take the field name dynamically from another (string) field.
bigendian O no yes/true → big-endian (every type except byte).
signed O no yes → read the raw value as two's complement. Only on byte, word, dword, qword; anywhere else the definition is rejected at load time.
format O .NET String.Format pattern for display, value on {0}.
valuedescriptor O Reference to a value table (plain text for the value).
mask O Bitmask applied to the value before display/comparison.
scale O Affine scaling: displayed value = raw · scale + offset. Accepts a decimal or a fraction like 100/255.
offset O 0 Additive offset for scale (e.g. -40 for °C).
unit O Unit string appended to the displayed value (e.g. rpm, km/h).
match O Validation pattern, see Match patterns.
visible O yes no/false hides the field in the output.
<word name="Address" bigendian="yes" format="0x{0:X4}" />
<byte name="Status" valuedescriptor="status-descriptor" />

Affine scaling turns raw integers into physical values. The displayed value is raw · scale + offset, optionally with a unit. scale may be a fraction (100/255). Parsing is culture-invariant (. as the decimal separator). Example (OBD-II PIDs):

<word name="EngineRPM"   bigendian="yes" scale="0.25"    unit="rpm" format="{0:F0}" />
<byte name="CoolantTemp"                 scale="1" offset="-40" unit="C" format="{0:F0}" />
<byte name="Throttle"                    scale="100/255" unit="%"  format="{0:F1}" />

The sign (signed="yes") is applied before scaling — which is exactly what telemetry needs:

<qword name="Timestamp" unit="us" />
<dword name="Latitude" signed="yes" scale="1e-7" unit="deg" format="{0:F7}" />
<float name="Roll" unit="rad" format="{0:F3}" />

Three things that would otherwise surprise you:

  • signed is a display attribute. match, switch/case and valuedescriptor still compare the unsigned raw value.
  • Without scale, qword does not go through double. A 64-bit timestamp has more digits than the double mantissa, so the raw value is formatted unconverted.
  • On float/double the attribute has no meaning and is rejected — the format carries the sign itself.

<array>

Fixed- or variable-length byte sequence.

Attribute M/O Meaning
size conditional Fixed length in bytes.
sizeRef conditional Length dynamically from a previously read field (by name).
name, format, visible, valuedescriptor O as for <byte>.

Exactly one of size/sizeRef must be specified.

<array size="4"        name="SerialNumber" />
<array sizeRef="Length" name="Payload" />

<string>

Text field. With size of fixed length, without size up to the null terminator.

Attribute M/O Meaning
size O Fixed byte length; if missing, read up to 0x00.
sizeRef O Length dynamically from a field.
encoding O Encoding name, resolved via Encoding.GetEncoding(name) (e.g. utf-8, iso-8859-1, ascii).
nonprintable O How bytes with no printable glyph appear: raw (default), hex (<02>) or names (<STX>). See below.
name, format, visible O as for <byte>.

nonprintable – making control characters visible

Value Result
raw Default. Bytes are decoded through the encoding; control characters stay control characters.
hex Everything without a printable glyph as a hex pair: <02>, <0D>, <80>.
names Control characters by their ASCII name: <STX>, <CR>. From 0x7F upwards a hex pair anyway – there are no names for those.

From hex on, the value is no longer trimmed. Without nonprintable a string value runs through a Trim that strips spaces, tabs and line breaks at both ends. The two belong together: whoever wants control characters visible does not want the indentation to disappear quietly.

Only for type="string". On a numeric field the engine rejects the attribute at load time rather than accepting it without effect.

<string size="16" encoding="iso-8859-1" name="DeviceName" />
<string name="Text" />              <!-- null-terminated -->

<field>

Generic notation; type selects the concrete field type. All attributes of the respective short form apply accordingly.

Attribute M/O Default Meaning
type O byte byte, word, dword, array, string, var, ascii-byte, ascii-word, ascii-dword, ascii-digits.
ref O References a previously defined field/var/const and adopts it (with overridable attributes).
<field type="dword" name="Timestamp" bigendian="yes" />

<var> – hidden field

Like <field>, but visible="false" by default. Typical usage: read a value once and split it into bit groups via multiple <field ref="…" mask="…">.

<var type="byte" name="flags" />
<field ref="flags" mask="0x0F" name="Lower 4 bits" />
<field ref="flags" mask="0xF0" name="Upper 4 bits"  format="{0:X1}" />

With a value attribute, <var> becomes a zero-width literal holder: it consumes no bytes from the wire and instead carries a fixed, referenceable value (decimal or 0x…). Use it to feed a per-message constant into a shared calculation – e.g. MAVLink's CRC_EXTRA, picked up by a common <calc … appendValueRef="…"> (see <calc>).

<var value="50" name="CrcExtra" />   <!-- not on the wire, referenceable by name -->

ASCII number fields

<ascii-byte>, <ascii-word>, <ascii-dword> read a number written as ASCII digits and return the numeric value. <ascii-digits> reads a plain digit sequence of fixed length and requires size.

<ascii-word name="Temperature" />          <!-- e.g. "0235" -> 235 -->
<ascii-digits size="3" name="Channel" />    <!-- exactly 3 digits -->

Constants

<const>

Either expects a fixed byte value in the stream or marks a frame boundary (token) without consuming bytes.

Attribute M/O Default Meaning
type conditional Data type of value (byte/word/dword). Mandatory if not a frame token.
value conditional Expected value (0x.. or decimal). Mandatory if not a frame token.
token O Marks a stream position: framestart, frameend, eof, invalid, specialvalue, normalinput.
ref O Reference to a previously defined const/calc result – expects its value at this position.
mask O Bitmask on the comparison.
validate O false true: a deviation marks the frame as faulty, parsing continues. false: a deviation discards the path.
valuedescriptor O Plain-text table for the value.
format O Display pattern for the value. Allowed without a placeholder too: format="Ack" on <const value="0x41" name="Acknowledge"> renders Acknowledge: Ack instead of Acknowledge: 65. That is exactly what it is for — for a constant the raw value says nothing.
consume O true no: the node reads its token like any other, but its byte does not belong to the frame's raw data. For buses whose separator sits between records rather than inside one. Only allowed on <const> — on a data field it would be a value the display shows and the raw data does not contain.
bigendian O false yes: the value sits on the wire with its most significant byte first. It affects the display – without it the word is read in host order and a big-endian checksum shows up byte-swapped (wire BC E6, display 0xE6BC). The comparison stays correct either way: the expected value is turned along with it – from a <calc> (wire order) as well as from a field (whose bytes are already turned). Combined with type="ascii-*" the attribute is rejected: it is not implemented there.
<const type="byte" value="0x02" name="STX" />              <!-- literal byte -->
<const type="byte" value="0x41" name="Acknowledge" format="Ack" />
<const ref="ExpectedChecksum" validate="true" name="Checksum" />

ASCII-hex checksums (type="ascii-byte"). When the value travels over the wire as ASCII-hex digits (two hex characters per byte, e.g. NMEA *47) but the reference was computed as a raw byte via <calc>, add type="ascii-byte" (or ascii-word / ascii-dword) to the validating ConstRef. It then reads two characters per byte, decodes them, and compares against the referenced raw value. format controls how the validated value is shown.

<calc name="ChecksumXor" type="ByteXor" from="1" to="-1" resultType="byte" />
<const type="byte" value="0x2A" name="*" />
<const ref="ChecksumXor" validate="true" type="ascii-byte" name="Checksum" format="{0:X2}" />

The token values at a glance:

token Meaning
framestart Frame start (re-sync point).
frameend Frame end.
eof End of the input stream.
specialvalue Special token (supplied by the decoder).
normalinput Normal input byte (rarely needed explicitly).
invalid Invalid input token.

Important: The special tokens (framestart, frameend, specialvalue, …) arise only through a decoder. Without a decoder the stream consists exclusively 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>

When nothing matches

Two cases, and they mean different things:

  • No <case> matches and there is no <default> → the branch fails. Inside a <group type="OR"> the next alternative gets its turn; inside a repetition (*, +) the repetition ends and the message continues with what it has read so far; otherwise the message fails and its bytes come out as an error frame.
  • The value in ref has not been read on this path at all → the branch fails as well. <default> does not step in: the default branch means "the value matched no case", not "there was no value".

A ref that does not exist in the definition at all is a different matter: it is rejected at load time (Undefined node "…" referenced in …).

The value is scoped to one frame. A <switch> cannot see what a previous frame read at the same place.


Computation & decoding

<calc> – checksums / computations

Computes a value over a byte range and registers it under name, so that a subsequent <const ref="…" validate="true"> or <switch ref="…"> can access it.

Attribute M/O Default Meaning
name M Name of the result (for ref). Use a name different from any <const>'s display name, otherwise the ref resolves to itself.
type O Built-in function: ByteSum, ByteXor, LRC, WordSum, or CRC.
function O Name of a custom function registered via code.
from M Start byte offset.
to M End byte offset (inclusive; negative = relative to the current position).
startValue O 0 Start value of the accumulator (ignored for CRC – use init).
resultType O byte Result type: byte/word/dword/qword.
encoding O ascii-hex: decode the input range from ASCII-hex (2 digits per byte) to binary before computing.
byteOrder O see below Order of a multi-byte result: little or big.

Either type or function must be specified. The built-in types work everywhere – including in the runtime XML interpreter. function="…", in contrast, references a function registered on the server and is available only in bundled system interpreters.

  • LRC is the Modbus-ASCII longitudinal redundancy check: a byte-sum mod 256, then two's complement (LRC = (byte)(-sum)).
  • WordSum is the 16-bit byte sum (RDM: "the sum of all preceding bytes"). Unlike ByteSum it keeps the carry byte and yields two bytes; resultType="word" belongs with it.

byteOrder has a different default per type, deliberately: little for CRC, big for WordSum. Each follows what the respective protocols put on the wire. If you would rather not rely on that, write it out.

  • encoding="ascii-hex" is needed when the bytes the checksum covers travel as ASCII-hex on the wire (e.g. Modbus ASCII), because the checksum is defined over the decoded bytes, not the ASCII characters.
<calc name="Checksum" type="ByteSum" from="0" to="9" resultType="byte" />
<const ref="Checksum" validate="true" />

<!-- Modbus ASCII: LRC over the decoded Address+Function+Data bytes -->
<calc name="LrcCalc" type="LRC" from="1" to="-1" resultType="byte" encoding="ascii-hex" />
<const ref="LrcCalc" validate="true" type="ascii-byte" name="LRC" format="{0:X2}" />

type="CRC" – generic parameterised CRC

One configurable CRC (Rocksoft/Williams model) covers the common reflected and non-reflected variants used by Modbus RTU, MAVLink, CRSF, DNP3 and others. Add these attributes alongside from/to:

Attribute M/O Default Meaning
width O 16 Result width in bits: 8, 16, or 32.
poly O 0 Generator polynomial in normal form (decimal or 0x…). Modbus = 0x8005, not 0xA001.
init O 0 Initial register value.
reflectIn O false Reflect each input byte before processing.
reflectOut O false Reflect the final result.
xorOut O 0 Final XOR applied to the result.
byteOrder O little Wire byte order of the multi-byte result: little (low byte first) or big.
appendValue O Constant byte folded into the CRC after the [from..to] range (before reflectOut/xorOut). For MAVLink's CRC_EXTRA (a per-message byte that is not on the wire).
appendValueRef O Like appendValue, but the byte is taken at runtime from a referenced field/var/const. Lets one shared <calc> pull its CRC_EXTRA from a per-message <var value=… name=…> (DRY instead of one calc per message).

The polynomial is the normal-form value. The reflected Modbus polynomial 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> – the byte tokenizer

A decoder sits in front of the grammar. It receives the raw stream byte by byte and turns it into a stream of tokens: framestart, frameend, specialvalue, normalinput, invalidinput. On the way it may drop bytes, replace them, merge them, split them, and pull in values from the side channel (LineStatus/ModemStatus).

Only then does the grammar take over: <const token="framestart"/> consumes exactly the token the decoder produced.

The decoder is fully declarative – no line of code required.

<decoder>
  <escape-mask input="0xAC" operation="XOR" mask="0x20" tokentype="normalinput"/>
  <sequence    input="0xAA" tokentype="framestart"/>
  <sequence    input="0xAB" tokentype="frameend"/>
</decoder>

<esc-decoder> is a legacy name for the same thing and remains valid; it builds the same engine.

There are two shapes, and they are mutually exclusive:

Shape For
Rule chain (<sequence>, <escape-mask>, <when>, <map>, <merge>, <counted>, <mode>) Protocols with delimiters, escapes, modes. Every byte is classified once and for good.
Window (<frame>) Protocols without delimiters, where a frame is only recognisable because the header fields are plausible and the checksum matches. Decisions are taken back across many bytes.

Both in one file would give the same rule set two meanings; the mix is rejected at load time.

Attributes on <decoder>

Attribute M/O Meaning
name O Label, for readability only.
mode O Start mode. Defaults to the first declared <mode>.
modeScope O frame (default) or stream – how far a mode reaches, see modes.
dedup O pairs: every byte appears twice on the wire, one copy is dropped.
merge O Merging of two bytes into one value (short form without <mode>).
onInvalid O Token type for bytes that are not a valid digit in merge mode.

All six belong to the rule chain. Together with <frame> they are rejected.

Rules

Rules live either directly inside <decoder> (a single, implicit mode) or inside <mode> groups.

<sequence> – byte sequence to token type

Attribute M/O Meaning
input M One or more bytes, space-separated ("0x1B 0x5B").
tokentype M Token type of the match.
output O Replacement bytes. Without it the input byte passes through.
enter O Mode to switch to.
collapse O next: the match produces no token – instead the next byte carries both its value and this token type. For protocols where ETX and checksum together form the frame end.
resetMerge O no: the match leaves a half nibble pair standing. Default yes.

<escape-mask> – escape prefix that transforms the following byte

Attribute M/O Meaning
input M The escape prefix.
operation M ADD, SUB, INC, DEC, NOT, AND, OR, XOR (case-insensitive).
mask M Operand of the operation.
tokentype M Token type of the escaped byte.
enter O Mode to switch to.
resetMerge O no: leaves a half nibble pair standing. Default yes.

<when> / <otherwise> – predicate on byte or side channel

Some buses do not carry the frame boundary in the byte value but in a line state – for instance the 9th bit of a 9-bit transmission.

Attribute M/O Meaning
source value, linestatus or modemstatus.
mask O Bit mask (default 0xFF).
value O Expected value after masking. Without it: the mask itself, i.e. "all mask bits set".
max O Upper bound – turns the comparison into a value range.
tokentype O Token type. Omitted when <emit> children are given.
enter O Mode to switch to.

¹ Mandatory in the short form – see <condition> below.

<!-- The 9th bit marks the address byte and thus the start of the frame -->
<decoder>
  <when source="linestatus" mask="0x40" tokentype="framestart"/>
</decoder>

<otherwise tokentype="…"/> sets the token type for everything no rule matched. tokentype="notoken" explicitly means: this byte produces nothing – the way to discard unknown line states.

<condition> – several conditions, combined with AND

Sometimes only the combination of byte value and side channel carries the meaning. Then <condition> children hold the conditions; all of them must hold for the rule to fire.

Attribute M/O Meaning
source M value, linestatus or modemstatus.
mask O Bit mask (default 0xFF).
value O Expected value after masking.
max O Upper bound – value range.
<!-- RAF500: the frame starts at 0xB4 WITH the 9th bit set. Without the
     conjunction, every data byte holding 0xB4 would start a frame. -->
<when tokentype="specialvalue">
  <condition source="linestatus" mask="0x40"/>
  <condition source="value" value="0xB4"/>
</when>

Two rules go with it:

  • Short form or children, not both. A source on the <when> itself next to a <condition> child is rejected – otherwise it would stay open whether the attributes are another condition or a default for the children.
  • No condition at all is rejected. A <when> without source and without <condition> would always match and shadow every rule after it. If that is what you want, write <otherwise> – and say so.

<emit> – one input byte, several tokens

Attribute M/O Meaning
source M value, linestatus or modemstatus.
mask O Bit mask applied to the value read.
tokentype M Token type.

<emit> sits exclusively as a child of a <when> – the fan-out belongs to the condition that triggers it:

<!-- The masked line state becomes a data value itself -->
<when source="linestatus" mask="0x1F" value="0x00" max="0x08">
  <emit source="linestatus" mask="0x1F" tokentype="specialvalue"/>
  <emit source="value"                  tokentype="normalinput"/>
</when>

<map> – fixed value translation

For line codes where the transmitted symbol is not the payload value.

Attribute M/O Meaning
onMiss O Replacement value for unmapped symbols. Without it the byte is left unchanged.
onMissToken O Token type for unmapped symbols.
<map onMiss="0xFF" onMissToken="specialvalue">
  <entry from="0x55" to="0x00"/>
  <entry from="0x56" to="0x01"/>
</map>

<counted> – length-driven region

For protocols whose frame length is carried inside the frame.

Attribute M/O Meaning
startToken O Token type that opens the region (default framestart).
lengthAt O Position of the length byte within the region; 0 is the opening byte (default 1).
adjust O Addend applied to the length value read, may be negative.
endToken O Token type of the last byte in the region (default frameend).
<!-- 0xDD, then the length byte; the frame ends after (length-2) data bytes -->
<decoder>
  <sequence input="0xDD" tokentype="framestart"/>
  <counted  lengthAt="1" adjust="-2" endToken="frameend"/>
</decoder>

<frame> – framing without delimiters

Some protocols have neither a start nor an end character. A frame is only recognisable there because the header fields are plausible and the checksum matches. If either fails, it was not a frame — the decoder starts over one byte later.

That is what <frame> is for. It buffers a window, checks the frame at the head of the window, and rewinds on failure.

<decoder>
  <frame minSize="4" maxSize="60">
    <at index="0" match="(0x01-0x3F,0xC0-0xFF)"/>          <!-- address     -->
    <at index="1" match="(0x00-0x3C)" length="total"/>     <!-- total length -->
    <at index="2" match="(0x01,0x02,0xC0,0xC1)"/>          <!-- command     -->
    <checksum type="bytesum"/>
  </frame>
</decoder>
Attribute on <frame> M/O Meaning
size O Fixed frame size for protocols without a length field. Excludes <at length="total">.
minSize O Smallest possible frame size. Acts as a lower bound on the length read, not just as a start threshold. Defaults to size, otherwise 1.
maxSize O Hard upper bound. A length field claiming more discards the candidate.
startToken O Token before the frame (default framestart).
endToken O Token after the frame (default frameend).

<at> – predicate on a fixed position

Counted from the start of the frame, not from the stream.

Attribute M/O Meaning
index M 0-based position within the frame.
match O Accepted values, see match patterns – the same notation as on <byte match=…>.
length O total: this field carries the total size of the frame.
size O Width of the length field in bytes (1–4, default 1). Only with length="total".
byteOrder O Order of the length field: big (default) or little. Only with length="total".
adjust O Addend applied to the length value read, may be negative. Only with length="total".

<checksum> – the checksum as a framing condition

Here the checksum is not an error indicator but part of the recognition: if it does not match, it was not a frame. (Flagging a recognised frame as broken is still <calc> together with <const validate="true">.)

Attribute M/O Meaning
type M bytesum, bytexor, lrc, wordsum, or crc.
at O Position of the checksum within the frame; negative counts from the end. Default: at the very end.
from O First covered byte (default 0).
to O Last covered byte, inclusive. Default: the byte before the checksum.
size O Number of checksum bytes. Derived from width for crc and not settable there.
byteOrder O little (default) or big.
width, poly, init, xorOut, reflectIn, reflectOut O Only for type="crc"; meaning as for <calc type="CRC">. On other types they are rejected.

from/to are needed as soon as the checksum does not cover everything before it — MAVLink computes from byte 1, leaving the start character out:

<frame size="5">
  <at index="0" match="(0xFE)"/>
  <checksum type="bytesum" from="1"/>
</frame>

Rejected at load time – each of these would otherwise leave a port silent: it accepts bytes, checks, discards, and never reports anything. Missing length source; size against minSize/maxSize; an <at> position outside the largest possible frame; a frame size above the window limit; type="crc" without poly; CRC attributes on other types; a misspelled byteOrder; adjust/size/byteOrder without length="total".

Modes

Many protocols switch their reading within one telegram: a header in raw bytes, then ASCII hex, with a text section in between. A <mode> bundles the rules that currently apply.

<decoder mode="asciihex" modeScope="stream">
  <mode name="asciihex" merge="ascii-hex" onInvalid="invalidinput">
    <sequence input="0x3A" tokentype="framestart"/>
    <sequence input="0x0D" tokentype="frameend"/>
    <sequence input="0x02" tokentype="specialvalue" enter="text"/>
  </mode>
  <mode name="text">
    <sequence input="0x03" tokentype="specialvalue" enter="asciihex"/>
  </mode>
</decoder>

merge – two bytes make one value

Value Meaning
none No merging (default).
ascii-hex Strict hex digits 0-9 A-F a-f. Anything else is invalid, see onInvalid.
ascii-hex-upper Like ascii-hex but upper case only. af are invalid.
low-nibble byte & 0x0F – the lower nibble, e.g. for streams whose values already came through <map>.
ascii-digit-legacy (byte - 0x30) & 0x0F. Decodes AF incorrectly. It exists only to reproduce existing protocols unchanged – never choose it for a new definition.

<after> – mode switch after a fixed length

Attribute M/O Meaning
bytes O* Number of wire bytes.
values O* Number of merged values (with ascii-hex that is two wire bytes each).
enter M Target mode.
apply O deferred (default) or immediate – see below.

* Exactly one of bytes/values must be set.

modeScope says how far a mode reaches:

  • frame (default): within one telegram only. At the frame boundary the mode falls back to the start mode. Right when a telegram always begins the same way.
  • stream: across frame boundaries. Needed when a switch (into a text section, say) still applies to the next telegram.

Not every combination is admissible. With frame, every mode needs a path back to the start mode – otherwise no token would ever be produced there. The engine checks this on load and rejects the definition rather than leaving the port silent.

apply on <after> says whether the mode ends with this byte (immediate) or after it (deferred, default). For classifying the next byte both are equivalent; the difference is whether a frame may be cut here.

Evaluation order

The order in the document is free – the evaluation is fixed:

1. dedup            drop the byte?
2. escape-mask      open or resolve an escape
3. sequence / when  control character or predicate, possibly a mode switch
4. map              only for bytes that 1-3 did not match
5. merge            only for bytes that 1-3 did not match
6. emit / collapse  token output
7. counted / after  counters

Two rules follow from this:

  • A control character is never translated or merged. map and merge apply exclusively to bytes that no rule in steps 1-3 matched.
  • A matched control character resets the merge cadence – by default. This is a property of the rule, not of the decoder: with resetMerge="no" a half nibble pair survives the match. Both forms occur in real protocols.
  • Inside a running <counted> region the length governs, not the byte. Sequences and predicates do not apply there – otherwise a payload byte that happens to look like a delimiter would tear open its own frame.

Collecting & look-ahead

<unify> – combine bytes

Collects the bytes read by the child elements and combines them into a value of type type. With name the result becomes referenceable (e.g. in a switch); without name it serves as an anonymous collector.

Attribute M/O Meaning
type M Target type (byte/word/dword/array/string).
name O Name of the result.
format O Display format. Without it a string is quoted; format="{0}" drops the quotes.
valuedescriptor O Value table for the unified value – a unified field is a field. With type="string" the table compares strings (value='"3"').
nonprintable O As for <string>; only with type="string".
visible O no/false hides the result in the output.
<unify type="array" name="Data">
  <group type="*">
    <lookahead distance="1" result="failed">
      <const type="byte" value="0x03" />   <!-- while not ETX -->
    </lookahead>
    <byte />
  </group>
</unify>

<lookahead> – speculative look-ahead

Checks whether a pattern matches starting at a certain distance, without consuming bytes. This allows reading, for example, variable data ranges up to a terminator.

Attribute M/O Values Meaning
distance M integer ≥ 1 Tokens from the read head, the tested one included: 1 tests the token under the head, 3 skips two and tests the third.
result M matched, failed (accepted = legacy alias of matched) Desired result: with matched the look-ahead succeeds when the children match; with failed when they do not match (negation).

The example under <unify> uses result="failed" to express "read bytes as long as the next byte is not the end byte (ETX)".

Careful with distance greater than 1 inside a repetition. A guard with distance="3" ends the repetition two tokens before the pattern – no matter whether the bytes in between belong to the pattern at all. If a checksum follows the payload, a checksum byte may happen to equal the pattern: the guard then fires in the middle of the payload, the field is cut short and the frame falls apart. And it fires from every position in its window: distance="3" spans two bytes, so a two-byte checksum is fully covered. Measured on the eComm interpreter: out of 600 telegrams with a brim-full text part, four failed – exactly the four whose CRC carried a 0x01 byte anywhere, i.e. roughly one in 128.

Inside a fixed-length region (<struct sizeRef="…">) the guard is unnecessary anyway: the size check already caps the repetition at the announced length – no field reads past it.

The guard in front of an optional field

The most common use: a field is sometimes present and sometimes not, followed by a fixed tail (checksum, line ending). Read forwards you cannot tell whether the characters under the read head belong to the optional field or already to the checksum. The guard looks past it:

<group type="?">
  <lookahead distance="3" result="failed"><const token="frameend" /></lookahead>
  <ascii-byte name="Mode" />
</group>
<ascii-byte name="Checksum" />
<const token="frameend" visible="false" />

If the line ending already sits at position 3, nothing but the checksum is left — so the group is skipped. Any number of such groups may follow one another; each one decides for itself.

Two traps, both silent.

  1. The token type must match. <const type="byte" value="0x0D" /> will never match a frameend token — the engine compares the type first. The guard would never fire, the group would eat the checksum, and the frame would vanish without a trace. Use <const token="frameend" />.
  2. distance counts the tested token. For a two-character field in front of a two-character checksum the answer is 3, not 2. A field of n characters additionally needs the guards 5, 7, … n+1, or it misfires on a partial remainder.

Output

<match>

Produces the visible frame label. There are two mutually exclusive ways to determine the label:

  • label – a static text.
  • format – a .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.
error O false true: marks the frame as faulty (FrameError), so the UI colours it accordingly. Meant for catch-all messages: an alternative that deliberately accepts an unusable frame so it becomes fully visible. A frame the grammar rejects outright is reported too — but only with the bytes the engine discarded while resetting, and not at all while the parser is still waiting for more input. The catch-all branch stakes out the frame boundaries itself and reports the bytes that were on the wire. Before this, only <const validate="true"> could set the flag, and only as a side effect of a value comparison.

Limitation: the mark is a parser-level pending flag, consumed only when the frame completes. If the <match error='true'/> sits in a branch that later fails while a different branch succeeds, the successful frame carries the mark. Harmless in practice as long as the error match is the last child behind the closing token — which is its natural place.

A catch-all branch that keeps broken frames visible:

<group type="OR">
  <struct ref="Frame body" visible="false" />
  <struct ref="Unreadable frame" visible="false" />
</group>
...
<struct name="Unreadable frame" visible="false">
  <group type="*">
    <lookahead distance="1" result="failed"><const token="frameend" /></lookahead>
    <byte visible="false" />
  </group>
  <const token="frameend" visible="false" />
  <match label="Frame Error!" error="true" />
</struct>

Do not use 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 A source instead of a field – see the table below.
useRawValue O true: use the raw field value instead of the value-table plain text.

value knows three sources; anything else is rejected at load time (a typo would otherwise leave an empty placeholder in every label):

value Meaning
label the label of the frame as built so far
frametext the printable dump of the frame: control characters as <SOH>, printable ASCII as characters, anything above as <XX>
framehex the same dump in hex ("41 42 "), for binary protocols

The dump ends at the read head – at the bytes consumed up to this <match>. That is the capability, not its limit: it lets you print a prefix and format the rest differently.

<!-- ESPA: <SOH>1<STX>1<US>1234<ETX><7B>
     The dump comes BEFORE the checksum field so the checksum shows up as a
     hex pair rather than as a character. -->
<const type="byte" value="0x03" name="ETX" visible="no" />
<match format="{0}"><param value="frametext" /></match>

<byte name="Checksum" format="{0:X2}" visible="no" />
<match format="{0}&lt;{1}&gt;">
  <param value="label" />
  <param ref="Checksum" />
</match>
<byte name="Address" format="{0}" />
<byte name="Status"  valuedescriptor="status-descriptor" />
<match format="Adr {0} = {1}">
  <param ref="Address" />
  <param ref="Status" />
</match>

Value tables (enums, bitfields, flags)

<valuedescriptor> / <descriptor>

Maps values to plain text. type determines the kind of mapping.

<valuedescriptor> attribute M/O Meaning
type M byte/word/dword (value mapping), string/array (text mapping), bitfield, flags.
name M Identifier (for valuedescriptor="…" on fields).
hideWhenEmpty O type='flags' only: if no bit is set, the referencing field's description is suppressed entirely (behaves like visible='false'). Without the attribute the field stays visible even for value 0. Default: false.
render O inline (default): the matching descriptor's name is appended in parentheses — Mode: 0 (without). child: the meaning becomes a sub-description with the same offset and length — Mode: 0 with the child without protocol extension.
<descriptor> attribute M/O Meaning
value O Value to match.
mask O Bitmask (for bitfield/flags).
name M internal identifier.
label O displayed text.
valuedescriptor O recursive reference to another table.
default O yes: applies when no other descriptor matches — the default: branch. No value.

Simple enum:

<valuedescriptor type="byte" name="status-descriptor">
  <descriptor value="0x00" name="OK"    label="Ready" />
  <descriptor value="0x01" name="Busy"  label="Busy" />
  <descriptor value="0xFF" name="Fault" label="Fault" />
</valuedescriptor>

Flags/bitfield (mask instead of single value):

<valuedescriptor type="flags" name="alarm-flags">
  <descriptor mask="0x01" name="FEUER"   label="Fire alarm" />
  <descriptor mask="0x02" name="STOERUNG" label="Fault" />
  <descriptor mask="0x04" name="ABSCHALT" label="Shutdown" />
</valuedescriptor>

render and default apply to type='flags' as well: render="child" puts the resolved bits on their own line below the raw value instead of in parentheses behind it, and a <descriptor default="yes" name="0" /> supplies the text for "no bit set". Together they reproduce what Enum.ToString() does on a [Flags] enum — the shape hand-written interpreters use for their status words.

Order: the engine emits matches in reverse document order. To reproduce the ascending order of Enum.ToString(), write the highest bit first.

Hide empty flag fields with hideWhenEmpty (type='flags' only): bytes with no bit set then produce no description line at all. Handy for long bitmaps (e.g. a Consolidated Heartbeat) where only the set addresses matter:

<valuedescriptor type="flags" name="sys-flags-0" hideWhenEmpty="true">
  <descriptor value="0x01" name="System 0" />
  <descriptor value="0x02" name="System 1" />
  <!-- ... -->
</valuedescriptor>

For value 0x00 the field's line is dropped entirely; for 0x06 it shows Systeme 0-7: 6 (System 1, System 2).

Wide bitfields: bits instead of a mask

A mask goes through the number parser and stops at 32 bits there. For bitfields that reach further or straddle byte boundaries, use the bit position:

<!-- CRSF/S.BUS: 16 channels of 11 bits, packed across 22 bytes -->
<valuedescriptor type="bitfield" name="rc-channels">
  <descriptor name="Channel 1" bits="0..10" />
  <descriptor name="Channel 2" bits="11..21" />
  <descriptor name="Channel 3" bits="22..32" />
  <!-- ... -->
</valuedescriptor>
Notation Meaning
bits="11..21" Bits 11 through 21 inclusive.
bits="7" Exactly bit 7.
  • Bit 0 is the least significant bit of the value. With bigendian="yes" the field has already reversed its bytes — counting happens on the value, not in wire order.
  • At most 64 bits wide; beyond that the value could no longer be displayed, and silent truncation would be worse than an error.
  • Only under type="bitfield" or type="flags". Under a value table the descriptor value is a comparison value, not a mask; bits="0..2" would silently mean "value == 7" there.
  • bits and mask/value are mutually exclusive.

Match patterns (pattern matching)

The attributes match (on <field>, <const>) and match (on <case>) allow patterns instead of fixed single values. Optionally, ~ introduces a negation.

Character sets [ … ]

For character-based matching, with ranges via -:

[0-9]            a decimal digit
[0-9a-fA-F]      a hex digit
~[ \t\r\n]       any byte except whitespace

Value sets ( … )

Comma-separated values and ranges (hex or decimal):

(0x02)               only 0x02
(0x41,0x43)          0x41 or 0x43
(0x10-0x1F)          range 0x10 .. 0x1F
(0x01-0x10,0x20)     mixed: range plus single value
~(0x00)              anything except 0x00

Examples in use:

<field match="[0-9]" name="Digit" />
<const match="(0x0A,0x0D)" />
<case  match="(0x10-0x1F)"> ... </case>
<case  match="~(0xFF)">     ... </case>

Static vs. dynamic attributes (…Ref)

Several attributes exist in a static and a "reference" variant. The reference variant resolves the value at parse time from another field:

Static Dynamic Effect
size="8" sizeRef="LenField" fixed vs. length read from a field
maxsize="32" maxsizeRef="MaxField" fixed vs. dynamic upper bound
name="X" nameRef="NameField" fixed vs. dynamic field name

This is how, for example, a length-prefixed payload field is expressed:

<byte  name="Length" />
<array sizeRef="Length" name="Payload" />

Patterns & recipes

Recurring tasks and their idiomatic TrIML solution.

Frame delimiters with STX/ETX

The standard case of a frame-oriented protocol: expecting fixed start/end bytes as literal constants (no decoder required).

<const type="byte" value="0x02" name="STX" />
<!-- content -->
<const type="byte" value="0x03" name="ETX" />

If the stream needs to re-synchronize robustly in the middle of a frame, the boundary bytes can instead be turned into framestart/frameend tokens via an <esc-decoder> and matched with <const token="…"> — see Tokens and decoders and the <esc-decoder> section of the element reference.

Length-prefixed payload

A length byte, followed by exactly that many data bytes — via sizeRef:

<byte  name="Length" />
<array sizeRef="Length" name="Payload" />

Compute and validate a checksum

calc produces the expected value, const ref=… validate="true" compares it with the transmitted byte. On mismatch the frame is flagged as faulty, but parsing continues:

<calc  name="ExpectedCRC" type="ByteXor" from="0" to="-1" resultType="byte" />
<const ref="ExpectedCRC" validate="true" name="Checksum" />

Frame-type-dependent structure (switch)

A type byte determines the further layout:

<byte name="Type" valuedescriptor="type-descriptor" />
<switch ref="Type">
  <case value="0x01"><word name="Status" /></case>
  <case value="0x02"><array size="6" name="Data" /></case>
  <default><match label="Unknown" /></default>
</switch>

Multiple frame forms (group type="OR")

When a stream contains different frame forms that cannot be distinguished by a single type byte (e.g. a short polling byte vs. a full frame), OR alternatives separate the cases. The first matching <struct> wins — discrimination is often done via a match pattern on the first field:

<group type="OR">
  <struct>
    <const type="byte" value="0x02" name="SOT" />
    <byte name="Length" /> ...
  </struct>
  <struct>
    <byte match="(0xA0,0xB0)" name="Polling" />
    <match label="Polling" />
  </struct>
</group>

Variable-length data up to a terminator (unify + lookahead)

"Read bytes as long as the next byte is not the end byte" — the typical solution for payload without a length field. The lookahead inspects the next byte without consuming it:

<unify type="array" name="Data">
  <group type="*">
    <lookahead distance="1" result="failed">
      <const type="byte" value="0x03" />   <!-- while not ETX -->
    </lookahead>
    <byte />
  </group>
</unify>

Split a byte into bit groups (var + mask)

Read the value once, hidden, then expose it multiple times, masked:

<var   type="byte" name="ctrl" />
<field ref="ctrl" mask="0x07" name="Mode" />
<field ref="ctrl" mask="0x38" name="Channel" />
<field ref="ctrl" mask="0xC0" name="Priority" />

Flags as plain text (bitfield table)

<valuedescriptor type="flags" name="alarm-flags">
  <descriptor mask="0x01" name="FEUER"    label="Fire alarm" />
  <descriptor mask="0x02" name="STOERUNG" label="Fault" />
</valuedescriptor>
...
<byte name="Alarms" valuedescriptor="alarm-flags" />

ASCII-encoded numbers

Protocols that transmit numbers as text (e.g. "0235") are read with the ASCII field types:

<ascii-word   name="Temperature" />    <!-- "0235" -> 235 -->
<ascii-digits size="3" name="Channel" /> <!-- exactly 3 digits -->

Build the label incrementally (add-ref / add-label)

Instead of a single match at the end, the label can be extended field by field. add-ref appends a field value, add-label a fixed text — both implicitly set append="true":

<byte name="Receiver" format="{0:X2}" />
<match add-ref="Receiver" />
<byte name="Sender" format="{0:X2}" />
<match add-ref="Sender" />
...
<const type="byte" value="0x03" name="End" />
<match add-label="&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="FrameOriented" frameGapUs="4000" />
  <definition>

    <valuedescriptor type="byte" name="funccode">
      <descriptor value="0x01" name="ReadCoils"         label="Read Coils" />
      <descriptor value="0x03" name="ReadHoldingRegs"   label="Read Holding Registers" />
      <descriptor value="0x06" name="WriteSingleReg"    label="Write Single Register" />
      <descriptor value="0x10" name="WriteMultipleRegs" label="Write Multiple Registers" />
    </valuedescriptor>

    <message>
      <byte name="Address"  format="{0}" />
      <byte name="Function" valuedescriptor="funccode" />

      <switch ref="Function">
        <case value="0x03">                    <!-- Read Holding Registers (request) -->
          <word name="StartAddress" bigendian="yes" format="0x{0:X4}" />
          <word name="Count"        bigendian="yes" />
        </case>
        <case value="0x06">                    <!-- Write Single Register -->
          <word name="Register" bigendian="yes" format="0x{0:X4}" />
          <word name="Value"    bigendian="yes" format="0x{0:X4}" />
        </case>
        <default>
          <!-- add further function codes here -->
        </default>
      </switch>

      <word name="CRC" format="0x{0:X4}" />    <!-- CRC-16/Modbus, little-endian -->

      <match format="Modbus  Addr {0}  {1}">
        <param ref="Address" />
        <param ref="Function" />
      </match>
    </message>

  </definition>
</Trace-Interpreter-Definition>

Practical notes on Modbus RTU:

  • Framing by timing: Since there are no frame bytes, clean telegram separation depends on the transmission pause between frames.
  • Request vs. response: The same function code has different layouts in request and response. If both directions are on one port, you need additional logic (e.g. separate ports/interpreters per direction).
  • CRC-16/Modbus cannot be checked with the built-in 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

ByteSum, ByteXor, LRC, WordSum and the parameterised CRC – all without code, also in the runtime XML interpreter. function="name" references a function registered on the server and exists for the bundled system interpreters; new definitions do not need it.

Does <decoder> require code?

No. It is fully declarative and comes in two mutually exclusive shapes:

  • Rule chain<sequence>, <escape-mask>, <when>, <map>, <merge>, <counted> and <mode>. For protocols with delimiters, escapes and shifting readings. Every byte is classified once and for good.
  • Window<frame>. For protocols without delimiters, where a frame is only recognisable because the header fields are plausible and the checksum matches. The decoder buffers, checks, and rewinds on failure.

<esc-decoder> is a legacy name for the rule chain and builds the same engine.

A <decoder name="X"/> without children is the one remaining code reference: it points at an implementation hooked in via RegisterDecoder. That path is legacy and goes away with the migration.

Note: XSD defaults vs. engine behavior

The XSD lists default values for some attributes that the engine does not always handle identically. The engine behavior is authoritative. Verified points:

  • match/@usespace – 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.