Skip to main content
Version: v0.1.6

Decode a framed byte stream

Over a serial line, records always arrive in frames. Over TCP it depends on the analyzer: some keep the full framing, and some send bare records. This page finds out which you have, decodes frames, and parses the records they carry.

Find out whether the stream is framed​

detectFraming reads the leading byte. STX or ENQ means framed; a record type letter means raw:

import { detectFraming } from "@cosyte/astm";

const framed = new Uint8Array([0x05]); // ENQ, the start of a transfer
const raw = new TextEncoder().encode("H|\\^&\rL|1\r");
const unclear = new TextEncoder().encode("??");

detectFraming(framed).framing; // => "framed"
detectFraming(raw).framing; // => "raw"
detectFraming(unclear).framing; // => "framed"
detectFraming(unclear).defaulted; // => true
detectFraming(unclear).warnings[0]?.code; // => "ASTM_LTP_AMBIGUOUS_TRANSPORT"
detectFraming(unclear, { override: "raw" }).framing; // => "raw"

On a leading byte it cannot place, it answers framed, sets defaulted, and warns, rather than guessing quietly. When you know the analyzer, pass override, or set transport in a vendor profile. A raw stream goes straight to parseAstmRecords.

Decode the frames​

decodeAstmFrames returns the frames, the record bytes they carried, and the frame warnings:

import { decodeAstmFrames } from "@cosyte/astm";

// One final (ETX) frame carrying the record text "L|1\r", with its correct checksum "3A".
const bytes = new Uint8Array([0x02, 0x31, 0x4c, 0x7c, 0x31, 0x0d, 0x03, 0x33, 0x41, 0x0d, 0x0a]);
const { frames, records, warnings } = decodeAstmFrames(bytes);

frames[0]?.frameNumber; // => 1
frames[0]?.terminator; // => "ETX"
frames[0]?.checksum.valid; // => true
frames[0]?.trusted; // => true
records.length; // => 1
warnings; // => []
  • The checksum is the modulo-256 sum of the frame's bytes. It is written in upper case and accepted in lower case too, since some vendors send it that way.
  • A long record arrives split across frames: each intermediate frame ends with ETB and the last with ETX. The decoder joins them back into one record.
  • Frame numbers run 1 to 7, then 0, and around again. The decoder checks that each frame is the next one.

Parse only what the frames vouched for​

parseFramedAstm decodes the frames and parses the records in one call. Only frames that passed every check reach the record parser:

import { parseFramedAstm, results, serializeFramedAstm, parseAstmRecords } from "@cosyte/astm";

const framed = serializeFramedAstm(
parseAstmRecords("H|\\^&\rR|1|^^^687|28.6|U/L||N||F\rR|2|^^^688|5.1|mmol/L||N||F\rL|1\r"),
);

// Change one byte of the first result on the way.
const damaged = framed.slice();
damaged[new TextDecoder("latin1").decode(framed).indexOf("28.6")] = 0x33;

const { message, frameWarnings } = parseFramedAstm(damaged);
frameWarnings.map((w) => w.code); // => ["ASTM_FRAME_BAD_CHECKSUM"]
results(message).map((r) => r.value); // => ["5.1"]

The damaged frame is kept in frames with trusted: false, for audit, and its text never reaches a record. Read frameWarnings on every call: the record layer cannot report a record it never saw.

Know what each frame warning means​

CodeWhat happenedWhat the decoder did
ASTM_FRAME_BAD_CHECKSUMthe checksum did not match the frame's byteskept the frame untrusted, merged nothing
ASTM_FRAME_SEQUENCE_GAPa frame number was not the next one, so a frame may be missingdid not join records across the gap
ASTM_FRAME_UNTERMINATEDa frame, or a record's run of frames, ended without its last endkept the bytes untrusted, invented no record
ASTM_FRAME_OVERSIZEa frame carried more than 240 bytes of text without a splitkept it, and joined it if its checksum was correct

Each warning carries a frame number and a byte offset, never the record bytes. In the default lenient mode they are warnings; with { strict: true } they are a thrown AstmFrameStrictError that carries them.

Reference​