====== std.hl7 — HL7 v2 Messaging ======
**Fünf Units** für HL7 Version 2 — den im Gesundheitswesen meistverbreiteten Nachrichtenstandard. ''std.hl7'' deckt den gesamten Stack ab: MLLP-TCP-Framing, MSH/ACK-Engine, Patientenverwaltung (ADT), Auftragswesen (ORM/OML/RAS), Befundübermittlung (ORU/OUL) und Terminplanung (SIU).
→ [[lyx_-_programmiersprache:start|Übersicht]] · [[lyx_-_programmiersprache:units|Standard Library]] · [[lyx_-_programmiersprache:guides:welche-unit|Welche Unit?]]
Alle Beispiele dieser Seite sind mit ''lyxc 1.0.21A'' übersetzt und ausgeführt; die gezeigten Ausgaben sind die echten Programmausgaben.
----
===== Units =====
^ Unit ^ WP ^ Beschreibung ^
| [[lyx_-_programmiersprache:units:hl7:core|std.hl7.core]] | WP-HL7-00 | MLLP-Framing (VT+FS+CR), MSH-Parser, ACK-Generator, Duplikatserkennung (FNV1a-Ring), Versions- und Testmodus-Prüfung |
| [[lyx_-_programmiersprache:units:hl7:adt|std.hl7.adt]] | WP-HL7-01 | Patient Administration: ''%%ADT^Axx%%'' parsen/schreiben; PID, PV1, MRG, NK1; automatische Merge-Erkennung (A34–A45) |
| [[lyx_-_programmiersprache:units:hl7:orders|std.hl7.orders]] | WP-HL7-02 | Order Management: ''%%ORM^O01%%'', ''%%OML^O21%%'', ''%%RAS^O17%%'' parsen/schreiben; ORC, OBR, RXA; STAT-Erkennung; ''%%ORR^O02%%''-Quittung |
| [[lyx_-_programmiersprache:units:hl7:results|std.hl7.results]] | WP-HL7-03 | Result Reporting: ''%%ORU^R01%%'', ''%%OUL^R22%%'' parsen/schreiben; OBX (Panikwert- und Korrektur-Flags), NTE-Kommentare |
| [[lyx_-_programmiersprache:units:hl7:scheduling|std.hl7.scheduling]] | WP-HL7-04 | Terminplanung: ''%%SIU^S12%%''–''%%S26%%'' parsen/schreiben; SCH, AIS, AIG, AIL; Flags für Absage (S15), Löschung (S17), Nicht-Erscheinen (S26) |
Alle vier Fachunits importieren ''std.hl7.core'' — ''import std.hl7.adt;'' zieht den Kern also mit.
> **''%%^%%'' in DokuWiki-Tabellen.** Der Nachrichtentyp wird in HL7 mit ''%%^%%'' geschrieben (''%%ADT^A01%%''). In einer Wiki-Tabelle trennt genau dieses Zeichen die Zellen — deshalb steht es hier überall in ''%%…%%''. Wer diese Seite bearbeitet: bitte beibehalten, sonst zerfällt die Tabelle in Spalten.
----
===== Was ist HL7 v2? =====
HL7 (Health Level 7) Version 2 ist der faktische Standard für Datenintegration in Krankenhäusern. Jede Nachricht besteht aus zeilenweise (CR) geschriebenen **Segmenten** (3 Großbuchstaben + Feldtrenner), die ''|''-getrennte Felder und ''%%^%%''-getrennte Komponenten enthalten:
MSH|^~\&|LIS|HOSPITAL|HIS||20260616083000||ORU^R01|MSG001|P|2.5.1
PID|||PAT123^^^MPI||Muster^Max||19850315|M
OBR|1|ORD042|LAB042|1743-4^Glucose^LN|||20260616082000
OBX|1|NM|1743-4^Glucose^LN||195|mg/dL|70-99||||HH||F
Die Standard-Trennzeichen stehen in MSH-1 und MSH-2 und werden von ''Hl7MshParse'' aus der Nachricht gelesen, nicht angenommen:
^ Ebene ^ Zeichen ^ Beispiel ^
| Feld | ''%%|%%'' | ''%%MSH|^~\&|LIS%%'' |
| Komponente | ''^'' | ''%%Muster^Max%%'' |
| Wiederholung | ''%%~%%'' | ''%%ID1~ID2%%'' |
| Escape | ''%%\%%'' | ''%%\F\%%'' für ein literales ''%%|%%'' |
| Subkomponente | ''%%&%%'' | ''%%A&B%%'' |
----
===== MLLP-Framing =====
HL7 v2-Nachrichten werden über TCP immer im **MLLP-Frame** übertragen (Minimal Lower Layer Protocol):
[VT=0x0B] + HL7-Nachrichtenbytes + [FS=0x1C] + [CR=0x0D]
''MllpFrameWrite'' legt den Rahmen um eine fertige HL7-Nachricht, ''MllpFrameRead'' schält die Nachricht aus dem TCP-Empfangspuffer. ''MllpFrameRead'' sucht das VT auch mitten im Puffer — Datenmüll davor wird übersprungen.
----
===== Designprinzipien =====
* **Zero-Copy-Parsing**: Alle Parse-Funktionen speichern **Zeiger und Längen** in den Originalnachrichtenpuffer — keine String-Kopien. Der Puffer muss leben, solange die Struct benutzt wird.
* **Caller alloziert**: Alle Structs (MSH, PID, PV1, ADT, Order, Report, OBX, Appointment) legt der Aufrufer selbst an. Die Größe steht in den ''HL7_*_SIZE''-Konstanten.
* **Kein rekursiver Besitz**: Eine geparste ORU-Nachricht liefert nur den Befundkopf (''Hl7OruParse'') — die einzelnen OBX-Segmente werden getrennt mit ''Hl7ObxParse'' in einer Schleife geparst. Genauso bei NTE, AIS, AIG und AIL.
* **Abgeleitete Flags**: ''HL7_ORD_ISSTAT'', ''HL7_OBX_ISCRIT''/''HL7_OBX_ISCORR'' sowie ''HL7_APPT_ISCANCEL''/''ISDELETE''/''ISNOSHOW'' setzt der Parser selbst — keine manuellen String-Vergleiche nötig.
> **Zeiger aus einer Struct sind nicht nullterminiert.** Jedes Feld liegt als Paar vor: Offset ''X'' hält den Zeiger, Offset ''X_LEN'' die Länge. Wer den Wert an ''PrintLn'' oder ''StrConcat'' übergibt, druckt den Rest der Nachricht mit. Für die Ausgabe eine terminierte Kopie anlegen — siehe die Funktion ''Feld()'' im Quickstart unten.
----
===== Nachrichtenflüsse =====
==== Patientenaufnahme (ADT) ====
HIS/ADT-System HL7-Gateway / Lyx
| |
|-- ADT^A01 (Aufnahme) ->| Hl7AdtParse()
|<-- ACK^AA (Quittung) --| Hl7AckWrite() + MllpFrameWrite()
| |
|-- ADT^A08 (Update) --->| Hl7AdtParse()
|<-- ACK^AA -------------|
| |
|-- ADT^A03 (Entlassung)>| Hl7AdtParse()
|<-- ACK^AA -------------|
==== Laborauftrag und Befund (ORM → ORU) ====
KIS / Station LIS (Labor)
| |
|-- ORM^O01 (Auftrag) -->| Hl7OrmParse()
|<-- ORR^O02 (OK/ER) ----| Hl7OrrWrite()
| | [Analyse laeuft ...]
|<-- ORU^R01 (Befund) ---| Hl7OruParse() + Hl7ObxParse() je OBX
|-- ACK^AA ------------->|
==== Medikamentengabe (RAS) ====
Medikamentensystem Stationssystem
| |
|-- RAS^O17 ------------>| Hl7RasParse()
|<-- ACK^AA -------------|
==== Terminvergabe (SIU) ====
Terminplaner Fachabteilung
| |
|-- SIU^S12 (neu) ------>| Hl7SiuParse()
|-- SIU^S15 (Absage) --->| HL7_APPT_ISCANCEL = 1
|-- SIU^S26 (No-Show) -->| HL7_APPT_ISNOSHOW = 1
|<-- ACK^AA -------------|
----
===== Funktionsübersicht =====
==== std.hl7.core ====
^ Funktion ^ Rückgabe ^ Zweck ^
| ''Hl7StateInit(state)'' | – | Dedup-State nullen; einmalig pro Instanz |
| ''MllpFrameWrite(buf, bufMax, msg, msgLen)'' | Bytes oder ''-1'' | VT+Nachricht+FS+CR schreiben |
| ''MllpFrameRead(buf, bufLen, out, outMax)'' | Länge, ''-1'' (kein Frame), ''-2'' (out zu klein) | Nachricht aus dem Frame schälen |
| ''Hl7MshParse(msg, len, msh)'' | ''HL7_OK'' / ''HL7_ERR_SYNTAX'' | MSH-Segment in die Struct parsen |
| ''Hl7AckWrite(msh, code, text, out, outMax)'' | Bytes oder ''-1'' | ACK/NACK erzeugen; ''code'' ist ''%%"AA"c%%'', ''%%"AE"c%%'' oder ''%%"AR"c%%'' als ''pchar'' |
| ''Hl7DupCheck(state, partnerKey, ctrlId)'' | ''1'' = schon gesehen, ''0'' = neu | Duplikate je Partner über FNV1a-Ring (64 Einträge) |
| ''Hl7VersionCheck(msh)'' | ''HL7_OK'' / ''HL7_ERR_VERSION'' | Unterstützt 2.3.1, 2.4, 2.5, 2.5.1, 2.6, 2.7 |
| ''Hl7IsTestMsg(msh)'' | ''1'' = Testnachricht | MSH-11 beginnt mit ''T'' — nicht in die Produktivdatenbank schreiben |
| ''Hl7IsUtf8(msh)'' | ''1'' = UTF-8 | MSH-18 = ''UNICODE…''; **ohne MSH-18 gilt ISO-8859-1**, nicht UTF-8 |
''Hl7DupCheck'' registriert die Nachricht beim Rückgabewert ''0'' gleich mit — ein zweiter Aufruf mit derselben Control-ID liefert ''1''.
==== std.hl7.adt ====
^ Funktion ^ Zweck ^
| ''Hl7AdtParse(msg, len, adt)'' | Ganze Nachricht: MSH-Kopf, PID, PV1, MRG, NK1 in eine ''Hl7Adt''-Struct |
| ''Hl7AdtWrite(adt, event, out, outMax)'' | Nachricht aus der Struct schreiben; ''event'' z. B. ''%%"A01"c%%'' |
| ''Hl7PidParse'' · ''Hl7Pv1Parse'' · ''Hl7MrgParse'' · ''Hl7Nk1Parse'' | Einzelsegment parsen (Zeiger auf das Segment, nicht auf die Nachricht) |
| ''Hl7AdtIsMerge(eventPtr, eventLen)'' | ''1'' bei A34–A45 (Merge-/Move-Ereignisse) |
==== std.hl7.orders ====
^ Funktion ^ Zweck ^
| ''Hl7OrmParse'' · ''Hl7OmlParse'' | Auftragsnachricht in eine ''Hl7Order''-Struct |
| ''Hl7RasParse(msg, len, admin)'' | Medikamentengabe in eine ''Hl7RxAdmin''-Struct |
| ''Hl7OrcParse'' · ''Hl7ObrParse'' · ''Hl7RxaParse'' | Einzelsegmente |
| ''Hl7OrmWrite'' · ''Hl7OmlWrite'' | Auftrag schreiben |
| ''Hl7OrrWrite(order, status, out, outMax)'' | Auftragsquittung ''%%ORR^O02%%'' |
''HL7_ORD_MSGTYPE'' unterscheidet die Herkunft (''1'' = ORM, ''2'' = OML), ''HL7_ORD_ISSTAT'' meldet einen STAT-Auftrag (OBR-5 = ''S'').
==== std.hl7.results ====
^ Funktion ^ Zweck ^
| ''Hl7OruParse'' · ''Hl7OulParse'' | Befundkopf (OBR + PID) in eine ''Hl7Report''-Struct |
| ''Hl7ObxParse(seg, segLen, obx)'' | Ein OBX-Segment; je Beobachtung einmal aufrufen |
| ''Hl7NteParse(seg, segLen, note)'' | Kommentarsegment |
| ''Hl7OruWrite(report, obx, out, outMax)'' | Befund schreiben |
''HL7_RPT_MSGTYPE'' ist ''HL7_RPT_TYPE_ORU'' (1) oder ''HL7_RPT_TYPE_OUL'' (2). ''HL7_OBX_ISCRIT'' markiert Panikwerte (OBX-8 ''HH''/''LL''), ''HL7_OBX_ISCORR'' korrigierte Ergebnisse (OBX-11 ''C'').
==== std.hl7.scheduling ====
^ Funktion ^ Zweck ^
| ''Hl7SiuParse(msg, len, appt)'' | Terminnachricht in eine ''Hl7Appointment''-Struct |
| ''Hl7SiuWrite(appt, out, outMax)'' | Termin schreiben |
| ''Hl7SchParse'' · ''Hl7AisParse'' · ''Hl7AigParse'' · ''Hl7AilParse'' | Einzelsegmente: Termin, Leistung, Ressource, Ort |
----
===== Rückgabewerte =====
^ Konstante ^ Wert ^ Bedeutung ^
| ''HL7_OK'' | 0 | erfolgreich |
| ''HL7_ERR_TRUNC'' | 1 | Nachricht abgeschnitten |
| ''HL7_ERR_SYNTAX'' | 2 | Segment nicht lesbar (z. B. kein ''MSH'' am Anfang) |
| ''HL7_ERR_DUP'' | 3 | Duplikat |
| ''HL7_ERR_OVERFLOW'' | 4 | Zielpuffer zu klein |
| ''HL7_ERR_VERSION'' | 5 | HL7-Version nicht unterstützt |
Die Schreib-Funktionen (''…Write'') melden Überlauf dagegen mit **''-1''** als Rückgabewert, nicht mit ''HL7_ERR_OVERFLOW'' — der Rückgabewert ist dort die geschriebene Bytezahl.
''HL7_ACK_AA'' / ''HL7_ACK_AE'' / ''HL7_ACK_AR'' (1/2/3) sind Kennzahlen für eigene Zustandslogik. ''Hl7AckWrite'' erwartet **nicht** sie, sondern den zweibuchstabigen Code als ''pchar'' — seit [[https://github.com/SEOLizer/LyX-Compiler/issues/1617|#1617]] ohne Cast: ''%%"AA"c%%''.
----
===== Struct-Größentabelle =====
^ Struct ^ Konstante ^ Bytes ^ Unit ^ Zweck ^
| Hl7Msh | ''HL7_MSH_SIZE'' | 192 | core | MSH-Header einer beliebigen Nachricht |
| Hl7Slot | ''HL7_SLOT_SIZE'' | 528 | core | Ein Partner-Slot des Dedup-Rings (16 + 64×8) |
| Hl7State | ''HL7_STATE_SIZE'' | 8448 | core | Dedup-State (16 Partner × 528 Bytes) |
| Hl7Pid | ''HL7_PID_SIZE'' | 176 | adt | Patient Identification |
| Hl7Pv1 | ''HL7_PV1_SIZE'' | 160 | adt | Patient Visit / Stationsbelegung |
| Hl7Mrg | ''HL7_MRG_SIZE'' | 48 | adt | Patient Merge |
| Hl7Nk1 | ''HL7_NK1_SIZE'' | 48 | adt | Next of Kin / Angehörige |
| Hl7Adt | ''HL7_ADT_SIZE'' | 544 | adt | Vollständige ADT-Nachricht (Kopf + PID + PV1 + MRG + NK1) |
| Hl7Order | ''HL7_ORD_SIZE'' | 192 | orders | Auftragsstruktur (ORC + OBR + PID + PV1) |
| Hl7RxAdmin | ''HL7_RXA_SIZE'' | 112 | orders | Medikamentengabe (RXA) |
| Hl7ObxResult | ''HL7_OBX_SIZE'' | 144 | results | Einzelbeobachtung mit Panikwert-Flag |
| Hl7NteNote | ''HL7_NTE_SIZE'' | 16 | results | Kommentar (NTE) |
| Hl7Report | ''HL7_RPT_SIZE'' | 104 | results | Befundkopf (OBR + PID, ohne OBX) |
| Hl7Sch | ''HL7_SCH_SIZE'' | 128 | scheduling | Terminsegment (SCH) |
| Hl7Ais | ''HL7_AIS_SIZE'' | 48 | scheduling | Terminleistung (AIS) |
| Hl7Aig | ''HL7_AIG_SIZE'' | 48 | scheduling | Terminressource (AIG) |
| Hl7Ail | ''HL7_AIL_SIZE'' | 32 | scheduling | Terminort (AIL) |
| Hl7Appointment | ''HL7_APPT_SIZE'' | 152 | scheduling | Vollständige SIU-Nachricht |
Die eingebetteten Structs der ADT-Nachricht liegen an festen Offsets: ''HL7_ADT_PID'' = 112, ''HL7_ADT_PV1'' = 288, ''HL7_ADT_MRG'' = 448, ''HL7_ADT_NK1'' = 496. Ein Feld daraus wird also mit ''%%peek64(adt + HL7_ADT_PID + HL7_PID_FAMILY)%%'' gelesen.
----
===== Quickstart =====
Empfangen, prüfen, parsen, quittieren — der vollständige Weg einer ADT-Nachricht:
import std.hl7.core;
import std.hl7.adt;
import std.alloc;
import std.string;
// Zero-Copy: die Struct-Felder zeigen in den Nachrichtenpuffer und sind
// NICHT nullterminiert. Fuer die Ausgabe eine Kopie mit \0 anlegen.
fn Feld(ptr: int64, len: int64): pchar {
var buf: int64 := alloc(len + 1);
var i: int64 := 0;
while (i < len) { poke8(buf + i, peek8(ptr + i)); i := i + 1; }
poke8(buf + len, 0);
return buf as pchar;
}
fn main(): int64 {
// Dedup-State: einmalig pro Server-Instanz
var state: int64 := alloc(HL7_STATE_SIZE);
Hl7StateInit(state);
// Eine Nachricht, wie sie aus dem Socket kommt (hier selbst gerahmt)
var msg: pchar := "MSH|^~\\&|HIS|KH|LIS|LAB|20260813081500||ADT^A01|MSG001|P|2.5.1\rPID|||PAT123^^^MPI||Muster^Max||19850315|M\rPV1||I|ITS^201^A^KH|||||||||||||||V4711\r"c;
var tcpBuf: int64 := alloc(65536);
var tcpLen: int64 := MllpFrameWrite(tcpBuf, 65536, msg as int64, StrLen(msg));
// MLLP-Frame auspacken
var msgBuf: int64 := alloc(65536);
var msgLen: int64 := MllpFrameRead(tcpBuf, tcpLen, msgBuf, 65536);
if (msgLen < 0) { return 1; }
// MSH parsen und pruefen
var msh: int64 := alloc(HL7_MSH_SIZE);
if (Hl7MshParse(msgBuf, msgLen, msh) != HL7_OK) { return 1; }
if (Hl7VersionCheck(msh) != HL7_OK) { return 1; }
if (Hl7IsTestMsg(msh) == 1) { PrintLn("Testnachricht - nicht speichern"); }
// Doppelte Nachricht? (Partnerschluessel: sendende Anwendung)
var sendApp: int64 := peek64(msh + HL7_MSH_SEND_APP);
var ctrlId: int64 := peek64(msh + HL7_MSH_CTRL_ID);
if (Hl7DupCheck(state, sendApp, ctrlId) == 1) { PrintLn("Duplikat"); return 0; }
// ADT vollstaendig parsen
var adt: int64 := alloc(HL7_ADT_SIZE);
Hl7AdtParse(msgBuf, msgLen, adt);
var famPtr: int64 := peek64(adt + HL7_ADT_PID + HL7_PID_FAMILY);
var famLen: int64 := peek64(adt + HL7_ADT_PID + HL7_PID_FAMILY_LEN);
var bedPtr: int64 := peek64(adt + HL7_ADT_PV1 + HL7_PV1_LBED);
var bedLen: int64 := peek64(adt + HL7_ADT_PV1 + HL7_PV1_LBED_LEN);
PrintLn(StrConcat("Nachname: ", Feld(famPtr, famLen)));
PrintLn(StrConcat("Bett: ", Feld(bedPtr, bedLen)));
// ACK bauen und rahmen
var ackBuf: int64 := alloc(512);
var ackLen: int64 := Hl7AckWrite(msh, "AA"c, "OK"c, ackBuf, 512);
var outBuf: int64 := alloc(600);
var outLen: int64 := MllpFrameWrite(outBuf, 600, ackBuf, ackLen);
PrintLn(StrConcat("ACK-Bytes: ", IntToStr(outLen)));
free(outBuf, 600); free(ackBuf, 512); free(adt, HL7_ADT_SIZE);
free(msh, HL7_MSH_SIZE); free(msgBuf, 65536); free(tcpBuf, 65536);
free(state, HL7_STATE_SIZE);
return 0;
}
Nachname: Muster
Bett: A
ACK-Bytes: 66
Das ''Bett'' kommt aus PV1-3.3: die Ortsangabe ''%%ITS^201^A^KH%%'' zerlegt der Parser in Station (''ITS''), Zimmer (''201''), Bett (''A'') und Einrichtung (''KH'').
----
===== Fallstricke =====
* **Segmente trennt ''\r'', nicht ''\n''.** Eine mit Zeilenumbrüchen aufgebaute Nachricht wird nicht geparst; der Parser sucht CR (0x0D).
* **Zeiger überleben den Puffer nicht.** Wird ''msgBuf'' freigegeben oder wiederbenutzt, zeigen alle Felder der Structs ins Leere. Werte, die länger gebraucht werden, vorher kopieren.
* **Zeichensatz prüfen.** Ohne MSH-18 gilt ISO-8859-1. Umlaute aus einem solchen Strom sind kein UTF-8 — ''Hl7IsUtf8'' fragen, bevor der Text weitergereicht wird.
* **Testnachrichten erkennen.** ''Hl7IsTestMsg'' vor jedem Schreibzugriff auf die Produktivdatenbank aufrufen; MSH-11 ''T'' kommt in Kopplungstests regelmäßig vor.
* **Dedup-Ring ist begrenzt.** 16 Partner × 64 Control-IDs. Bei mehr Partnern verdrängt der Ring die ältesten Einträge — die Duplikatserkennung ist eine Nahbereichs-Sicherung, kein Ersatz für eine persistente Nachrichtentabelle.
* **''Hl7AckWrite'' erwartet den Code als Text** (''%%"AA"c%%''), nicht die Konstante ''HL7_ACK_AA''.
----
**Weiterführend:** [[lyx_-_programmiersprache:units:hl7:core|std.hl7.core]] · [[lyx_-_programmiersprache:units:hl7:adt|std.hl7.adt]] · [[lyx_-_programmiersprache:units:hl7:orders|std.hl7.orders]] · [[lyx_-_programmiersprache:units:hl7:results|std.hl7.results]] · [[lyx_-_programmiersprache:units:hl7:scheduling|std.hl7.scheduling]] · [[lyx_-_programmiersprache:units:net|std.net]] · [[lyx_-_programmiersprache:sprache:rohspeicher|Rohspeicher]]
Letzte Aktualisierung: 2026-08-13 — Tabellen repariert (''%%^%%'' in Zellen maskiert), „Vier Units" → fünf, Funktionsübersicht je Unit, Rückgabewerte, Größentabelle um die scheduling-Structs und ''HL7_SLOT_SIZE'' ergänzt, Quickstart lauffähig gemacht (er benutzte eine nicht deklarierte ''tcpBufLen''). Alles gegen ''std/hl7/*.lyx'' und ''lyxc 1.0.21A'' geprüft.
Codebeispiele geprüft: gegen **lyxc 1.2.5C** übersetzt (Prüflauf 2026-09-08 über die gesamte Doku: 574 Vollprogramme, 0 echte Fehler; zusätzlich 5159 Aufrufe gegen die ''pub fn''-Signaturen in ''aurum/std'' gehalten, 0 Abweichungen).