TrIML – Trace Interpreter Markup Language

TrIML ist die XML-Sprache, mit der in Trace Protokoll-Interpreter beschrieben werden. Ein Interpreter liest einen Byte-Strom (z.B. von einer seriellen Schnittstelle), erkennt darin Telegramme (Frames) eines bestimmten Protokolls und liefert sie als strukturierte, benannte Felder zurück – inklusive Klartext- Labels für Werte (Enums), Prüfsummen-Validierung und verschachtelte Strukturen.

Der große Vorteil: Ein neues Protokoll lässt sich rein deklarativ in XML beschreiben – ohne eine Zeile C#-Code. Die Auswertung übernimmt der gemeinsame XmlMessageParser (Engine), der die XML-Definition zur Laufzeit interpretiert.

Wer braucht diese Doku? Jeder, der ein eigenes Protokoll in Trace einlesbar machen will, ohne den Server-Code anzufassen. Die Referenz beschreibt jedes XML-Element und -Attribut; das Tutorial führt am konkreten Beispiel durch den Aufbau eines Interpreters.

Was ein Interpreter tut

Der Parser arbeitet als rekursiver Top-Down-Matcher über den Byte-Strom:

  1. Bytes werden Stück für Stück in den Parser gegeben.
  2. Der Parser versucht, die in der XML beschriebene Message-Struktur auf den Strom zu legen (Felder konsumieren Bytes, Konstanten erwarten exakte Werte, Verzweigungen wählen je nach gelesenem Wert einen Pfad).
  3. Passt die Struktur vollständig, entsteht ein Match – ein erkanntes Telegramm mit Label und hierarchischen Feld-Beschreibungen.
  4. Passt sie nicht, wird zurückgesetzt (Backtracking) und der nächste mögliche Telegramm-Start probiert.

Frame-orientiert vs. Byte-orientiert

Im Wurzelelement legt das type-Attribut den Interpretations-Modus fest:

Modus Bedeutung
FrameOriented Das Protokoll hat klar abgegrenzte Telegramme mit Start-/End-Markierung (z.B. STX … ETX). Der häufigste Fall.
ByteOriented Es gibt keine feste Frame-Begrenzung; der Strom wird fortlaufend gedeutet.

Tokens und Decoder

Der Parser arbeitet nicht direkt auf den Roh-Bytes, sondern auf einem Token-Strom. Standardmäßig – ohne eigenen Decoder – wird jedes Byte zu einem NormalInput-Token, das seinen Wert trägt. Felder wie <byte>, <word> und literale <const value="…"> konsumieren genau solche NormalInput-Tokens. Das ist der Normalfall und braucht keinen Decoder.

Daneben kennt die Engine Sonder-Tokens, die eine Bedeutung statt nur einen Wert transportieren:

Token Bedeutung
framestart Anfang eines Telegramms (expliziter Re-Sync-Punkt)
frameend Ende eines Telegramms
specialvalue protokollspezifisches Sonderzeichen
eof Ende des Eingabestroms
invalid ungültiges Eingabezeichen

Diese Sonder-Tokens entstehen nicht von selbst – sie werden von einem Decoder aus dem Byte-Strom erzeugt. Ein <const token="frameend" /> matcht ein solches Token; ohne einen Decoder, der frameend-Tokens produziert, trifft es nie zu.

Daraus folgen zwei Wege, Telegrammgrenzen zu behandeln:

  1. Als literale Bytes – der einfachste Weg, kein Decoder nötig:

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

    Funktioniert sofort, auch im reinen Laufzeit-XML-Interpreter. Für den Einstieg und die meisten Protokolle ist das die richtige Wahl.

  2. Als Frame-Tokens über einen <esc-decoder> – wenn der Strom mitten im Telegramm beginnen kann und sauber re-synchronisieren soll, oder wenn Escape-/Maskierungslogik nötig ist. Der Decoder ordnet den Grenzbytes Token-Typen zu; anschließend matchen <const token="…"> diese 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" />
    

    Der <esc-decoder> ist rein deklarativ und funktioniert ebenfalls ohne Code (Details in der Element-Referenz, Abschnitt <esc-decoder>).

Faustregel: Beginne mit literalen Bytes (Weg 1). Greife zu Frame-Tokens (Weg 2) erst, wenn du robustes Re-Sync oder Escape-Logik wirklich brauchst.

Das Grundgerüst einer TrIML-Datei

Jede Interpreter-Definition hat denselben äußeren Rahmen:

<?xml version="1.0" encoding="utf-8"?>
<Trace-Interpreter-Definition>

  <interpreter name="Mein-Interpreter" type="FrameOriented" />

  <definition>

    <!-- Optional: globale Wert-Tabellen (Enums) -->
    <valuedescriptor type="byte" name="typ-descriptor">
      <descriptor value="0x01" name="Status"  label="Statusmeldung" />
      <descriptor value="0x02" name="Command" label="Kommando" />
    </valuedescriptor>

    <!-- Optional: wiederverwendbare Strukturen, Decoder … -->

    <!-- Pflicht: mindestens eine Message -->
    <message>
      <const  type="byte" value="0x02" name="STX" />
      <byte   name="Typ" valuedescriptor="typ-descriptor" />
      <byte   name="Länge" />
      <array  sizeRef="Länge" name="Nutzdaten" />
      <const  type="byte" value="0x03" name="ETX" />
      <match  label="Telegramm" />
    </message>

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

Die zwei Pflicht-Bausteine direkt unter der Wurzel:

  • <interpreter> – Metadaten: Name und Modus.
  • <definition> – enthält alle globalen Definitionen (Wert-Tabellen, Strukturen, Decoder) und mindestens eine <message> mit der eigentlichen Telegramm-Beschreibung.

Wie ein Interpreter geladen wird

Du arbeitest ausschließlich mit der XML-Definition – es gibt nichts zu kompilieren und keine DLL zu bauen. Trace erzeugt aus deiner XML zur Laufzeit eine Interpreter-Instanz, die du einem Port zuweisen kannst.

Zwei Wege:

  • Über die TraceUI (empfohlen): Der Interpreter-Assistent bietet einen XML-Editor mit Live-Vorschau. Du schreibst die Definition, lässt sie gegen die aktuell sichtbaren Bytes laufen und siehst sofort, welche Telegramme erkannt werden. Passt es, wird der Interpreter angelegt und kann einem Port zugewiesen werden.
  • Über die REST-API: Die XML wird per Request übergeben, der Server legt einen ephemeren Interpreter an (lebt im laufenden Prozess):
POST /api/interpreters/from-xml
{ "name": "MiniBus", "xml": "<Trace-Interpreter-Definition> … </Trace-Interpreter-Definition>" }

Der name muss mit dem interpreter/@name-Attribut in der XML übereinstimmen.

Live-Test ohne Risiko: POST /api/interpreters/dryrun mit { "xml": …, "bytes": [ … ] } parst die Bytes mit einer frischen Instanz und liefert die erkannten Matches zurück – ohne den Server-Zustand zu verändern. Genau das nutzt der Assistent für seine Vorschau, und es eignet sich für automatisierte Tests deiner Sample-Vektoren.

