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 funktionieren im Laufzeit-XML-Interpreter ohne jeglichen Code – Felder,
switch,group,unify,lookahead, Wert-Tabellen, die eingebautencalc-Funktionen (ByteSum,ByteXor,LRC,WordSum, parametrierteCRC) und der vollständige<decoder>, einschließlich Modi und dem Fenster-Framing<frame>für Protokolle ohne Delimiter.Übrig bleibt genau ein Weg, der registrierten C#-Code voraussetzt und daher im reinen XML-Workflow nicht zur Verfügung steht:
calc function="…"und ein<decoder name="X"/>ohne Kinder. Beides ist Altbestand; für neue Definitionen braucht man es 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). Bei ByteOriented schließt ein Flush() von außen die offene Nachricht ab – der Weg für Protokolle ohne Frame-Ende in der Grammatik. |
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>, <qword>, <float>, <double>
Ganzzahlen mit 8, 16, 32 bzw. 64 Bit sowie IEEE-754 mit einfacher (4 Byte) und
doppelter (8 Byte) Genauigkeit. Alle mehrbytigen Typen 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 (alle Typen außer byte). |
signed |
O | no |
yes → Rohwert als Zweierkomplement lesen. Nur an byte, word, dword, qword; anderswo wird die Definition beim Laden abgelehnt. |
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}" />
Vorzeichen (signed="yes") wird vor der Skalierung gebildet — der
typische Telemetriefall ist genau das:
<qword name="Zeitstempel" unit="us" />
<dword name="Breite" signed="yes" scale="1e-7" unit="deg" format="{0:F7}" />
<float name="Rollwinkel" unit="rad" format="{0:F3}" />
Drei Dinge dazu, die sonst überraschen:
signedist ein Anzeige-Attribut.match,switch/caseundvaluedescriptorvergleichen weiterhin den vorzeichenlosen Rohwert.qwordrechnet ohnescalenicht überdouble. Ein 64-Bit-Zeitstempel hat mehr Stellen als die double-Mantisse; der Rohwert wird deshalb ungewandelt formatiert.- An
float/doublehatsignedkeine Bedeutung und wird abgelehnt — das Format trägt das Vorzeichen selbst.
<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). |
nonprintable |
O | Wie Bytes ohne druckbares Zeichen erscheinen: raw (Vorgabe), hex (<02>) oder names (<STX>). Siehe unten. |
name, format, visible |
O | wie bei <byte>. |
nonprintable – Steuerzeichen sichtbar machen
| Wert | Ergebnis |
|---|---|
raw |
Vorgabe. Die Bytes werden über die Kodierung in Text gewandelt; Steuerzeichen bleiben Steuerzeichen. |
hex |
Alles ohne druckbares Zeichen als Hexpaar: <02>, <0D>, <80>. |
names |
Steuerzeichen mit ihrem ASCII-Namen: <STX>, <CR>. Ab 0x7F trotzdem ein Hexpaar – dafür gibt es keine Namen. |
Ab
hexwird der Wert nicht mehr beschnitten. Ohnenonprintableläuft ein Zeichenketten-Wert durch einTrim, das Leerzeichen, Tabs und Zeilenumbrüche am Rand entfernt. Beides gehört zusammen: wer Steuerzeichen sichtbar haben will, will die Einrückung nicht still verlieren.
Nur für
type="string". An einem Zahlenfeld lehnt die Engine das Attribut beim Laden ab, statt es wirkungslos hinzunehmen.
<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. |
format |
O | – | Anzeigemuster für den Wert. Auch ohne Platzhalter erlaubt: format="Ack" schreibt bei <const value="0x41" name="Quittierung"> schlicht Quittierung: Ack statt Quittierung: 65. Genau dafür ist es da — bei einer Konstanten sagt der Rohwert nichts. |
consume |
O | true |
no: der Knoten liest sein Token wie jeder andere, sein Byte gehört aber nicht in die Rohdaten des Frames. Für Busse, deren Trenner zwischen den Sätzen steht statt in einem. Nur an <const> erlaubt — an einem Datenfeld wäre es ein Wert, den die Anzeige zeigt und den die Rohdaten nicht hergeben. |
bigendian |
O | false |
yes: der Wert steht mit dem höchstwertigen Byte zuerst auf der Leitung. Wirkt auf die Anzeige – ohne das Attribut liest sie das Wort in Host-Reihenfolge, und eine big-endian Prüfsumme erscheint byte-vertauscht (Leitung BC E6, Anzeige 0xE6BC). Der Vergleich bleibt in beiden Fällen richtig: der erwartete Wert wird mitgedreht – aus einem <calc> (Leitungsreihenfolge) ebenso wie aus einem Feld (das seine Bytes schon gedreht hat). Zusammen mit type="ascii-*" wird das Attribut abgelehnt: dort ist es nicht umgesetzt. |
<const type="byte" value="0x02" name="STX" /> <!-- literales Byte -->
<const type="byte" value="0x41" name="Quittierung" format="Ack" />
<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>
Wenn nichts passt
Zwei Fälle, die verschiedene Dinge bedeuten:
- Kein
<case>trifft und es gibt kein<default>→ der Zweig scheitert. Steht er in einer<group type="OR">, kommt die nächste Alternative an die Reihe; steht er in einer Wiederholung (*,+), endet die Wiederholung, und die Nachricht läuft mit dem weiter, was sie bis dahin gelesen hat; sonst scheitert die Nachricht, und ihre Bytes kommen als Fehlerframe heraus. - Der Wert in
refist auf diesem Pfad noch gar nicht gelesen worden → der Zweig scheitert ebenfalls.<default>greift dabei nicht: der Vorgabezweig heißt „der Wert passte zu keinemcase", nicht „es gab keinen Wert".
Ein ref, den es in der Definition überhaupt nicht gibt, ist etwas anderes:
das wird schon beim Laden abgelehnt (Undefined node "…" referenced in …).
Der Wert gilt innerhalb eines Rahmens. Ein <switch> sieht nicht, was ein
vorheriger Rahmen an derselben Stelle gelesen hat.
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, WordSum 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/qword. |
encoding |
O | – | ascii-hex: den Eingabebereich vor der Berechnung von ASCII-Hex (2 Ziffern je Byte) zu Binärbytes dekodieren. |
byteOrder |
O | siehe unten | Reihenfolge des Multi-Byte-Ergebnisses: little oder big. |
Anzugeben ist type oder function. Die eingebauten Typen 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)).WordSumist die 16-Bit-Bytesumme (RDM: „the sum of all preceding bytes"). Anders alsByteSumträgt sie das Übertragsbyte und liefert zwei Bytes;resultType="word"gehört dazu.
byteOrderhat je Typ einen anderen Vorgabewert, und zwar mit Absicht: beiCRCgiltlittle, beiWordSumgiltbig. Beide folgen dem, was die jeweiligen Protokolle auf die Leitung legen. Wer sich nicht darauf verlassen will, schreibt es hin.
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> – der Byte-Tokenizer
Ein Decoder sitzt vor der Grammatik. Er bekommt den Rohstrom Byte für Byte
und macht daraus einen Strom von Tokens: framestart, frameend,
specialvalue, normalinput, invalidinput. Dabei darf er Bytes verwerfen,
ersetzen, zusammenfassen, aufspalten und Werte aus dem Seitenkanal
(LineStatus/ModemStatus) einbringen.
Erst danach greift die Grammatik: <const token="framestart"/> konsumiert genau
das Token, das der Decoder gesetzt hat.
Der Decoder ist vollständig deklarativ – keine Codezeile nötig.
<decoder>
<escape-mask input="0xAC" operation="XOR" mask="0x20" tokentype="normalinput"/>
<sequence input="0xAA" tokentype="framestart"/>
<sequence input="0xAB" tokentype="frameend"/>
</decoder>
<esc-decoder> ist ein Alt-Name für dieselbe Sache und darf weiter benutzt
werden; er baut dieselbe Engine.
Es gibt zwei Bauformen, und sie schließen einander aus:
| Bauform | Wofür |
|---|---|
Regelkette (<sequence>, <escape-mask>, <when>, <map>, <merge>, <counted>, <mode>) |
Protokolle mit Delimitern, Escapes, Modi. Jedes Byte wird einzeln und endgültig eingeordnet. |
Fenster (<frame>) |
Protokolle ohne Delimiter, bei denen ein Frame nur daran erkennbar ist, dass Kopffelder plausibel sind und die Prüfsumme stimmt. Entscheidungen werden über viele Bytes hinweg zurückgenommen. |
Beides in einer Datei ergäbe zwei Bedeutungen für dasselbe Regelwerk; die Mischung wird beim Laden abgelehnt.
Attribute am <decoder>
| Attribut | P/O | Bedeutung |
|---|---|---|
name |
O | Bezeichnung, nur zur Lesbarkeit. |
mode |
O | Startmodus. Ohne Angabe der erste deklarierte <mode>. |
modeScope |
O | frame (Vorgabe) oder stream – wie weit ein Modus gilt, siehe Modi. |
dedup |
O | pairs: jedes Byte steht doppelt auf der Leitung, eines wird verworfen. |
merge |
O | Verschmelzung zweier Bytes zu einem Wert (Kurzform ohne <mode>). |
onInvalid |
O | Token-Typ für Bytes, die im Merge-Modus keine gültige Ziffer sind. |
Alle sechs gehören zur Regelkette. Zusammen mit <frame> werden sie abgelehnt.
Regeln
Alle Regeln stehen entweder direkt im <decoder> (ein einziger, impliziter
Modus) oder in <mode>-Gruppen.
<sequence> – Bytefolge auf Token-Typ
| Attribut | P/O | Bedeutung |
|---|---|---|
input |
P | Ein oder mehrere Bytes, leerzeichengetrennt ("0x1B 0x5B"). |
tokentype |
P | Token-Typ des Treffers. |
output |
O | Ersatzbytes. Ohne Angabe wird das Eingabebyte durchgereicht. |
enter |
O | Modus, in den gewechselt wird. |
collapse |
O | next: der Treffer erzeugt kein Token – stattdessen trägt das nächste Byte Wert und diesen Token-Typ. Für Protokolle, bei denen ETX und Prüfsumme zusammen das Frame-Ende bilden. |
resetMerge |
O | no: der Treffer lässt ein halbes Nibble-Paar stehen. Vorgabe yes. |
<escape-mask> – Escape-Präfix, das das Folgebyte verrechnet
| Attribut | P/O | Bedeutung |
|---|---|---|
input |
P | Das Escape-Präfix. |
operation |
P | ADD, SUB, INC, DEC, NOT, AND, OR, XOR (Groß-/Kleinschreibung egal). |
mask |
P | Operand der Rechnung. |
tokentype |
P | Token-Typ des entkommenen Bytes. |
enter |
O | Modus, in den gewechselt wird. |
resetMerge |
O | no: lässt ein halbes Nibble-Paar stehen. Vorgabe yes. |
<when> / <otherwise> – Prädikat auf Byte oder Seitenkanal
Manche Busse übertragen die Frame-Grenze nicht im Byte-Wert, sondern in einem Leitungszustand – etwa das 9. Bit einer 9-Bit-Übertragung.
| Attribut | P/O | Bedeutung |
|---|---|---|
source |
P¹ | value, linestatus oder modemstatus. |
mask |
O | Bitmaske (Vorgabe 0xFF). |
value |
O | Erwarteter Wert nach der Maskierung. Ohne Angabe: die Maske selbst, also „alle Maskenbits gesetzt“. |
max |
O | Obergrenze – macht aus dem Vergleich einen Wertebereich. |
tokentype |
O | Token-Typ. Entfällt, wenn <emit>-Kinder angegeben sind. |
enter |
O | Modus, in den gewechselt wird. |
¹ Pflicht in der Kurzform – siehe <condition> unten.
<!-- Das 9. Bit markiert das Adressbyte und damit den Frame-Anfang -->
<decoder>
<when source="linestatus" mask="0x40" tokentype="framestart"/>
</decoder>
<otherwise tokentype="…"/> setzt den Token-Typ für alles, was keine Regel
getroffen hat. tokentype="notoken" heißt ausdrücklich: dieses Byte erzeugt
nichts – der Weg, unbekannte Leitungszustände zu verwerfen.
<condition> – mehrere Bedingungen, mit UND verknüpft
Manchmal ergibt erst das Zusammentreffen von Byte-Wert und Seitenkanal die
Bedeutung. Dann tragen <condition>-Kinder die Bedingungen; alle müssen
zutreffen, damit die Regel greift.
| Attribut | P/O | Bedeutung |
|---|---|---|
source |
P | value, linestatus oder modemstatus. |
mask |
O | Bitmaske (Vorgabe 0xFF). |
value |
O | Erwarteter Wert nach der Maskierung. |
max |
O | Obergrenze – Wertebereich. |
<!-- RAF500: der Rahmen beginnt bei 0xB4 MIT gesetztem 9. Bit.
Ohne die Verknüpfung wäre jedes Datenbyte mit dem Wert 0xB4
ein Rahmenbeginn. -->
<when tokentype="specialvalue">
<condition source="linestatus" mask="0x40"/>
<condition source="value" value="0xB4"/>
</when>
Zwei Regeln dazu:
- Kurzform oder Kinder, nicht beides. Steht
sourceam<when>selbst und daneben ein<condition>, wird die Definition abgelehnt – sonst bliebe offen, ob die Attribute eine weitere Bedingung sind oder eine Vorgabe für die Kinder. - Ohne Bedingung geht nicht. Ein
<when>ganz ohnesourceund ohne<condition>träfe immer zu und verdeckte jede folgende Regel. Wer das will, schreibt<otherwise>– und sagt damit auch, dass er es will.
<emit> – ein Eingabebyte, mehrere Tokens
| Attribut | P/O | Bedeutung |
|---|---|---|
source |
P | value, linestatus oder modemstatus. |
mask |
O | Bitmaske auf den gelesenen Wert. |
tokentype |
P | Token-Typ. |
<emit> steht ausschließlich als Kind eines <when> – der Fan-out hängt an
der Bedingung, die ihn auslöst:
<!-- Der maskierte Leitungszustand wird selbst zum Datenwert -->
<when source="linestatus" mask="0x1F" value="0x00" max="0x08">
<emit source="linestatus" mask="0x1F" tokentype="specialvalue"/>
<emit source="value" tokentype="normalinput"/>
</when>
<map> – feste Wertabbildung
Für Leitungscodes, bei denen das übertragene Symbol nicht der Nutzwert ist.
| Attribut | P/O | Bedeutung |
|---|---|---|
onMiss |
O | Ersatzwert für nicht abgebildete Symbole. Ohne Angabe bleibt das Byte unverändert. |
onMissToken |
O | Token-Typ für nicht abgebildete Symbole. |
<map onMiss="0xFF" onMissToken="specialvalue">
<entry from="0x55" to="0x00"/>
<entry from="0x56" to="0x01"/>
</map>
<counted> – längen-getriebener Bereich
Für Protokolle, deren Frame-Länge im Frame selbst steht.
| Attribut | P/O | Bedeutung |
|---|---|---|
startToken |
O | Token-Typ, der den Bereich eröffnet (Vorgabe framestart). |
lengthAt |
O | Position des Längenbytes im Bereich; 0 ist das eröffnende Byte (Vorgabe 1). |
adjust |
O | Summand auf den gelesenen Längenwert, auch negativ. |
endToken |
O | Token-Typ des letzten Bytes im Bereich (Vorgabe frameend). |
<!-- 0xDD, dann das Laengenbyte; das Frame endet nach (Laenge-2) Datenbytes -->
<decoder>
<sequence input="0xDD" tokentype="framestart"/>
<counted lengthAt="1" adjust="-2" endToken="frameend"/>
</decoder>
<frame> – Framing ohne Delimiter
Manche Protokolle haben kein Start- und kein Endzeichen. Ein Frame ist dort nur daran erkennbar, dass die Kopffelder plausibel sind und die Prüfsumme stimmt. Trifft eines davon nicht zu, war es kein Frame — der Decoder setzt ein Byte weiter neu an.
Genau dafür ist <frame> da. Er puffert ein Fenster, prüft den Frame am
Fensteranfang und spult bei Fehlschlag zurück.
<decoder>
<frame minSize="4" maxSize="60">
<at index="0" match="(0x01-0x3F,0xC0-0xFF)"/> <!-- Adresse -->
<at index="1" match="(0x00-0x3C)" length="total"/> <!-- Gesamtlänge -->
<at index="2" match="(0x01,0x02,0xC0,0xC1)"/> <!-- Kommando -->
<checksum type="bytesum"/>
</frame>
</decoder>
Attribut am <frame> |
P/O | Bedeutung |
|---|---|---|
size |
O | Feste Frame-Größe für Protokolle ohne Längenfeld. Schließt <at length="total"> aus. |
minSize |
O | Kleinste mögliche Frame-Größe. Wirkt als Untergrenze auf die gelesene Länge, nicht nur als Startschwelle. Ohne Angabe: size, sonst 1. |
maxSize |
O | Harte Obergrenze. Ein Längenfeld, das mehr behauptet, verwirft den Kandidaten. |
startToken |
O | Token vor dem Frame (Vorgabe framestart). |
endToken |
O | Token nach dem Frame (Vorgabe frameend). |
<at> – Prädikat auf eine feste Position
Gezählt wird ab dem Frame-Anfang, nicht ab dem Strom.
| Attribut | P/O | Bedeutung |
|---|---|---|
index |
P | 0-basierte Position im Frame. |
match |
O | Erlaubte Werte, siehe Match-Muster – dieselbe Schreibweise wie an <byte match=…>. |
length |
O | total: dieses Feld trägt die Gesamtgröße des Frames. |
size |
O | Breite des Längenfelds in Bytes (1–4, Vorgabe 1). Nur mit length="total". |
byteOrder |
O | Reihenfolge des Längenfelds: big (Vorgabe) oder little. Nur mit length="total". |
adjust |
O | Summand auf den gelesenen Längenwert, auch negativ. Nur mit length="total". |
<checksum> – die Prüfsumme als Framing-Bedingung
Hier ist die Prüfsumme keine Fehleranzeige, sondern Teil der Erkennung:
stimmt sie nicht, war es kein Frame. (Die Fehleranzeige eines erkannten Frames
macht weiterhin <calc> zusammen mit <const validate="true">.)
| Attribut | P/O | Bedeutung |
|---|---|---|
type |
P | bytesum, bytexor, lrc, wordsum oder crc. |
at |
O | Position der Prüfsumme im Frame; negativ zählt vom Ende. Vorgabe: ganz hinten. |
from |
O | Erstes gedecktes Byte (Vorgabe 0). |
to |
O | Letztes gedecktes Byte, einschließlich. Vorgabe: das Byte vor der Prüfsumme. |
size |
O | Anzahl Prüfsummenbytes. Bei crc aus width abgeleitet und dort nicht setzbar. |
byteOrder |
O | little (Vorgabe) oder big. |
width, poly, init, xorOut, reflectIn, reflectOut |
O | Nur bei type="crc"; Bedeutung wie bei <calc type="CRC">. An anderen Typen werden sie abgelehnt. |
from/to braucht man, sobald die Prüfsumme nicht alles vor sich abdeckt —
MAVLink rechnet ab Byte 1, das Startzeichen bleibt außen vor:
<frame size="5">
<at index="0" match="(0xFE)"/>
<checksum type="bytesum" from="1"/>
</frame>
Was beim Laden abgelehnt wird – jeder dieser Fälle ließe sonst einen Port stumm zurück: er nimmt Bytes an, prüft, verwirft und meldet nie etwas. Fehlende Längenquelle;
sizegegenminSize/maxSize; eine<at>-Position außerhalb des größten möglichen Frames; eine Frame-Größe über der Fenstergrenze;type="crc"ohnepoly; CRC-Attribute an anderen Typen; einbyteOrdermit Tippfehler;adjust/size/byteOrderohnelength="total".
Modi
Viele Protokolle wechseln innerhalb eines Telegramms die Lesart: erst ein
Kopfbereich in rohen Bytes, dann ASCII-Hex, dazwischen ein Textabschnitt. Ein
<mode> bündelt die Regeln, die gerade gelten.
<decoder mode="asciihex" modeScope="stream">
<mode name="asciihex" merge="ascii-hex" onInvalid="invalidinput">
<sequence input="0x3A" tokentype="framestart"/>
<sequence input="0x0D" tokentype="frameend"/>
<sequence input="0x02" tokentype="specialvalue" enter="text"/>
</mode>
<mode name="text">
<sequence input="0x03" tokentype="specialvalue" enter="asciihex"/>
</mode>
</decoder>
merge – zwei Bytes ergeben einen Wert
| Wert | Bedeutung |
|---|---|
none |
Keine Verschmelzung (Vorgabe). |
ascii-hex |
Strenge Hex-Ziffern 0-9 A-F a-f. Alles andere ist ungültig → onInvalid. |
ascii-hex-upper |
Wie ascii-hex, aber nur Großbuchstaben. a–f sind ungültig. |
low-nibble |
Byte & 0x0F – das untere Halbbyte, z. B. für Ströme, die ihre Nibbles schon über <map> bekommen haben. |
ascii-digit-legacy |
(Byte - 0x30) & 0x0F. Dekodiert A–F falsch. Existiert nur, um vorhandene Protokolle unverändert nachzubilden – für neue Definitionen niemals wählen. |
<after> – Moduswechsel nach fester Länge
| Attribut | P/O | Bedeutung |
|---|---|---|
bytes |
O* | Anzahl Wire-Bytes. |
values |
O* | Anzahl verschmolzener Werte (bei ascii-hex also je zwei Wire-Bytes). |
enter |
P | Zielmodus. |
apply |
O | deferred (Vorgabe) oder immediate – siehe unten. |
* Genau eines von bytes/values muss gesetzt sein.
modeScope sagt, wie weit ein Modus gilt:
frame(Vorgabe): nur innerhalb eines Telegramms. Am Frame-Rand fällt der Modus auf den Startmodus zurück. Passend, wenn ein Telegramm immer gleich beginnt.stream: über Frame-Grenzen hinweg. Nötig, wenn eine Umschaltung (etwa in einen Textabschnitt) auch das nächste Telegramm noch betrifft.
Nicht jede Kombination ist zulässig. Bei
framemuss aus jedem Modus ein Weg zurück zum Startmodus führen – sonst entstünden dort nie Tokens. Die Engine prüft das beim Laden und lehnt die Definition ab, statt den Port stumm zu lassen.
apply an <after> sagt, ob der Modus mit diesem Byte endet
(immediate) oder nach ihm (deferred, Vorgabe). Für die Einordnung des
nächsten Bytes ist beides gleich; der Unterschied ist, ob an dieser Stelle ein
Frame geschnitten werden darf.
Auswertungsreihenfolge
Die Reihenfolge im Dokument ist frei – die Auswertung steht fest:
1. dedup Byte verwerfen?
2. escape-mask Escape eröffnen oder auflösen
3. sequence / when Steuerzeichen bzw. Prädikat, ggf. Moduswechsel
4. map nur für Bytes, die 1–3 nicht getroffen haben
5. merge nur für Bytes, die 1–3 nicht getroffen haben
6. emit / collapse Token-Ausgabe
7. counted / after Zählwerke
Zwei Regeln, die daraus folgen:
- Ein Steuerzeichen wird nie übersetzt oder verschmolzen.
mapundmergegelten ausschließlich für Bytes, die keine Regel der Stufen 1–3 getroffen hat. - Ein getroffenes Steuerzeichen setzt den Merge-Takt zurück – standardmäßig.
Das ist eine Eigenschaft der Regel, nicht des Decoders: mit
resetMerge="no"überlebt ein halbes Nibble-Paar den Treffer. Beide Formen kommen in realen Protokollen vor. - Innerhalb eines laufenden
<counted>-Bereichs regiert die Länge, nicht das Byte. Sequenzen und Prädikate greifen dort nicht – sonst risse ein Nutzdatum, das zufällig wie ein Delimiter aussieht, das eigene Frame auf.
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. Ein String steht ohne Angabe in Anführungszeichen; format="{0}" lässt sie weg. |
valuedescriptor |
O | Klartext-Tabelle für den zusammengefassten Wert – ein zusammengefasstes Feld ist ein Feld. Bei type="string" vergleicht die Tabelle Zeichenketten (value='"3"'). |
nonprintable |
O | Wie bei <string>; nur bei type="string". |
visible |
O | no/false blendet das Ergebnis in der Anzeige aus. |
<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 ≥ 1 | Tokens ab dem Lesekopf, das geprüfte eingeschlossen: 1 prüft das Token unter dem Lesekopf, 3 überspringt zwei und prüft das dritte. |
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.
Vorsicht mit
distancegrößer 1 in einer Wiederholung. Ein Wächter mitdistance="3"beendet die Wiederholung zwei Tokens vor dem Muster – und zwar unabhängig davon, ob die Bytes dazwischen zum Muster gehören. Steht hinter den Nutzdaten eine Prüfsumme, kann ein Prüfsummen-Byte zufällig dem Muster gleichen: der Wächter greift dann mitten in den Nutzdaten, das Feld bricht zu früh ab und der Rahmen zerfällt. Und er greift auf jeder Position seines Fensters: beidistance="3"liegen zwei Bytes darin, bei einer zwei Byte breiten Prüfsumme also beide. Am eComm-Interpreter gemessen: von 600 Telegrammen mit randvollem Textteil scheiterten vier – genau die vier, deren CRC irgendwo ein0x01trug, also etwa jedes 128.In einer Region mit fester Länge (
<struct sizeRef="…">) braucht es den Wächter ohnehin nicht: die Längenprüfung deckelt die Wiederholung an der angekündigten Länge – über sie hinaus liest kein Feld.
Der Wächter vor einem optionalen Feld
Die häufigste Anwendung: ein Feld ist mal da und mal nicht, dahinter steht ein fester Schwanz (Prüfsumme, Zeilenende). Vorwärts gelesen ist nicht zu erkennen, ob die Zeichen unter dem Lesekopf zum optionalen Feld gehören oder schon zur Prüfsumme. Der Wächter blickt darüber hinweg:
<group type="?">
<lookahead distance="3" result="failed"><const token="frameend" /></lookahead>
<ascii-byte name="Modus" />
</group>
<ascii-byte name="Prüfsumme" />
<const token="frameend" visible="false" />
Liegt das Zeilenende schon an Position 3, ist außer der Prüfsumme nichts mehr da — die Gruppe entfällt. Beliebig viele solcher Gruppen dürfen hintereinander stehen; jede prüft für sich.
Zwei Fallen, beide lautlos.
- Der Tokentyp muss stimmen.
<const type="byte" value="0x0D" />trifft einframeend-Token nie — die Engine vergleicht zuerst den Typ. Der Wächter griffe dann nie, die Gruppe frisst die Prüfsumme, und das Frame verschwindet ersatzlos. Richtig ist<const token="frameend" />.distancezählt das geprüfte Token mit. Für ein zwei Zeichen breites Feld vor einer zwei Zeichen breiten Prüfsumme ist3richtig, nicht2. Ein Feld von n Zeichen braucht zusätzlich die Wächter5,7, …n+1, sonst greift es bei einem angebrochenen Rest daneben.
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. |
error |
O | false |
true: markiert den Frame als fehlerhaft (FrameError) — die Oberfläche färbt ihn entsprechend. Für Auffang-Nachrichten: eine Alternative, die ein unbrauchbares Frame absichtlich annimmt, damit es vollständig sichtbar wird. Ein Frame, das die Grammatik gar nicht annimmt, meldet die Engine zwar auch — aber nur die Bytes, die sie beim Zurücksetzen verworfen hat, und gar nichts, wenn der Parser noch auf weitere Eingabe wartet. Der Auffang-Zweig steckt die Frame-Grenzen selbst ab und meldet die Bytes, die auf der Leitung standen. Bis dahin konnte nur <const validate="true"> die Markierung setzen, und das nur als Nebenwirkung eines Wert-Vergleichs. |
Einschränkung: die Markierung ist ein Merker am Parser, der erst beim Abschluss des Frames verbraucht wird. Liegt das
<match error='true'/>in einem Zweig, der später scheitert, während ein anderer Zweig gelingt, trägt das gelungene Frame die Markierung. In der Praxis unkritisch, solange der Fehler-Match das letzte Kind hinter dem abschließenden Token ist — was der natürliche Ort dafür ist.
Ein Auffang-Zweig, der kaputte Frames sichtbar macht:
<group type="OR">
<struct ref="Frame-Inhalt" visible="false" />
<struct ref="Unlesbares Frame" visible="false" />
</group>
...
<struct name="Unlesbares Frame" visible="false">
<group type="*">
<lookahead distance="1" result="failed"><const token="frameend" /></lookahead>
<byte visible="false" />
</group>
<const token="frameend" visible="false" />
<match label="Frame Error!" error="true" />
</struct>
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 | Eine Quelle statt eines Feldes – siehe Tabelle unten. |
useRawValue |
O | true: den rohen Feldwert statt des Wert-Tabellen-Klartexts verwenden. |
Drei Quellen kennt value; jeder andere Wert wird beim Laden abgelehnt (ein
Tippfehler ergäbe sonst einen leeren Platzhalter in jeder Beschriftung):
value |
Bedeutung |
|---|---|
label |
die bisher aufgebaute Beschriftung des Frames |
frametext |
der druckbare Abzug des Frames: Steuerzeichen als <SOH>, druckbares ASCII als Zeichen, alles darüber als <XX> |
framehex |
derselbe Abzug in hex ("41 42 "), für Binärprotokolle |
Der Abzug endet am Lesekopf – bei den Bytes, die bis zu diesem <match>
gelesen sind. Das ist die Fähigkeit und nicht ihre Grenze: so lässt sich ein
Präfix abdrucken und der Rest anders formatieren.
<!-- ESPA: <SOH>1<STX>1<US>1234<ETX><7B>
Der Abzug steht VOR dem Prüfsummenfeld, damit die Prüfsumme als
Hexpaar erscheint und nicht als Zeichen. -->
<const type="byte" value="0x03" name="ETX" visible="no" />
<match format="{0}"><param value="frametext" /></match>
<byte name="Checksum" format="{0:X2}" visible="no" />
<match format="{0}<{1}>">
<param value="label" />
<param ref="Checksum" />
</match>
<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/qword (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. |
render |
O | inline (Default): der Name des zutreffenden Descriptors kommt in Klammern an die Beschriftung — Modus: 0 (ohne). child: die Bedeutung wird eine Unterbeschreibung mit demselben Offset und derselben Länge — Modus: 0 mit dem Kind ohne Protokollerweiterung. |
<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. |
default |
O | yes: greift, wenn kein anderer Descriptor zutrifft — der default:-Zweig. Ohne value. |
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>
Zweistufig — Wert oben, Bedeutung als Kind (render="child"):
<valuedescriptor type="byte" name="modus" render="child">
<descriptor value="0x00" name="ohne" label="ohne Protokollerweiterung" />
<descriptor value="0x01" name="mit" label="mit Protokollerweiterung" />
<descriptor default="yes" name="unbekannt!" />
</valuedescriptor>
ergibt
Kommunikationsmode: 0
ohne Protokollerweiterung
statt Kommunikationsmode: 0 (ohne). Diese Form brauchen Protokolle, deren
Klartext eine eigene Zeile verdient — handgeschriebene Interpreter bauen ihren
Beschreibungsbaum so. Ohne label ist der name der Klartext.
default="yes" ist der default:-Zweig: er greift, wenn kein anderer Wert
passt, und darf überall in der Liste stehen. Ohne ihn bleibt ein unbekannter
Wert kommentarlos — der Leser wüsste dann nicht, ob die Definition den Fall
nicht kennt oder ihn für belanglos hält.
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>
render und default wirken auch auf type='flags': render="child" setzt die
aufgelösten Bits eine Zeile unter den Rohwert statt in Klammern dahinter, und ein
<descriptor default="yes" name="0" /> liefert den Text für „kein Bit gesetzt".
Beides zusammen bildet nach, was Enum.ToString() auf einem [Flags]-Enum tut —
die Form, in der handgeschriebene Interpreter ihre Statuswörter zeigen.
Reihenfolge: die Engine gibt die Treffer in umgekehrter Schreibreihenfolge aus. Wer die aufsteigende Sortierung von
Enum.ToString()nachbilden will, schreibt das höchste Bit zuerst.
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).
Breite Bitfelder: bits statt Maske
Eine mask läuft über den Zahlen-Parser und endet dort bei 32 Bit. Für
Bitfelder, die breiter liegen oder über Bytegrenzen laufen, gibt es die
Bitlage:
<!-- CRSF/S.BUS: 16 Kanaele zu 11 Bit, gepackt ueber 22 Byte -->
<valuedescriptor type="bitfield" name="rc-channels">
<descriptor name="Kanal 1" bits="0..10" />
<descriptor name="Kanal 2" bits="11..21" />
<descriptor name="Kanal 3" bits="22..32" />
<!-- ... -->
</valuedescriptor>
| Schreibweise | Bedeutung |
|---|---|
bits="11..21" |
Bits 11 bis einschließlich 21. |
bits="7" |
genau Bit 7. |
- Bit 0 ist das niederwertigste Bit des Werts. Bei
bigendian="yes"hat das Feld seine Bytes vorher schon gedreht — gezählt wird am Wert, nicht in Wire-Reihenfolge. - Höchstens 64 Bit breit; darüber ließe sich der Wert nicht mehr anzeigen, und eine stille Kürzung wäre schlimmer als eine Fehlermeldung.
- Nur unter
type="bitfield"odertype="flags". Unter einem Wertekatalog ist der Descriptor-Wert ein Vergleichswert und keine Maske;bits="0..2"hieße dort still „Wert == 7". bitsundmask/valueschließen einander aus.
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="FrameOriented" frameGapUs="4000" />
<definition>
<valuedescriptor type="byte" name="funccode">
<descriptor value="0x01" name="ReadCoils" label="Read Coils" />
<descriptor value="0x03" name="ReadHoldingRegs" label="Read Holding Registers" />
<descriptor value="0x06" name="WriteSingleReg" label="Write Single Register" />
<descriptor value="0x10" name="WriteMultipleRegs" label="Write Multiple Registers" />
</valuedescriptor>
<message>
<byte name="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
ByteSum, ByteXor, LRC, WordSum und die parametrierte CRC – alle ohne
Code, auch im Laufzeit-XML-Interpreter. function="name" verweist auf eine im
Server registrierte Funktion und ist nur für mitgelieferte System-Interpreter
gedacht; für neue Definitionen braucht man ihn nicht.
Braucht der <decoder> Code?
Nein. Er ist vollständig deklarativ und kennt zwei Bauformen, die einander ausschließen:
- Regelkette –
<sequence>,<escape-mask>,<when>,<map>,<merge>,<counted>und<mode>. Für Protokolle mit Delimitern, Escapes und wechselnden Lesarten. Jedes Byte wird einzeln und endgültig eingeordnet. - Fenster –
<frame>. Für Protokolle ohne Delimiter, bei denen ein Frame nur daran erkennbar ist, dass die Kopffelder plausibel sind und die Prüfsumme stimmt. Der Decoder puffert, prüft und spult bei Fehlschlag zurück.
<esc-decoder> ist ein Alt-Name für die Regelkette und baut dieselbe Engine.
Ein <decoder name="X"/> ohne Kinder ist die einzige verbliebene
Code-Referenz: sie verweist auf eine per RegisterDecoder eingehängte
Implementierung. Der Weg ist Altbestand und fällt mit der Migration weg.
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.