Analyzers as they behave¶
Taken from captures of live laboratories, not from documentation. Each is
reproduced as a fixture under src/app/testing/fixtures/captured/ and replayed
by src/app/services/analyzer-captures.spec.ts, so a change that alters what a
real analyzer's message stores fails there first.
Field numbers count the record type as field 1, the way the parser indexes
them: in O|1|S1|S1^RUN^A1, O.3 is S1 and O.4 is S1^RUN^A1.
Cepheid GeneXpert — ASTM¶
- The
Hrecord declares its delimiters as@^\rather than\^&. - One message per test, each in its own session.
- Frames are filled to 240 bytes and cut wherever that falls, so records
split across
<ETB>continuation frames, routinely mid-field. O.3is whatever the operator typed as the sample ID — sometimes a patient name, sometimes a free-text note.R.1carries the assay result with a trailing component separator:NOT DETECTED^,DETECTED^,ERROR^,INVALID^. For viral load the number is in the second component (^1234.56),R.5is the unit andR.6the reportable range.- A
Crecord explains an error:Error^code^title^description^timestamp. O.26isFeven for a run that errored, so the result text and the comment are what say the run failed — not the status field.
Locale changes everything except the shape¶
A GeneXpert 6.5 running in French sends NON DÉTECTÉ, DÉTECTÉ, ERREUR,
PAS DE RÉSULTAT, with RÉUSSITE/NÉG/POS on control records, and writes
every number with a decimal comma — in the result, the reportable range, and
every Ct and endpoint. Its error comments are French and contain a no-break
space. A viral load can come back DÉTECTÉ with no number at all, flagged
< in R.7, meaning detected below the reportable range — so the unit alone
does not tell you whether there is a number to read.
Abbott m2000 — ASTM¶
- A whole run in one session: one
H, thenP/O/Rper specimen. O.4isspecimenId^run^well; only the first component is the identifier.- Results include
Not detected,< 40, and plain counts. - A failed order arrives with no
Rrecord at all andO.26=X. The explanation is in aCrecord, e.g.4442 : Internal control cycle number is too high. - Empty sessions —
<ENQ>then<EOT>with nothing between — are normal between runs, as is a stray NUL byte.
Roche COBAS AmpliPrep/TaqMan 96 — ASTM¶
Taken from AMPLILINK 3.3.5 and 3.3.7 logs, which hold the records but not the framing.
- One message per sample:
H,P,O, oneR, severalC,L. A session can carry a whole batch. R.4is the result as AMPLILINK wrote it, and the format depends on the version. 3.3.5 prints2.52E+3 (3.40), a mantissa with its log value, andR.5iscp/mL. 3.3.7 sends the unrounded number,1035.95864507225, andR.5iscp/ml.Target Not Detectedhas an empty unit in both.- Sample IDs are free text, double spaces included.
- The
Crecords carryAcceptedand the instrument flags, such asTM40^ STEP_CORR-2. The notes keep the text of each,STEP_CORR-2, and drop the flag code. - The order record stops at
O.12, so it has noO.26report type, which is where every ASTM analyzer's final status is read from. TaqMan results are therefore stored as not final. TaqMan does mark each resultV(verified) inR.9, but nothing reads that field today. O.5names the test^^^ALL, so the test type is stored asALL. The assay code,HI2CAP96, is only inR.3.- Both of those are open until a TaqMan laboratory confirms what its LIS expects.
- Checksums are unconfirmed. An AMPLILINK simulator trace shows standard frames with checksums, and the laboratory setup is remembered as sending none. Both are tested. Choose the ASTM protocol that matches what the instrument actually sends.
- In every log, the LIS asked for results with a
Qrecord before AMPLILINK sent them. This tool does not send queries, so AMPLILINK has to be set to send results without being asked.
Abbott Alinity m — HL7¶
SPM.2andSPM.3are always empty. The sample identifier is inSAC.3. Reading it from the specimen segment, as most analyzers would have it, stores an empty sample ID.OBX.6is^Copies/mL: the identifier component empty, the text filled.- Sixteen
INVsegments and anNTEsit between the resultOBXand the supplemental ones. - Not-detected and below-range outcomes come through as text rather than numbers.
Roche cobas 4800, 5800, 6800/8800 — HL7¶
All three send OUL^R22 over MLLP and disagree about where things are.
- The
&ROCHEsuffix is a 4800 and 5800 habit. Both sendSPM.2as<id>&ROCHE, and it is stripped to get the identifier; the 5800 repeats the plain identifier inSAC.3. The 6800/8800 sends a plainSPM.2with no suffix, and leavesSPM.1empty. - 6800/8800: four
OBXper sample. The first carries the result — a value, orValueNotSetwith a flag inOBX.8(ND,BT,RR,NR). The second isNAwith Ct values, and the fourth the textual outcome. An invalid run has an empty secondOBX, flags such asP02T, andOBX.11=X. - 5800: a quantitative result is
NMwith a three-digit mantissa inOBX.5and a UCUM power of ten inOBX.6— 367 with10*-1.{copies}/mL. Both are stored as sent; multiplying them out is the LIS's job, not this tool's. - 4800: one message per run, every sample in it. Each specimen group
carries two
OBX. The first is a run-time range; the second is the result, as text:3.26E+05 cp/mLwith unit1/mL^^UCUM, orTarget Not Detected,< Titer min,> Titer max,Invalid,Failed. The qualitative assay0BHIV1QUALreportsNot Detected,Detected, andValidon its control. - 4800 result status:
OBX.11isF,Xfor a failed run, orP.Pis how at least one laboratory reports: nearly every sample it sent asPwas never sent again asF. HL7 results are therefore all stored as final. - 4800 flags:
NTE1 carries the run flags (F;X2,X3,F;R3223,X2). They are what explains aFailedorInvalidresult, and they are not kept in the notes today. - 4800
> Titer maxwas always rewritten to> 10000000in code (and>10000000by older versions). It is now a result rule: HL7 instruments configured before 4.5.0 start with it, new ones store> Titer maxas sent. - A 4800 control can fail before it is identified:
SPM.2is&ROCHEandSAC.3is empty. Its sample ID is stored empty. - Instrument errors and flags arrive as text and are stored as
Failedwith the detail kept in the notes.
The generic choices¶
Other ASTM (with checksum), Other ASTM (without checksum) and Other HL7
parse the standards without model-specific field mapping. An analyzer that is in
the list above will store results under a generic choice, but fields that depend
on knowing the dialect — which component holds the specimen ID, what an empty
result field means — may come through empty.
Adding an analyzer¶
- Capture what it actually sends. The stored raw transmissions are the source; a vendor's document is a description of them.
- Anonymise: identifiers, operator names, serial numbers, dates. Record layouts, value formats and framing stay exactly as transmitted.
- Add it as a fixture under
src/app/testing/fixtures/captured/, with a header comment saying what the capture showed. - Replay it in
analyzer-captures.spec.ts, asserting the stored rows. - Only then write the mapping.