Parsers
Without a parser, watch() delivers bytes in batches (about 20 ms, or size
bytes), so a line or packet can be split across onData calls or several can
arrive in one. Pass parser and the native side frames the stream for you:
each complete frame is exactly one onData call, sent as soon as it is
complete (the batch window no longer applies).
await port.watch(
{ onData: (line) => console.log("line:", line) },
{ parser: { type: "readline", delimiter: "\r\n" } },
);
// Binary packets: keep bytes, one Uint8Array per packet.
await port.watch(
{ onData: (frame) => handle(frame as Uint8Array) },
{ decode: false, parser: { type: "startEnd", startDelimiter: [0x02], endDelimiter: [0x03] } },
);
The parsers follow the Node.js @serialport/parser-* packages (their test suites
are ported to Rust in the serial-codec crate), so code written for Node
serialport maps over directly.
Parser types
Byte sequences (ByteSeq) are a UTF-8 string, a number[] or a Uint8Array.
maxBufferSize defaults to 65536 bytes.
type | Node package | Options | Frame |
|---|---|---|---|
delimiter | parser-delimiter | delimiter: ByteSeq (required), includeDelimiter = false, maxBufferSize | Bytes up to each delimiter |
readline | parser-readline | delimiter: ByteSeq = "\n", includeDelimiter = false, maxBufferSize | One line |
byteLength | parser-byte-length | length (> 0) | Exactly length bytes |
interByteTimeout | parser-inter-byte-timeout | intervalMs (alias interval, > 0), maxBufferSize | Everything received before intervalMs of silence |
packetLength | parser-packet-length | delimiter: number | number[] = 0xaa, delimiterBytes = 1 (1–4), packetOverhead = 2, lengthBytes = 1 (1–8), lengthOffset = 1, maxLen = 0xff | Delimiter + little-endian length field + payload |
ready | parser-ready | delimiter: ByteSeq (required) | Nothing until the marker, then data passes through as it arrives |
regex | parser-regex | regex: string (required), maxBufferSize | UTF-8 text between matches |
startEnd | parser-start-end | startDelimiter, endDelimiter (required), includeStartDelimiter = false, includeEndDelimiter = false, maxBufferSize | Bytes between the markers |
slip | parser-slip-encoder (SlipDecoder) | start, esc = 0xdb, end = 0xc0, escStart, escEnd = 0xdc, escEsc = 0xdd | One decoded SLIP (RFC 1055) packet |
ccTalk | parser-cctalk | maxDelayMs = 50 (0 disables) | One ccTalk message (length in byte 1); a partial message is dropped after maxDelayMs |
An invalid configuration (empty delimiter, length: 0, a bad regex, …) rejects
watch() with invalid parser options: … before the watch starts.
Behaviour
- Decoding. With
decode: true(default) every frame is decoded on its own with the portencoding, so one frame is always one string. Usedecode: falsefor binary frames. - Time-based parser.
interByteTimeoutis driven by the native read loop: a frame is emitted when the silence elapses, even if no further bytes arrive.ccTalknever emits on time; it drops a stale partial message (older thanmaxDelayMs) when the next byte arrives. - URC routing. With
routeUrc(on by default whenonUrcis given), AT URC lines are split off first and go toonUrc; only the remaining lines reach the parser. An unterminated line is handed to the parser on the batch timer, the same way it reachesonDatawithout a parser. - End of stream. On
unwatch(),close()or a disconnect, the parser's buffered partial frame (if any) is delivered as a lastonDatacall, beforeonDisconnect. Node emits it the same way when the stream ends.readyandccTalkhave nothing to flush;packetLengthflushes only a started packet. - Auto-reconnect re-creates the watch with the same parser (and a fresh, empty buffer).
- CMUX channels (
openMuxChannel()) take the sameparseroption, but a channel watch ignoresrouteUrcandsize: URC lines on a channel are not split off toonUrc, and batches are not flushed early atsize. - Empty frames are never emitted.
Differences from Node serialport
- Buffers are bounded. A stream that never completes a frame cannot grow memory
forever:
delimiter,readline,regex,interByteTimeoutandslipemit the full buffer as a frame atmaxBufferSize;startEnddrops the overlong open frame (keeping a later start marker). - Frames are bytes. Node's
encodingoption onreadline/regexonly changes how Node renders a frame; heredecodeand the portencodingdo that. readyfinds its marker after a false start ("RREADY"matches"READY"; Node misses it).packetLengthfinds a multi-byte delimiter anywhere in the stream (Node only finds it at stream start or right after a packet). At end of stream only a started packet is flushed. A packet also ends once it is at leastlen + packetOverheadlong, so a header longer than that cannot buffer forever.regexkeeps a UTF-8 character split across reads for the next chunk (Node turns it into U+FFFD). The pattern uses Rustregex-litesyntax as a string, not a JavaScriptRegExp, and capture groups are not spliced into the output the wayString.splitdoes.- Timers come from the native read loop (about 10 ms granularity) instead of JavaScript timers.
Rust
The same parsers are available without the plugin in the
serial-codec crate (serial_codec::framing:
FramerConfig::build() and the Framer trait), and from Rust through
SerialPort::watch_framed / SerialManager::watch_framed(path, options, Some(config), …).