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:
- Bytes werden Stück für Stück in den Parser gegeben.
- 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).
- Passt die Struktur vollständig, entsteht ein Match – ein erkanntes Telegramm mit Label und hierarchischen Feld-Beschreibungen.
- 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:
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.
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/dryrunmit{ "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 eingebautencalc-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 customcalc 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=0x03begrenzen das Telegramm.Typist ein Byte mit fester Bedeutung (Status / Kommando / Fehler).Adrist eine 16-Bit-Adresse (big-endian).Längegibt die Anzahl der folgenden Nutzdaten-Bytes an.Prüfsummeist 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>zuframestart/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 demdryrun-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).
labeloderformat– nicht beides. Sind beide an einem<match>gesetzt, gewinntformatund daslabelwird verworfen. Statischen Text deshalb direkt in denformat-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 2×frameGapUsstill war, injiziert er einen synthetischenframestart, sodass das letzte Frame sauber (CRC validiert) ohne Folge-Byte schließt. Das gilt nur für gap-geframte Interpreter; length-prefixed und delimiter-geframte 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 ausNormalInput-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.
LRCist 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
0xA001wird alspoly="0x8005"zusammen mitreflectIn/reflectOutausgedrü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. |
labelundformatnicht zusammen verwenden. Sind beide am selben<match>gesetzt, gewinntformatund überschreibt daslabel. 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="<ETX>" />
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. Dastype="ascii-byte"am validierenden ConstRef überbrückt „berechnetes Roh-Byte" und „ASCII-Hex auf der Leitung" (siehe Element-Referenz,const/validate).
Eigene Definition testen
- XML im Interpreter-Assistenten der TraceUI schreiben.
- Gegen aufgezeichnete oder live anliegende Bytes per Vorschau (
dryrun) prüfen. - Felder und Labels mit den Soll-Werten abgleichen, bis die Erkennung stimmt.
- 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:
- XML im Interpreter-Assistenten der TraceUI schreiben (oder per
POST /api/interpreters/dryrunmit{ "xml": …, "bytes": [ … ] }). - Gegen eine kleine, bekannte Byte-Folge laufen lassen –
dryrunliefert die erkannten Matches zurück, ohne den Server-Zustand zu verändern. - Erkannte Felder gegen die Soll-Werte abgleichen und die XML anpassen, bis es
stimmt. Dann per
POST /api/interpreters/from-xmlanlegen 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 mittoken="framestart"/frameend"arbeitest: diese Tokens entstehen nur über einen Decoder – ohne<esc-decoder>(oder Code-<decoder>) matchen sie nie. Siehe Tokens und Decoder. - Ein
constohnevalidatebricht den Pfad ab. Stimmt ein erwarteter Festwert nicht exakt, wird der ganze Pfad verworfen. Für Prüfsummen immervalidate="true"setzen, damit nur der Frame als fehlerhaft markiert wird. size/sizeRefzu groß. Verlangt ein Feld mehr Bytes als das Telegramm hergibt, schlägt der Match fehl.sizeRefmuss 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
namelassen sich nicht referenzieren und tauchen je nach Kontext nicht eigenständig auf. - Das sichtbare Label entsteht erst durch ein
<match>– ohnematch-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– Defaulttrue(Trenn-Leerzeichen zwischen Parametern);falseschaltet sie ab.match/@append– Defaultfalse;add-label/add-format/add-refsetzen es implizit auftrue.lookahead/@result– kanonischmatched/failed;acceptedist ein Legacy-Alias fürmatched.
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.