Skip to main content

ASTM Channels

Clinical analyzers — Bio-Rad, Beckman, Roche, and many others — commonly speak ASTM E1381 (the link layer) carrying ASTM E1394 (the record format). The Agent handles these with a byte stream channel, configured entirely through query parameters on the channel's Endpoint.address.

This page covers the configuration those analyzers need, and the behaviors that most often surprise people integrating one for the first time.

:::info Transport: raw TCP only

Everything here describes ASTM carried over a raw TCP stream. A byte stream channel listens on a TCP port, so an analyzer that presents a serial (RS-232) interface — still the majority — has to reach the Agent through a serial-to-TCP/IP adapter, sometimes sold as a serial device server or terminal server. The adapter passes bytes through untouched, so the ASTM framing and the configuration below are unaffected by it.

Connecting to a serial port directly from the Agent is not yet supported, but is planned. Until then an adapter is required for serial-only analyzers.

:::

tcp://0.0.0.0:9004
?startChar=%05&endChar=%04
&autoRespond=%05:%06,%03:%06,%17:%06
&stripControlChars=true&keepControlChars=%0D%0A
&bodyEncoding=utf-8
&ignoreResponse=true

(Shown wrapped for readability; the real value is a single line.)

ParameterWhy an ASTM channel needs it
startChar=%05&endChar=%04Frames on ENQ/EOT, so one ASTM session becomes one message. See Framing.
autoRespond=%05:%06,%03:%06,%17:%06Answers ENQ, ETX, and ETB with ACK, which is the full set of bytes E1381 acknowledges. See Acknowledgement.
stripControlChars=trueRemoves the ENQ, STX, ETX/ETB and EOT framing, leaving readable records.
keepControlChars=%0D%0AExempts CR and LF from that sweep. ASTM records are CR-terminated; without this the whole message is one run-on line.
bodyEncoding=utf-8Delivers the body to the Bot as text. The default, hex, would require the Bot to decode it first.
ignoreResponse=trueMakes the channel one-way, for analyzers that never read a reply. See One-way channels.

Framing: one session per message

An ASTM session looks like this on the wire:

--> ENQ
<-- ACK
--> STX 1 H|\^&|||... CR ETX C1C2 CR LF
<-- ACK
--> STX 2 P|1|... CR ETX C1C2 CR LF
<-- ACK
--> EOT

Framing on ENQ/EOT (startChar=%05&endChar=%04) makes the Agent treat everything between them as a single message, so the Bot receives the complete result set — H through L — in one invocation.

:::danger Do not frame on STX/ETX

startChar=%02&endChar=%03 looks natural, since STX and ETX delimit each frame. It will deliver one message per frame, so a Bot that needs the header, order, and result records together sees them split across several invocations with no way to correlate them.

:::

E1381 expects exactly one ACK per frame. autoRespond answers these at the byte level, before framing and without involving the Bot, which is what lets an analyzer proceed without waiting on a round trip to the server.

ByteRuleAcknowledged?Notes
ENQ (0x05)%05:%06YesEstablishment.
ETX (0x03)%03:%06YesTerminates the final frame of a message.
ETB (0x17)%17:%06YesTerminates an intermediate frame of a split message.
EOT (0x04)NoNever acknowledged; the sender has already dropped to idle.
LF (0x0A)NoNot an ASTM acknowledgement point.

Two mistakes are easy to make here:

  • Forgetting %17:%06. Records longer than a frame are split, with every frame but the last terminated by ETB rather than ETX. Without a rule for ETB those frames go unanswered and the analyzer stalls, retransmits, and eventually aborts the session. Short messages work fine, so this often surfaces only once a larger result set arrives.
  • Mapping %0A:%06 or %04:%06. Every frame ends CR LF, so a rule on LF produces a second ACK for each frame. A sender expecting one reply per frame then runs permanently one ahead, reading frame N's ACK as the answer to frame N+1 — which means a frame that was actually rejected can be read as accepted. A rule on EOT sends an unsolicited ACK into the idle state, which stricter analyzers log as a protocol violation.

:::caution autoRespond cannot validate frames

It matches bytes; it does not parse frames or verify checksums, so it ACKs a corrupt frame exactly as readily as a good one and can never send a NAK. Checksum-driven retransmission is not available through this mechanism. If an analyzer depends on it, validate in the Bot and reconcile out of band.

:::

What the Bot receives

Filtering operates on bytes, not frames, so everything printable in the analyzer's framing survives. A session arrives looking like this:

1H|\^&|||Bio-Rad CDM System SN 12345^...|||||LIS||P|1|20260806041500<CR>
P|1||||Doe^John||19700101|M<CR>
O|1|511979^002^01^0002||^^^4||...<CR>
R|9|^^^A1c^AREA|5.2|NGSP|...<CR>
L|1|N<CR>
D5<CR><LF>

Two artifacts to plan for:

  1. Frame sequence numbers. The 1 ahead of H is the frame number. It is an ASCII digit, so stripControlChars cannot remove it.
  2. Checksums. The trailing D5 is the frame checksum. Splitting on CR leaves it on a line of its own, where most parsers harmlessly ignore it.

Frame packing changes which lines carry a sequence number

Only the first record in each frame is prefixed with the frame number. An analyzer that packs a whole message into one frame produces exactly one prefix, on the H record — so startsWith('O|1|') works. The same analyzer splitting that message across frames prefixes the first record of every frame, and the same check silently stops matching.

Do not rely on the packing staying constant; it changes with result-set size, retransmissions, and firmware. Normalize instead:

const lines = input
.split(/\r\n?/)
// Strip a frame sequence number only where one precedes a record type letter.
.map((line) => line.replace(/^[0-7](?=[A-Z]\|)/, ''));

One-way channels

Most analyzers send results and never read a reply — the link-level ACKs already told them the message was accepted. Set ignoreResponse=true so the Bot's response is discarded rather than written back to the socket.

Without it, every message writes something to the analyzer, and it is often not what you would expect: when a Bot returns no value, the server substitutes the Bot's execution logs as the response body. An analyzer sitting idle then receives kilobytes of unframed log text.

tip

An empty string is a falsy return value, so returning '' still produces the execution-log fallback. ignoreResponse=true is the reliable way to send nothing.

Sessions with no data

An analyzer with nothing to report may still open a session — ENQ, take the ACK, then EOT. Both framing bytes are removed by stripControlChars, so what remains is a zero-length body, and the Agent dispatches it to the Bot like any other message.

Guard against it at the top of the Bot rather than paying for a full execution per poll:

if (!input?.trim()) {
return;
}

Verifying a new channel

When bringing up an analyzer, confirm each layer separately — a failure at any one looks like "nothing happened" from the outside:

  1. Link layer. Watch the ACKs. Exactly one per frame, and none after EOT. Extra or missing ACKs point at the autoRespond rules.
  2. Framing. Log the body's length and whether it contains CR. If there is no CR, check keepControlChars. If the body starts with a control byte instead of a record type letter, check stripControlChars.
  3. Parsing. Log each line and which branch matched it. A (no match) on a record you expected to match is usually a frame sequence number, per Frame packing.

A Bot that logs these and writes nothing is worth keeping around; channel logs and the Bot's AuditEvent history together cover all three layers.