Weitere Endpunkte: GET /api/interpreters (Liste), GET /api/interpreters/{name} (XML zurücklesen), PUT /api/interpreters/{name} (XML ersetzen – die laufenden Ports werden heiß umgeschaltet), DELETE /api/interpreters/{name}.

Rein deklarativ: Alle in dieser Referenz beschriebenen Konstrukte – Felder, switch, group, unify, lookahead, Wert-Tabellen, <esc-decoder> und die eingebauten calc-Funktionen (ByteSum/ByteXor) – funktionieren im Laufzeit-XML-Interpreter ohne jeglichen Code. Nur zwei fortgeschrittene Konstrukte erfordern registrierten C#-Code und stehen daher im reinen XML-Workflow nicht zur Verfügung: der code-basierte <decoder> und custom calc function="…". Für die allermeisten Protokolle braucht man sie nicht.

Weiter geht's

  • Tutorial – einen kleinen Interpreter von Grund auf bauen.
  • Element-Referenz – jedes Tag und Attribut im Detail.
  • Muster & Rezepte – Prüfsummen, variable Länge, Bitfelder, ASCII-Zahlen.
  • Beispiele – vollständige Definitionen für öffentliche Protokolle (Modbus RTU, NMEA 0183).

Tutorial: Ein Interpreter in 7 Schritten

Wir bauen Schritt für Schritt einen Interpreter für ein kleines, erfundenes Protokoll – ein typisches Status-Telegramm, wie es in der Gebäude- oder Anlagentechnik vorkommt. Jeder Schritt führt genau ein neues TrIML-Konzept ein.

Unser Beispiel-Protokoll „MiniBus":

 STX  Typ  Adr(2 B)  Länge  Nutzdaten(Länge B)  Prüfsumme  ETX
 0x02  ..   .. ..      ..       ...                 ..      0x03
  • STX = 0x02, ETX = 0x03 begrenzen das Telegramm.
  • Typ ist ein Byte mit fester Bedeutung (Status / Kommando / Fehler).
  • Adr ist eine 16-Bit-Adresse (big-endian).
  • Länge gibt die Anzahl der folgenden Nutzdaten-Bytes an.
  • Prüfsumme ist die XOR-Summe über Typ..Nutzdaten.

Schritt 1 – Das Grundgerüst

Jede Definition beginnt mit Wurzel, <interpreter> und <definition> samt einer <message>:

<?xml version="1.0" encoding="utf-8"?>
<Trace-Interpreter-Definition>
  <interpreter name="MiniBus" type="FrameOriented" />
  <definition>
    <message>
      <!-- folgt -->
    </message>
  </definition>
</Trace-Interpreter-Definition>

Schritt 2 – Rahmen erkennen (const)

Das Telegramm beginnt mit STX (0x02) und endet mit ETX (0x03). Beides sind feste Bytes, die wir als literale Konstanten erwarten:

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

Ein literales <const value="…"> matcht genau dieses Byte. Das genügt zur Telegramm-Erkennung und kommt ohne Decoder aus.

Optional: Wer expliziten Re-Sync braucht (der Strom kann mitten im Telegramm beginnen), kann STX/ETX über einen <esc-decoder> zu framestart/frameend- Tokens machen und mit <const token="…"> darauf matchen – siehe Tokens und Decoder.

Schritt 3 – Feste Felder lesen (byte, word, bigendian)

Zwischen den Rahmen kommen die Datenfelder. Ein einzelnes Byte liest <byte>, ein 16-Bit-Wort <word>. Da die Adresse big-endian übertragen wird, setzen wir bigendian="yes":

<const type="byte" value="0x02" name="STX" />

<byte name="Typ" />
<word name="Adresse" bigendian="yes" format="0x{0:X4}" />

<const type="byte" value="0x03" name="ETX" />

Das format-Attribut bestimmt die Darstellung des Werts in der UI – hier als 4-stelliger Hex-Wert (format ist ein .NET-String.Format-Muster, der Wert landet auf {0}).

Schritt 4 – Werte benennen (valuedescriptor)

Typ ist ein Aufzählungswert. Statt einer nackten Zahl wollen wir Klartext. Dazu definieren wir oben in der <definition> eine Wert-Tabelle und verweisen am Feld darauf:

<definition>

  <valuedescriptor type="byte" name="typ-descriptor">
    <descriptor value="0x01" name="Status"  label="Statusmeldung" />
    <descriptor value="0x02" name="Command" label="Kommando" />
    <descriptor value="0x03" name="Error"   label="Fehlermeldung" />
  </valuedescriptor>

  <message>
    <const type="byte" value="0x02" name="STX" />
    <byte  name="Typ" valuedescriptor="typ-descriptor" />
    <word  name="Adresse" bigendian="yes" format="0x{0:X4}" />
    ...

Liest der Parser für Typ den Wert 0x02, zeigt die UI „Kommando" an.

Schritt 5 – Variable Länge (sizeRef)

Das Länge-Byte bestimmt, wie viele Nutzdaten-Bytes folgen. Genau dafür gibt es sizeRef: ein <array>, dessen Größe zur Laufzeit aus einem zuvor gelesenen Feld kommt:

<byte  name="Länge" />
<array sizeRef="Länge" name="Nutzdaten" />

sizeRef="Länge" referenziert das Feld per name. (Wäre die Länge fix, stünde stattdessen size="8" direkt da.)

Schritt 6 – Prüfsumme berechnen und prüfen (calc + validate)

Die Prüfsumme ist die XOR-Summe über die Bytes von Typ bis Nutzdaten. Wir berechnen sie mit <calc> und vergleichen sie anschließend mit dem übertragenen Byte. from/to sind Byte-Offsets im Telegramm (0 = erstes Byte nach dem Frame-Start; die Engine zählt ab dem ersten konsumierten Inhalts-Byte):

<byte  name="Länge" />
<array sizeRef="Länge" name="Nutzdaten" />

<!-- XOR über Typ(0) .. Ende der Nutzdaten -->
<calc name="Soll-Prüfsumme" type="ByteXor" from="0" to="-1" resultType="byte" />

<!-- erwartet das nächste Byte == berechneter Wert; bei Abweichung Frame-Fehler -->
<const ref="Soll-Prüfsumme" validate="true" name="Prüfsumme" />

validate="true" ist hier entscheidend: Stimmt die Prüfsumme nicht, wird das Telegramm als fehlerhaft markiert (im Stream sichtbar), der Parse-Vorgang läuft aber weiter. Ohne validate würde eine Abweichung den Pfad komplett verwerfen.

Hinweis zu to: Endgrenzen können je nach Engine-Version absolut oder relativ (negativ = vom aktuellen Ende) angegeben werden. Prüfe das Ergebnis im Zweifel mit dem dryrun-Endpunkt gegen reale Daten.

Schritt 7 – Ausgabe formen (match)

