Guides

Parsers

Native framing for watch(): one data event per complete frame, Node serialport parser semantics.

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.

typeNode packageOptionsFrame
delimiterparser-delimiterdelimiter: ByteSeq (required), includeDelimiter = false, maxBufferSizeBytes up to each delimiter
readlineparser-readlinedelimiter: ByteSeq = "\n", includeDelimiter = false, maxBufferSizeOne line
byteLengthparser-byte-lengthlength (> 0)Exactly length bytes
interByteTimeoutparser-inter-byte-timeoutintervalMs (alias interval, > 0), maxBufferSizeEverything received before intervalMs of silence
packetLengthparser-packet-lengthdelimiter: number | number[] = 0xaa, delimiterBytes = 1 (1–4), packetOverhead = 2, lengthBytes = 1 (1–8), lengthOffset = 1, maxLen = 0xffDelimiter + little-endian length field + payload
readyparser-readydelimiter: ByteSeq (required)Nothing until the marker, then data passes through as it arrives
regexparser-regexregex: string (required), maxBufferSizeUTF-8 text between matches
startEndparser-start-endstartDelimiter, endDelimiter (required), includeStartDelimiter = false, includeEndDelimiter = false, maxBufferSizeBytes between the markers
slipparser-slip-encoder (SlipDecoder)start, esc = 0xdb, end = 0xc0, escStart, escEnd = 0xdc, escEsc = 0xddOne decoded SLIP (RFC 1055) packet
ccTalkparser-cctalkmaxDelayMs = 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 port encoding, so one frame is always one string. Use decode: false for binary frames.
  • Time-based parser. interByteTimeout is driven by the native read loop: a frame is emitted when the silence elapses, even if no further bytes arrive. ccTalk never emits on time; it drops a stale partial message (older than maxDelayMs) when the next byte arrives.
  • URC routing. With routeUrc (on by default when onUrc is given), AT URC lines are split off first and go to onUrc; only the remaining lines reach the parser. An unterminated line is handed to the parser on the batch timer, the same way it reaches onData without a parser.
  • End of stream. On unwatch(), close() or a disconnect, the parser's buffered partial frame (if any) is delivered as a last onData call, before onDisconnect. Node emits it the same way when the stream ends. ready and ccTalk have nothing to flush; packetLength flushes only a started packet.
  • Auto-reconnect re-creates the watch with the same parser (and a fresh, empty buffer).
  • CMUX channels (openMuxChannel()) take the same parser option, but a channel watch ignores routeUrc and size: URC lines on a channel are not split off to onUrc, and batches are not flushed early at size.
  • 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, interByteTimeout and slip emit the full buffer as a frame at maxBufferSize; startEnd drops the overlong open frame (keeping a later start marker).
  • Frames are bytes. Node's encoding option on readline / regex only changes how Node renders a frame; here decode and the port encoding do that.
  • ready finds its marker after a false start ("RREADY" matches "READY"; Node misses it).
  • packetLength finds 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 least len + packetOverhead long, so a header longer than that cannot buffer forever.
  • regex keeps a UTF-8 character split across reads for the next chunk (Node turns it into U+FFFD). The pattern uses Rust regex-lite syntax as a string, not a JavaScript RegExp, and capture groups are not spliced into the output the way String.split does.
  • 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), …).