The interface engine
The integration plane is the plumbing that turns activity into a chart. It is also the plane most systems get wrong by skipping it — pointing an HL7 feed straight at the EMR. Real hospitals put an interface engine in the middle, and so does the lab.
Why a separate engine
Section titled “Why a separate engine”OpenEMR’s strength is being the clinician-facing record; its native HL7v2 inbound is weak. An interface engine’s whole job is to receive, acknowledge, transform, and route messages. We use OpenIntegrationEngine (OIE), the FOSS fork that continues Mirth Connect after it went proprietary at version 4.5. Each piece then stays good at one job:
Simulated Hospital ──MLLP (HL7v2)──▶ OpenIntegrationEngine ──JDBC──▶ OpenEMR (activity) (integration) (application)The channel
Section titled “The channel”OIE runs a single channel, ADT to OpenEMR, generated by
scripts/gen-channel.py:
- Source — a TCP/MLLP listener on port 6661. HL7v2 in, auto-generated ACK out.
- Destination — a JavaScript Writer that parses each message and writes to the
OpenEMR MariaDB over JDBC, computing ids from OpenEMR’s own
sequencescounter.
Every write is idempotent (keyed on MRN, visit number, or a per-patient title), so replays and overlapping messages don’t duplicate data.
What each segment becomes
Section titled “What each segment becomes”| HL7 segment / event | OpenEMR destination |
|---|---|
PID | patient_data (incl. SSN from the SS-typed PID-3 repetition, address from PID-11, phone from PID-13) |
PV1 | form_encounter + forms (visit, keyed on visit number) |
OBR / OBX (Vital Signs) | form_vitals + forms — temp (°C→°F), pulse, BP, SpO₂ into columns; the rest into the note |
OBR / OBX (clinical note, encapsulated ^^txt^^ text) | pnotes — the note body, titled by document type (Referral Letter, Prescription…) |
OBR / OBX (other panels) | procedure_order → procedure_report → procedure_result (labs) |
AL1 | lists type allergy |
DG1 | lists type medical_problem |
PR1 | lists type surgery |
IN1 | insurance_data (+ lookup-or-create insurance_companies by name) |
GT1 | the insurance_data subscriber (a guarantor who isn’t the patient — e.g. a minor’s parent — becomes the policyholder) |
ADT^A02 | transfer — updates the encounter location |
ADT^A03 | discharge — sets date_end + disposition |
ADT^A11 / A13 | cancel admit / cancel discharge |
Hard-won notes on Mirth/OIE channel XML
Section titled “Hard-won notes on Mirth/OIE channel XML”Authoring channel XML by hand is finicky. These cost real time and are written down so they don’t again:
- Use
PUT /api/channels/{id}, notPOST /api/channels. POST silently strips most of the channel body (connector properties, transformer datatypes); PUT preserves them. - The channel
idmust be a real hex UUID. A non-hex id 500s on PUT. - HL7v2 datatype sub-blocks are all-or-nothing per converter. If any one block
has a shape XStream doesn’t recognise, it discards the entire
HL7v2DataTypePropertiesobject —deserializationPropertiesbecomes null and deploy throws a NullPointerException. We ship onlyserializationProperties,deserializationProperties, andresponseGenerationProperties. responseGenerationPropertiesis mandatory for the ACK. Without it, the auto-generated ACK is null, the MLLP sender blocks waiting for a reply, and throughput collapses to a trickle. With it: hundreds of messages/min, zero errors.- No query params in the JDBC URL. The
&collides with XML escaping inside the<script>, and MariaDB’s native-password auth needs none of them.
A latent-bug lesson
Section titled “A latent-bug lesson”Mapping procedures surfaced a bug worth more than the feature: a long
point-of-care value overflowed the class_code column (varchar(10)) and
silently aborted the entire message — patient created, but encounter,
diagnosis, and procedure all lost. Live traffic never hit it because the
simulator sends short codes (SURG, ED); only an oddly-shaped test message
did. That is the exact shape of a bug that hides until one unusual real message
arrives in production. The fix — clamp the value to the column width — costs
nothing and closes the whole class.
The activity plane can also emit FHIR R4 directly (a separate path from this
channel). It works for everything except Observation, due to a library-version
skew in the from-source build — the full story is on the
Simulated Hospital page.
Where this fits
Section titled “Where this fits”This is the middle of the four-plane architecture. To bring it up and deploy the channel, see Run the clinical ecosystem.