Zum Schluss erzeugt <match> das sichtbare Telegramm-Label. Statischer Text und Feldwerte stehen gemeinsam im format-String; die <param>-Kinder liefern die Werte für {0}, {1}, … – hier die in Schritt 3/4 definierten Felder Typ und Adresse:

<match format="MiniBus {0} an {1}">
  <param ref="Typ" />       <!-- {0} -->
  <param ref="Adresse" />   <!-- {1} -->
</match>

Ergebnis in der Frame-Liste z.B.: MiniBus Kommando an 0x00A3 (Typ wird über seine Wert-Tabelle zu „Kommando" aufgelöst).

label oder format – nicht beides. Sind beide an einem <match> gesetzt, gewinnt format und das label wird verworfen. Statischen Text deshalb direkt in den format-String schreiben.

Die vollständige Definition

<?xml version="1.0" encoding="utf-8"?>
<Trace-Interpreter-Definition>
  <interpreter name="MiniBus" type="FrameOriented" />
  <definition>

    <valuedescriptor type="byte" name="typ-descriptor">
      <descriptor value="0x01" name="Status"  label="Statusmeldung" />
      <descriptor value="0x02" name="Command" label="Kommando" />
      <descriptor value="0x03" name="Error"   label="Fehlermeldung" />
    </valuedescriptor>

    <message>
      <const type="byte" value="0x02" name="STX" />
      <byte  name="Typ"      valuedescriptor="typ-descriptor" />
      <word  name="Adresse"  bigendian="yes" format="0x{0:X4}" />
      <byte  name="Länge" />
      <array sizeRef="Länge" name="Nutzdaten" />
      <calc  name="Soll-Prüfsumme" type="ByteXor" from="0" to="-1" resultType="byte" />
      <const ref="Soll-Prüfsumme" validate="true" name="Prüfsumme" />
      <const type="byte" value="0x03" name="ETX" />
      <match format="MiniBus {0} an {1}">
        <param ref="Typ" />
        <param ref="Adresse" />
      </match>
    </message>

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

Mehr Konstrukte – Verzweigungen (switch/case), Wiederholungen (group), Bitfelder, ASCII-Zahlen und eigene Decoder – findest du in der Element-Referenz und unter Muster & Rezepte.

Element-Referenz

Vollständige Referenz aller TrIML-Elemente und ihrer Attribute. Die beschriebenen Bedeutungen entsprechen dem Verhalten des XmlMessageParser (Engine); wo XSD und Engine abweichen, gilt das hier dokumentierte Engine-Verhalten.

Schreibweise der Attribut-Tabellen: P = Pflicht, O = optional. „Default" nennt den Wert, den die Engine annimmt, wenn das Attribut fehlt.

Dokumentstruktur

<Trace-Interpreter-Definition>

Wurzelelement. Enthält genau ein <interpreter> und genau ein <definition>.

<interpreter>

Metadaten des Interpreters. Wird nicht zur Parse-Zeit ausgewertet, sondern bei der Registrierung gelesen.

Attribut P/O Werte Bedeutung
name P beliebig Anzeigename des Interpreters.
type P FrameOriented, ByteOriented Interpretations-Modus (siehe Überblick).
frameGapUs O Ganzzahl (µs) Timing-basiertes Framing für delimiter-lose Protokolle (siehe unten). 0/fehlt = aus.

Timing-basiertes Framing (frameGapUs)

Manche Protokolle (z.B. Modbus RTU) haben keine Delimiter-Bytes – Frames werden nur durch Sendepausen auf der Leitung getrennt (≥ 3,5 Zeichenzeiten). Mit frameGapUs misst die Engine die Lücke zwischen aufeinanderfolgenden Bytes aus dem monotonen Mikrosekunden-Tick, den jedes Byte trägt (ByteInfo.Tick), und injiziert bei Erreichen der Schwelle einen framestart-Token. Die Grammatik verankert jeden Frame mit einem führenden <const token="framestart"/>; das allererste Byte des Stroms startet immer einen Frame.

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

Auflösungs-Vorbehalt. Echtes Sub-Millisekunden-Gap-Framing erfordert Capture-Hardware, die pro Byte in Mikrosekunden stempelt. Handelsübliche USB-Serial-Adapter liefern Bytes im Batch – innerhalb eines Batches geht das Per-Byte-Timing verloren, der praktische Boden bleibt bei ~1 ms (deckt Modbus RTU bis ~38400 Baud ab). Für höhere Raten ist strukturelles Framing + CRC-Resync die robuste Ergänzung.

Letztes Frame bei stiller Leitung. Da ein Frame erst beim nächsten framestart (= nächste Pause) schließt, würde das letzte Frame vor einer Bus-Stille auf das nächste Telegramm warten. Der Idle-Flush der Laufzeit löst das: nachdem die Leitung frameGapUs still war, injiziert er einen synthetischen framestart, sodass das letzte Frame sauber (CRC validiert) ohne Folge-Byte schließt. Das gilt nur für gap-gefra​mte Interpreter; length-prefixed und delimiter-gefra​mte werden nie zwangsgeflusht (ein legitim pausierendes Frame darf nicht abgeschnitten werden).

<definition>

Container für alle Definitionen. Direkte Kinder: <valuedescriptor>, <struct> (wiederverwendbar), <decoder>, <esc-decoder> und mindestens eine <message>. Per name benannte Elemente in der <definition> sind über ref referenzierbar.

<message>

Einstiegspunkt des Parsings – die Top-Level-Telegrammstruktur. Darf alle Inhalts-Elemente (Felder, Konstanten, Gruppen, Switch, …) enthalten.

Attribut P/O Bedeutung
name O Bezeichner der Nachricht.
visible O Sichtbarkeit in der Ausgabe (yes/no).

Feldtypen

Felder konsumieren Bytes aus dem Strom. Es gibt zwei austauschbare Schreibweisen: das generische <field type="…"> und die Kurzformen <byte>, <word>, <dword>, <array>, <string>, <var>.

<byte>, <word>, <dword>

Ganzzahlen mit 8, 16 bzw. 32 Bit. word/dword sind standardmäßig little-endian; mit bigendian="yes" big-endian.

Attribut P/O Default Bedeutung
name O Feldname (für Labels und ref/sizeRef/switch ref).
nameRef O Feldname dynamisch aus einem anderen (String-)Feld übernehmen.
bigendian O no yes/true → big-endian (nur word/dword).
format O .NET-String.Format-Muster für die Anzeige, Wert auf {0}.
valuedescriptor O Verweis auf eine Wert-Tabelle (Klartext für den Wert).
mask O Bitmaske, die vor Anzeige/Vergleich auf den Wert angewandt wird.
scale O Affine Skalierung: angezeigter Wert = Roh · scale + offset. Erlaubt eine Dezimalzahl oder einen Bruch wie 100/255.
offset O 0 Additiver Offset zu scale (z. B. -40 für °C).
unit O Einheit, die hinter den angezeigten Wert gehängt wird (z. B. rpm, km/h).
match O Validierungs-Muster, siehe Match-Muster.
visible O yes no/false blendet das Feld in der Ausgabe aus.
<word name="Adresse" bigendian="yes" format="0x{0:X4}" />
<byte name="Status" valuedescriptor="status-descriptor" />

Affine Skalierung macht aus Rohzahlen physikalische Werte. Der angezeigte Wert ist Roh · scale + offset, optional mit unit. scale darf ein Bruch sein (100/255). Geparst wird kultur-invariant (. als Dezimaltrenner). Beispiel (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>

Feste oder variabel lange Byte-Sequenz.

Attribut P/O Bedeutung
size bedingt Feste Länge in Bytes.
sizeRef bedingt Länge dynamisch aus einem zuvor gelesenen Feld (per name).
name, format, visible, valuedescriptor O wie bei <byte>.

Genau eines von size/sizeRef ist anzugeben.

<array size="4"        name="Seriennummer" />
<array sizeRef="Länge" name="Nutzdaten" />

<string>

Text-Feld. Mit size fester Länge, ohne size bis zum Null-Terminator.

Attribut P/O Bedeutung
size O Feste Byte-Länge; fehlt sie, wird bis 0x00 gelesen.
sizeRef O Länge dynamisch aus einem Feld.
encoding O Encoding-Name, via Encoding.GetEncoding(name) aufgelöst (z.B. utf-8, iso-8859-1, ascii).
name, format, visible O wie bei <byte>.
<string size="16" encoding="iso-8859-1" name="Gerätename" />
<string name="Text" />              <!-- null-terminiert -->

<field>

Generische Schreibweise; type wählt den konkreten Feldtyp. Alle Attribute der jeweiligen Kurzform gelten entsprechend.

Attribut P/O Default Bedeutung
type O byte byte, word, dword, array, string, var, ascii-byte, ascii-word, ascii-dword, ascii-digits.
ref O Verweist auf ein zuvor definiertes Feld/var/const und übernimmt es (mit überschreibbaren Attributen).
<field type="dword" name="Zeitstempel" bigendian="yes" />

<var> – verstecktes Feld

Wie <field>, aber standardmäßig visible="false". Typische Verwendung: einen Wert einmal lesen und über mehrere <field ref="…" mask="…"> in Bitgruppen zerlegen.

<var type="byte" name="flags" />
<field ref="flags" mask="0x0F" name="Untere 4 Bit" />
<field ref="flags" mask="0xF0" name="Obere 4 Bit"  format="{0:X1}" />

Mit einem value-Attribut wird <var> zum zero-width Literal-Halter: es liest keine Bytes von der Leitung, sondern trägt einen festen, per Name referenzierbaren Wert (dezimal oder 0x…). Damit lässt sich eine per-Message- Konstante in eine gemeinsame Berechnung einspeisen – z.B. MAVLinks CRC_EXTRA, das ein geteilter <calc … appendValueRef="…"> aufgreift (siehe <calc>).

<var value="50" name="CrcExtra" />   <!-- nicht auf der Leitung, per Name referenzierbar -->

ASCII-Zahlenfelder

<ascii-byte>, <ascii-word>, <ascii-dword> lesen eine als ASCII-Ziffern geschriebene Zahl und liefern den numerischen Wert. <ascii-digits> liest eine reine Ziffernfolge fester Länge und erfordert size.

<ascii-word name="Temperatur" />          <!-- z.B. "0235" -> 235 -->
<ascii-digits size="3" name="Kanal" />    <!-- genau 3 Ziffern -->

Konstanten

<const>

Erwartet entweder einen festen Byte-Wert im Strom oder markiert eine Frame-Grenze (Token), ohne Bytes zu konsumieren.

Attribut P/O Default Bedeutung
type bedingt Datentyp von value (byte/word/dword). Pflicht, wenn kein Frame-Token.
value bedingt Erwarteter Wert (0x.. oder dezimal). Pflicht, wenn kein Frame-Token.
token O Markiert eine Strom-Position: framestart, frameend, eof, invalid, specialvalue, normalinput.
ref O Verweis auf ein zuvor definiertes const/calc-Ergebnis – erwartet dessen Wert an dieser Stelle.
mask O Bitmaske auf den Vergleich.
validate O false true: Abweichung markiert den Frame als fehlerhaft, Parsing läuft weiter. false: Abweichung verwirft den Pfad.
valuedescriptor O Klartext-Tabelle für den Wert.
<const type="byte" value="0x02" name="STX" />              <!-- literales Byte -->
<const ref="Soll-Prüfsumme" validate="true" name="Prüfsumme" />

ASCII-Hex-Prüfsummen (type="ascii-byte"). Steht der Wert auf der Leitung als ASCII-Hex-Ziffern (zwei Hex-Zeichen je Byte, z.B. NMEA *47), während die Referenz per <calc> als Roh-Byte berechnet wurde, ergänze type="ascii-byte" (bzw. ascii-word/ascii-dword) am validierenden ConstRef. Er liest dann zwei Zeichen je Byte, dekodiert sie und vergleicht gegen den referenzierten Roh-Wert. format steuert die Anzeige des validierten Werts.

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

Die Token-Werte im Überblick:

token Bedeutung
framestart Telegramm-Anfang (Re-Sync-Punkt).
frameend Telegramm-Ende.
eof Ende des Eingabestroms.
specialvalue Sonder-Token (vom Decoder geliefert).
normalinput Normales Eingabe-Byte (selten explizit gebraucht).
invalid Ungültiges Eingabe-Token.

Wichtig: Die Sonder-Tokens (framestart, frameend, specialvalue, …) entstehen nur über einen Decoder. Ohne Decoder besteht der Strom ausschließlich aus NormalInput-Bytes, und ein <const token="…"> trifft nie zu. Für einfache Telegrammgrenzen literale Bytes verwenden (<const value="0x02" />); für Token-basiertes Framing einen <esc-decoder> deklarieren. Siehe Tokens und Decoder.


Kontrollfluss

<group> – Wiederholung, Option, Alternative

Das type-Attribut steuert die Semantik:

type Bedeutung
* 0..n Wiederholungen der Kinder (solange sie passen).
+ 1..n Wiederholungen (mindestens einmal).
? Optional – 0 oder 1 Vorkommen.
OR Alternativen: jedes Kind-<struct> ist eine Variante; die erste passende gewinnt.
<!-- 0..n Datensätze bis zum Frame-Ende -->
<group type="*">
  <byte name="Wert" />
</group>

<!-- entweder ein kurzes ENQ oder ein volles Telegramm -->
<group type="OR">
  <struct name="ENQ">    ... </struct>
  <struct name="Voll">   ... </struct>
</group>

<struct> – verschachtelte Struktur

Gruppiert Felder zu einer benannten Untereinheit. Kann inline stehen oder – als direktes Kind von <definition> – wiederverwendbar definiert und per ref eingebunden werden.

Attribut P/O Bedeutung
name O Bezeichner / Label der Struktur.
ref O Bindet eine in <definition> definierte Struktur ein.
size O Exakte Byte-Länge der Struktur.
maxsize O Obergrenze der Byte-Länge (nicht exakt).
sizeRef / maxsizeRef O size/maxsize dynamisch aus einem Feld.
nameRef O Name dynamisch aus einem Feld.
visible O Sichtbarkeit.
<struct name="Zeit">
  <byte name="Stunde"  format="{0:D2}" />
  <byte name="Minute"  format="{0:D2}" />
  <byte name="Sekunde" format="{0:D2}" />
</struct>

<switch> / <case> / <default>

Verzweigt abhängig vom Wert eines zuvor gelesenen Feldes. <switch ref="…"> nennt das Feld (oder calc/var/unify-Ergebnis); jedes <case> deckt einen Wert oder ein Muster ab, <default> fängt den Rest.

Element Attribut Bedeutung
switch ref (P) Feld, dessen Wert verglichen wird.
switch mask (O) Bitmaske, die vor dem Vergleich auf den Wert gelegt wird.
case value (O) Exakter Vergleichswert.
case match (O) Muster statt Einzelwert, siehe Match-Muster.
default Fallback, wenn kein case passt.

Pro <case> ist entweder value oder match anzugeben.

<byte name="Typ" />
<switch ref="Typ">
  <case value="0x01">
    <word name="Status" />
  </case>
  <case match="(0x42,0x44)">       <!-- 0x42 oder 0x44 -->
    <array size="6" name="Daten" />
  </case>
  <default>
    <match label="Unbekannter Typ" />
  </default>
</switch>

Berechnung & Dekodierung

<calc> – Prüfsummen / Berechnungen

Berechnet einen Wert über einen Byte-Bereich und registriert ihn unter name, sodass ein nachfolgendes <const ref="…" validate="true"> oder <switch ref="…"> darauf zugreifen kann.

Attribut P/O Default Bedeutung
name P Name des Ergebnisses (für ref). Anders als jeder <const>-Anzeigename wählen, sonst löst der ref auf sich selbst auf.
type O Eingebaute Funktion: ByteSum, ByteXor, LRC oder CRC.
function O Name einer per Code registrierten eigenen Funktion.
from P Start-Byte-Offset.
to P End-Byte-Offset (inklusive; negativ = relativ zur aktuellen Position).
startValue O 0 Startwert des Akkumulators (bei CRC ignoriert – stattdessen init).
resultType O byte Ergebnistyp: byte/word/dword.
encoding O ascii-hex: den Eingabebereich vor der Berechnung von ASCII-Hex (2 Ziffern je Byte) zu Binärbytes dekodieren.

Anzugeben ist type oder function. Die eingebauten Typen ByteSum, ByteXor, LRC und CRC funktionieren überall – auch im Laufzeit-XML-Interpreter. function="…" verweist dagegen auf eine im Server registrierte Funktion und steht nur in mitgelieferten System-Interpretern zur Verfügung.

  • LRC ist die Longitudinal-Redundancy-Check-Prüfsumme von Modbus ASCII: Bytesumme mod 256, dann Zweierkomplement (LRC = (byte)(-sum)).
  • encoding="ascii-hex" wird gebraucht, wenn die abgedeckten Bytes als ASCII-Hex auf der Leitung stehen (z.B. Modbus ASCII), denn die Prüfsumme ist über die dekodierten Bytes definiert, nicht über die ASCII-Zeichen.
<calc name="Checksumme" type="ByteSum" from="0" to="9" resultType="byte" />
<const ref="Checksumme" validate="true" />

<!-- Modbus ASCII: LRC über die dekodierten 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" – generische parametrierte CRC

Eine konfigurierbare CRC (Rocksoft/Williams-Modell) deckt die gängigen reflektierten und nicht-reflektierten Varianten ab – Modbus RTU, MAVLink, CRSF, DNP3 u.a. Zusätzlich zu from/to:

Attribut P/O Default Bedeutung
width O 16 Ergebnisbreite in Bit: 8, 16 oder 32.
poly O 0 Generatorpolynom in Normalform (dezimal oder 0x…). Modbus = 0x8005, nicht 0xA001.
init O 0 Startwert des Schieberegisters.
reflectIn O false Jedes Eingangsbyte vor der Verarbeitung bit-spiegeln.
reflectOut O false Das Endergebnis bit-spiegeln.
xorOut O 0 Finales XOR auf das Ergebnis.
byteOrder O little Byte-Reihenfolge des Multi-Byte-Ergebnisses auf der Leitung: little (lo-Byte zuerst) oder big.
appendValue O Konstant-Byte, das nach dem Bereich [from..to] in die CRC einfließt (vor reflectOut/xorOut). Für MAVLinks CRC_EXTRA (per-Message-Byte, das nicht auf der Leitung steht).
appendValueRef O Wie appendValue, aber das Byte stammt zur Laufzeit aus einem referenzierten Feld/var/const. So zieht ein gemeinsamer <calc> sein CRC_EXTRA aus einem per-Message-<var value=… name=…> (DRY statt ein calc je Message).

Das Polynom ist die Normalform. Das reflektierte Modbus-Polynom 0xA001 wird als poly="0x8005" zusammen mit reflectIn/reflectOut ausgedrückt.

Gängige Parametersätze (reveng-Katalog):

Protokoll 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 über Addr..letztes Daten-Byte, lo-Byte zuerst -->
<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 angehängt (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-Variante: ein geteilter calc, das CRC_EXTRA kommt je Message aus einem var -->
<var value="50" name="CrcExtra" />   <!-- in HEARTBEAT; SYS_STATUS nutzt 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> – eigener Decoder (Code, fortgeschritten)

Bindet einen per Code registrierten Decoder ein. Decoder transformieren den Rohstrom (Framing, zustandsbehaftete Escape-Sequenzen) vor dem eigentlichen Parsing.

Attribut P/O Bedeutung
function / name P Name des registrierten Decoders.
<decoder name="MyDecoder" />

Nicht im Laufzeit-XML-Workflow verfügbar. Ein code-basierter <decoder> setzt eine im Server registrierte Implementierung voraus und steht daher nur in mitgelieferten System-Interpretern zur Verfügung – nicht in einem zur Laufzeit aus XML erzeugten Interpreter. Für deklaratives Framing nutze stattdessen <esc-decoder> (siehe unten); für die allermeisten Protokolle reicht das aus.

<esc-decoder> – deklarativer Escape-Decoder

Beschreibt Escape-Sequenzen rein in XML – ohne Code. Kinder sind <sequence> (Byte-Folge → Ausgabe + Token) und <escape-mask> (Bit-Operation auf ein Byte).

<sequence>-Attribute: input (Eingabe-Bytes, leerzeichengetrennt), output (Ausgabe-Bytes), tokentype (resultierendes Token).

<escape-mask>-Attribute: 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>

Sammeln & Vorausschau

<unify> – Bytes zusammenfassen

Sammelt die von den Kind-Elementen gelesenen Bytes und führt sie zu einem Wert des Typs type zusammen. Mit name wird das Ergebnis referenzierbar (z.B. in einem switch); ohne name dient es als anonymer Sammler.

Attribut P/O Bedeutung
type P Zieltyp (byte/word/dword/array/string).
name O Name des Ergebnisses.
format O Anzeige-Format.
<unify type="array" name="Daten">
  <group type="*">
    <lookahead distance="1" result="failed">
      <const type="byte" value="0x03" />   <!-- solange nicht ETX -->
    </lookahead>
    <byte />
  </group>
</unify>

<lookahead> – spekulative Vorausschau

Prüft, ob ab einer bestimmten Distanz ein Muster zutrifft, ohne Bytes zu konsumieren. Damit lassen sich z.B. variable Datenbereiche bis zu einem Terminator lesen.

Attribut P/O Werte Bedeutung
distance P Ganzzahl Anzahl Bytes Vorausschau ab aktueller Position.
result P matched, failed (accepted = Legacy-Alias von matched) Gewünschtes Ergebnis: bei matched ist der Lookahead erfolgreich, wenn die Kinder passen; bei failed, wenn sie nicht passen (Negation).

Das Beispiel unter <unify> nutzt result="failed", um „lies Bytes, solange das nächste Byte nicht das Endebyte (ETX) ist" auszudrücken.


Ausgabe

<match>

Erzeugt das sichtbare Telegramm-Label. Es gibt zwei sich ausschließende Wege, das Label zu bestimmen:

  • label – ein statischer Text.
  • format – ein .NET-String.Format-Muster, in das die Werte der <param>-Kinder ({0}, {1}, …) in ihrer Reihenfolge eingesetzt werden.
Attribut P/O Default Bedeutung
label O Statisches Label.
format O Format-Muster über die <param>-Werte.
add-label O Wie label, aber anhängend (setzt implizit append="true").
add-format O Wie format, aber anhängend.
add-ref O Kürzel: hängt ein einzelnes Feld als Parameter an (anhängend).
append O false true: an die bisherige Ausgabe anhängen statt sie zu ersetzen.
usespace O true false: kein Trenn-Leerzeichen beim Anhängen.

label und format nicht zusammen verwenden. Sind beide am selben <match> gesetzt, gewinnt format und überschreibt das label. Für statischen Text plus Werte schreibst du den Text direkt in den Format-String: <match format="Status: {0}"><param ref="Wert" /></match>.

<param>-Attribute (Kinder von <match>, in der Reihenfolge {0}, {1}, …):

Attribut P/O Bedeutung
ref O Feld, dessen Wert eingesetzt wird – der Klartext aus der Wert-Tabelle, sonst der formatierte Feldwert.
value O Einziger unterstützter Wert: value="label" fügt das bisher aufgebaute Label ein.
useRawValue O true: den rohen Feldwert statt des Wert-Tabellen-Klartexts verwenden.
<byte name="Adresse" format="{0}" />
<byte name="Status"  valuedescriptor="status-descriptor" />
<match format="Adr {0} = {1}">
  <param ref="Adresse" />
  <param ref="Status" />
</match>

Wert-Tabellen (Enums, Bitfelder, Flags)

<valuedescriptor> / <descriptor>

Bildet Werte auf Klartext ab. type bestimmt die Art der Abbildung.

<valuedescriptor>-Attribut P/O Bedeutung
type P byte/word/dword (Wert-Mapping), string/array (Text-Mapping), bitfield, flags.
name P Bezeichner (für valuedescriptor="…" an Feldern).
hideWhenEmpty O Nur type='flags': ist kein Bit gesetzt, wird die Description des referenzierenden Feldes komplett unterdrückt (verhält sich wie visible='false'). Ohne das Attribut bleibt das Feld auch bei Wert 0 sichtbar. Default: false.
<descriptor>-Attribut P/O Bedeutung
value O Zu treffender Wert.
mask O Bitmaske (für bitfield/flags).
name P interner Bezeichner.
label O angezeigter Text.
valuedescriptor O rekursiver Verweis auf eine weitere Tabelle.

Einfaches Enum:

<valuedescriptor type="byte" name="status-descriptor">
  <descriptor value="0x00" name="OK"    label="Bereit" />
  <descriptor value="0x01" name="Busy"  label="Beschäftigt" />
  <descriptor value="0xFF" name="Fault" label="Störung" />
</valuedescriptor>

Flags/Bitfeld (Maske statt Einzelwert):

<valuedescriptor type="flags" name="alarm-flags">
  <descriptor mask="0x01" name="FEUER"   label="Feueralarm" />
  <descriptor mask="0x02" name="STOERUNG" label="Störung" />
  <descriptor mask="0x04" name="ABSCHALT" label="Abschaltung" />
</valuedescriptor>

Leere Flag-Felder ausblenden mit hideWhenEmpty (nur type='flags'): Bytes ohne gesetztes Bit erzeugen dann gar keine Description-Zeile. Nützlich z.B. für lange Bitmaps (Consolidated Heartbeat), bei denen nur die gesetzten Systemadressen interessieren:

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

Bei Wert 0x00 entfällt die Zeile für das Feld ganz; bei 0x06 erscheint Systeme 0-7: 6 (System 1, System 2).


Match-Muster (Pattern Matching)

Die Attribute match (an <field>, <const>) und match (an <case>) erlauben Muster statt fester Einzelwerte. Optional führt ~ eine Negation ein.

Zeichenmengen [ … ]

Für zeichenbasiertes Matching, mit Bereichen über -:

[0-9]            eine Dezimalziffer
[0-9a-fA-F]      eine Hex-Ziffer
~[ \t\r\n]       irgendein Byte außer Whitespace

Wertemengen ( … )

Komma-getrennte Werte und Bereiche (Hex oder dezimal):

(0x02)               nur 0x02
(0x41,0x43)          0x41 oder 0x43
(0x10-0x1F)          Bereich 0x10 .. 0x1F
(0x01-0x10,0x20)     gemischt: Bereich plus Einzelwert
~(0x00)              alles außer 0x00

Beispiele im Einsatz:

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

Statische vs. dynamische Attribute (…Ref)

Mehrere Attribute existieren in einer statischen und einer „Referenz"-Variante. Die Referenz-Variante löst den Wert zur Parse-Zeit aus einem anderen Feld auf:

Statisch Dynamisch Wirkung
size="8" sizeRef="LenField" feste vs. aus Feld gelesene Länge
maxsize="32" maxsizeRef="MaxField" feste vs. dynamische Obergrenze
name="X" nameRef="NameField" fester vs. dynamischer Feldname

So wird z.B. ein längenpräfigiertes Nutzdatenfeld ausgedrückt:

<byte  name="Länge" />
<array sizeRef="Länge" name="Nutzdaten" />

Muster & Rezepte

Wiederkehrende Aufgabenstellungen und ihre idiomatische TrIML-Lösung.

Telegramm-Rahmen mit STX/ETX

Der Standardfall eines frame-orientierten Protokolls: feste Start-/End-Bytes als literale Konstanten erwarten (kein Decoder nötig).

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

Soll der Strom mitten im Telegramm robust re-synchronisieren, lassen sich die Grenzbytes stattdessen über einen <esc-decoder> zu framestart/frameend- Tokens machen und mit <const token="…"> matchen – siehe Tokens und Decoder bzw. den <esc-decoder>-Abschnitt der Element-Referenz.

Längenpräfigierte Nutzdaten

Ein Längen-Byte, gefolgt von genau so vielen Datenbytes – über sizeRef:

<byte  name="Länge" />
<array sizeRef="Länge" name="Nutzdaten" />

Prüfsumme berechnen und validieren

calc erzeugt den Soll-Wert, const ref=… validate="true" vergleicht ihn mit dem übertragenen Byte. Bei Abweichung wird der Frame als fehlerhaft markiert, das Parsing läuft aber weiter:

<calc  name="Soll-CHK" type="ByteXor" from="0" to="-1" resultType="byte" />
<const ref="Soll-CHK" validate="true" name="Prüfsumme" />

Telegrammtyp-abhängige Struktur (switch)

Ein Typ-Byte bestimmt den weiteren Aufbau:

<byte name="Typ" valuedescriptor="typ-descriptor" />
<switch ref="Typ">
  <case value="0x01"><word name="Status" /></case>
  <case value="0x02"><array size="6" name="Daten" /></case>
  <default><match label="Unbekannt" /></default>
</switch>

Mehrere Telegrammformen (group type="OR")

Wenn ein Strom verschiedene, nicht über ein einzelnes Typ-Byte unterscheidbare Telegrammformen enthält (z.B. ein kurzes Polling-Byte vs. ein volles Telegramm), trennen OR-Alternativen die Fälle. Die erste passende <struct> gewinnt – Diskriminierung oft über ein match-Muster am ersten Feld:

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

Variabel langer Datenbereich bis zum Terminator (unify + lookahead)

„Lies Bytes, solange das nächste Byte nicht das Endebyte ist" – die typische Lösung für Nutzdaten ohne Längenangabe. Der lookahead prüft das nächste Byte, ohne es zu konsumieren:

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

Ein Byte in Bitgruppen zerlegen (var + mask)

Den Wert einmal versteckt lesen, dann mehrfach maskiert sichtbar machen:

<var   type="byte" name="ctrl" />
<field ref="ctrl" mask="0x07" name="Modus" />
<field ref="ctrl" mask="0x38" name="Kanal" />
<field ref="ctrl" mask="0xC0" name="Priorität" />

Flags als Klartext (Bitfeld-Tabelle)

<valuedescriptor type="flags" name="alarm-flags">
  <descriptor mask="0x01" name="FEUER"    label="Feueralarm" />
  <descriptor mask="0x02" name="STOERUNG" label="Störung" />
</valuedescriptor>
...
<byte name="Alarme" valuedescriptor="alarm-flags" />

ASCII-kodierte Zahlen

Protokolle, die Zahlen als Text übertragen (z.B. "0235"), lesen mit den ASCII-Feldtypen:

<ascii-word   name="Temperatur" />     <!-- "0235" -> 235 -->
<ascii-digits size="3" name="Kanal" /> <!-- genau 3 Ziffern -->

Label inkrementell aufbauen (add-ref / add-label)

Statt eines einzigen match am Ende lässt sich das Label feldweise erweitern. add-ref hängt einen Feldwert an, add-label einen festen Text – beide setzen implizit append="true":

<byte name="Empfänger" format="{0:X2}" />
<match add-ref="Empfänger" />
<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-Logik

  • Deklarativ über <esc-decoder> – feste Byte-Sequenzen und einfache Bit-Operationen. Funktioniert auch im Laufzeit-XML-Interpreter (siehe Beispiele).
  • Per Code über einen <decoder> – für zustandsbehaftete Framing-Logik (z.B. Verdopplung eines Escape-Bytes im Datenstrom). Erfordert eine registrierte Implementierung und ist daher nur in mitgelieferten System-Interpretern möglich, nicht im reinen XML-Workflow.

Beispiele

Die folgenden Beispiele beschreiben öffentlich bekannte Protokolle und zeigen die TrIML-Konstrukte im Zusammenspiel. Sie sind als Lernvorlagen gedacht – prüfe eine eigene Definition immer mit POST /api/interpreters/dryrun (oder der Live-Vorschau im Interpreter-Assistenten) gegen echte Beispieldaten.

Minimal: fortlaufender Byte-Dump

Das kleinste sinnvolle Gerüst: nach einem literalen Start-Byte jedes folgende Byte einzeln als Hex ausgeben. Zeigt ein literales Rahmenbyte und eine 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 ist binär und kennt keine Start-/Endebytes – Telegramme werden über eine Sendepause (3,5 Zeichenzeiten) getrennt. Der Aufbau:

 Adresse(1)  Funktion(1)  Daten(N)  CRC16(2, little-endian)

Das Beispiel zeigt valuedescriptor für die Funktionscodes, switch/case für den funktionsabhängigen Datenteil und bigendian für die 16-Bit-Register (Modbus überträgt Registerwerte 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="Adresse"  format="{0}" />
      <byte name="Funktion" valuedescriptor="funccode" />

      <switch ref="Funktion">
        <case value="0x03">                    <!-- Read Holding Registers (Anfrage) -->
          <word name="StartAdresse" bigendian="yes" format="0x{0:X4}" />
          <word name="Anzahl"       bigendian="yes" />
        </case>
        <case value="0x06">                    <!-- Write Single Register -->
          <word name="Register" bigendian="yes" format="0x{0:X4}" />
          <word name="Wert"     bigendian="yes" format="0x{0:X4}" />
        </case>
        <default>
          <!-- weitere Funktionscodes hier ergänzen -->
        </default>
      </switch>

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

      <match format="Modbus  Adr {0}  {1}">
        <param ref="Adresse" />
        <param ref="Funktion" />
      </match>
    </message>

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

Praxis-Hinweise zu Modbus RTU:

  • Framing per Timing: Da es keine Rahmenbytes gibt, hängt die saubere Telegramm-Trennung an der Sendepause zwischen den Frames.
  • Anfrage vs. Antwort: Derselbe Funktionscode hat in Anfrage und Antwort unterschiedliche Layouts. Wenn beide Richtungen auf einem Port liegen, braucht man Zusatzlogik (z.B. getrennte Ports/Interpreter je Richtung).
  • CRC-16/Modbus lässt sich mit den eingebauten calc-Typen (ByteSum/ByteXor) nicht prüfen – sie verwenden ein anderes Polynom. Hier wird das CRC nur als Feld gelesen, nicht validiert.

NMEA 0183

NMEA 0183 (u.a. GPS) ist textbasiert: ein Satz beginnt mit $, endet vor der Prüfsumme mit * und schließt mit <CR><LF>. Das Beispiel zeigt literale Rahmenbytes, das Einsammeln eines variabel langen Textkörpers mit unify + lookahead (bis zum *-Byte) und ein string-Feld.

 $ 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="$" />

      <!-- Satzkörper einsammeln, bis das nächste Byte '*' (0x2A) ist -->
      <unify type="string" name="Satz">
        <group type="*">
          <lookahead distance="1" result="failed">
            <const type="byte" value="0x2A" />
          </lookahead>
          <byte />
        </group>
      </unify>

      <!-- XOR über alle Bytes zwischen '$' und '*' (Index 1 .. zuletzt gelesenes Byte) -->
      <calc name="PruefsummeXor" type="ByteXor" from="1" to="-1" resultType="byte" />

      <const type="byte" value="0x2A" name="*" />
      <!-- Die Prüfsumme steht als 2 ASCII-Hex-Ziffern auf der Leitung.
           type="ascii-byte" am validierenden ConstRef dekodiert sie und vergleicht
           gegen PruefsummeXor; ein Mismatch markiert den Frame als FrameError, statt
           ihn zu verwerfen. format="{0:X2}" zeigt die Prüfsumme als 2-stelliges Hex. -->
      <const ref="PruefsummeXor" validate="true" type="ascii-byte" name="Prüfsumme" format="{0:X2}" />
      <const type="byte" value="0x0D" name="CR" />
      <const type="byte" value="0x0A" name="LF" />

      <match format="NMEA {0}">
        <param ref="Satz" />
      </match>
    </message>
  </definition>
</Trace-Interpreter-Definition>

Zur NMEA-Prüfsumme: Sie ist das XOR aller Zeichen zwischen $ und *, übertragen als zwei ASCII-Hex-Ziffern. calc type="ByteXor" berechnet das Roh-Byte, und <const ref="PruefsummeXor" validate="true" type="ascii-byte"> dekodiert die zwei Zeichen von der Leitung und validiert sie dagegen. Das type="ascii-byte" am validierenden ConstRef überbrückt „berechnetes Roh-Byte" und „ASCII-Hex auf der Leitung" (siehe Element-Referenz, const / validate).

Eigene Definition testen

  1. XML im Interpreter-Assistenten der TraceUI schreiben.
  2. Gegen aufgezeichnete oder live anliegende Bytes per Vorschau (dryrun) prüfen.
  3. Felder und Labels mit den Soll-Werten abgleichen, bis die Erkennung stimmt.
  4. Interpreter anlegen und dem Port zuweisen.

Weitere öffentliche Protokoll-Interpreter, die wir als Beispiel bereitstellen, findest du im Repository unter den jeweils benannten Beispiel-Definitionen.

FAQ & Fehlersuche

Wie teste ich eine neue Definition?

Du brauchst keine DLL und keinen Build – die XML wird zur Laufzeit geladen:

  1. XML im Interpreter-Assistenten der TraceUI schreiben (oder per POST /api/interpreters/dryrun mit { "xml": …, "bytes": [ … ] }).
  2. Gegen eine kleine, bekannte Byte-Folge laufen lassen – dryrun liefert die erkannten Matches zurück, ohne den Server-Zustand zu verändern.
  3. Erkannte Felder gegen die Soll-Werte abgleichen und die XML anpassen, bis es stimmt. Dann per POST /api/interpreters/from-xml anlegen und einem Port zuweisen.

Mein Telegramm wird gar nicht erkannt – woran liegt's?

  • Frame-Grenzen stimmen nicht. Prüfe das Start-Byte (<const value="…">). Falls du mit token="framestart"/frameend" arbeitest: diese Tokens entstehen nur über einen Decoder – ohne <esc-decoder> (oder Code-<decoder>) matchen sie nie. Siehe Tokens und Decoder.
  • Ein const ohne validate bricht den Pfad ab. Stimmt ein erwarteter Festwert nicht exakt, wird der ganze Pfad verworfen. Für Prüfsummen immer validate="true" setzen, damit nur der Frame als fehlerhaft markiert wird.
  • size/sizeRef zu groß. Verlangt ein Feld mehr Bytes als das Telegramm hergibt, schlägt der Match fehl. sizeRef muss auf das richtige Längenfeld zeigen.

value vs. match – wann was?

  • value="0x02" erwartet genau diesen Wert.
  • match="(0x02,0x06)" bzw. match="(0x10-0x1F)" erlaubt mehrere Werte/Bereiche.
  • match="[0-9]" matcht zeichenbasiert (Mengen mit [ ]).
  • Ein führendes ~ negiert: ~(0x00) = „alles außer 0x00".

Bei <case> gilt: entweder value oder match, nicht beide.

Warum erscheint mein Feld nicht in der Ausgabe?

  • visible="false" (oder ein <var>) blendet Felder bewusst aus.
  • Felder ohne name lassen sich nicht referenzieren und tauchen je nach Kontext nicht eigenständig auf.
  • Das sichtbare Label entsteht erst durch ein <match> – ohne match-Element fehlt die Telegramm-Bezeichnung.

Wie baue ich das Label schrittweise auf?

Mit add-ref (Feldwert anhängen) und add-label (Text anhängen). Beide setzen implizit append="true". Alternativ ein einzelnes <match> am Ende mit mehreren <param>-Kindern und einem format-Muster.

Big-Endian / Little-Endian?

word/dword sind standardmäßig little-endian. Für big-endian bigendian="yes" am Feld setzen.

Wie formatiere ich Werte (Hex, führende Nullen, Einheiten)?

format ist ein .NET-String.Format-Muster; der Wert steht auf {0}:

Muster Ausgabe für 47
{0:X2} 2F
0x{0:X4} 0x002F
{0:D3} 047
{0} °C 47 °C

Eingebaute calc-Funktionen

type="ByteSum" (Summe) und type="ByteXor" (XOR) über den Bereich from..to – beide ohne Code, auch im Laufzeit-XML-Interpreter. Komplexere Prüfsummen (CRC etc.) brauchen eine im Server registrierte Funktion (function="name") und stehen daher nur in mitgelieferten System-Interpretern zur Verfügung.

Deklarativer vs. Code-Decoder?

  • <esc-decoder> (deklarativ): für feste Escape-Sequenzen und einfache Bit-Operationen (AND/OR/XOR/…). Kein Code nötig – funktioniert im reinen XML-Workflow.
  • <decoder> (Code): für zustandsbehaftete Framing-Logik. Setzt eine im Server registrierte Implementierung voraus und ist daher nur in mitgelieferten System-Interpretern verfügbar.

Hinweis: XSD-Defaults vs. Engine-Verhalten

Das XSD nennt für einige Attribute Default-Werte, die die Engine nicht in jedem Fall identisch behandelt. Maßgeblich ist das Engine-Verhalten. Verifizierte Punkte:

  • match/@usespace – Default true (Trenn-Leerzeichen zwischen Parametern); false schaltet sie ab.
  • match/@append – Default false; add-label/add-format/add-ref setzen es implizit auf true.
  • lookahead/@result – kanonisch matched / failed; accepted ist ein Legacy-Alias für matched.

Im Zweifel das Ergebnis per dryrun an realen Daten prüfen.

Wo finde ich Vorlagen?

Auf der Beispiel-Seite stehen vollständige Definitionen für öffentlich bekannte Protokolle (Modbus RTU, NMEA 0183) sowie ein minimales Gerüst. Sie decken die wichtigsten Konstrukte ab und lassen sich als Ausgangspunkt für eigene Definitionen kopieren.