Serial / Ethernet Reader
Overview
This prefab reads raw data from a Serial (UART) port or an Ethernet connection and emits it downstream as BYTES packets. Because the incoming data arrives as a continuous stream with no built-in message boundaries, the prefab frames it according to a schema: an ordered list of steps you configure in the node’s constants panel. Each step consumes some bytes from the stream — scanning for a magic word, reading a fixed or dynamic number of bytes, decoding a length field, and so on. Once the last step in the list completes, the assembled packet is sent on output and the reader restarts from the first step.
The schema is framing only — it has no CRC checking and does not name fields in its output (a step can carry a conditional if, see Conditional steps below, but that’s as far as branching goes). To pull individual fields (like a decoded length or a status byte) out of the emitted packet, use Bytes to Value or Bytes to Map downstream.
How it Works
The reader runs in cycles. Each cycle it reads bytes from the Serial (UART) port / Ethernet connection and feeds them through the compiled schema, one step at a time, in order. A step that scans for a byte sequence (start, until) consumes bytes until it finds a match; a step that reads a fixed or computed number of bytes (skip, length, read) consumes exactly that many. When the last step in the list finishes, the packet is sent on output and the reader jumps back to the first step to begin the next packet.
If a packet cannot be completed within timeout(ms), the cycle ends. A schema consisting of a single listen step flushes whatever bytes have been collected so far as a packet; every other schema discards the partial packet. Either way, bytes already read that were not yet consumed by a step are kept, so the reader resumes scanning from wherever it left off rather than losing data — this is also why a schema is expected to start with a start step: without one, a stream that gets out of sync has no magic word to resynchronize on, and stays desynced until the next timeout.
If the optional trigger input is connected, the reader waits for a message on it before starting each cycle. This lets you gate reading behind another node (for example, read one packet only after a command has been written to the device). If trigger is not connected, the reader runs continuously.
A single packet can be at most 256 KiB (262,144 bytes). A schema whose statically-known bytes already exceed that limit fails to start; at runtime, a packet that would grow past the limit (from a decoded length or an unterminated until) is discarded with a warning and the reader resynchronizes on the next magic word.
On Ethernet, all of the above applies the same way whether network_mode is TCP or MACRAW. In
MACRAW mode the byte stream being framed is just the raw Ethernet frames arriving on the wire (each
including its own Ethernet header) instead of a TCP stream — the schema and trigger gating still
apply on top of it. Any single incoming frame larger than 256 bytes is truncated with a warning. See
Network settings below.
Constants
| Constant | Type | Description |
|---|---|---|
timeout(ms) | NUMBER | Required. Maximum time in milliseconds to complete one packet before the cycle ends (see above). |
schema | JSON | Required. The ordered list of steps described below. Edited through a Form tab in the node’s configuration panel, where each step is added or removed as its own card (new steps are appended at the end); a JSON tab next to it edits the same value as raw text, which is also how you reorder steps, since the form has no reorder control. |
Steps
A schema is a JSON array; each entry is one step, given as { "type": "...", ... }. The node’s configuration panel labels each step type as shown below.
start — scan for magic word
Scans the incoming stream for a fixed byte sequence — or, when several are given, whichever one appears first — and discards everything before it. Typically the first step in a schema, so the reader can resynchronize on this sequence after a discarded or timed-out packet.
| Field | Type | Description |
|---|---|---|
bytes | Byte list, or a list of byte lists | Required. The magic word to scan for. A list of numbers separated by commas or whitespace; square brackets are optional. Both decimal and hexadecimal (0x) notation are accepted — 0xAA, 0x55 is the same as [170, 85] or 170 85. Can also be a list of byte lists, to scan for several magic words at once. |
key | text | Optional. Binds the matched magic word’s 0-based declaration index to this name, for a later step’s if or count expression to reference — the same role a length step’s key plays. |
emit | BOOLEAN | Whether the magic word bytes are included in the emitted packet. Defaults to true. |
if | Condition | Optional. Run this step only when a condition holds. See Conditional steps below. |
Several magic words. Give bytes as a list of byte lists (instead of a single one) to frame several kinds of frames with one schema — for example a sniffed link carrying both a request and a response subheader. Up to 16 alternatives; none may be a prefix of another (a duplicate counts as a prefix of itself), since a chunk holding only the shorter one would be genuinely ambiguous. Whichever alternative appears earliest in the stream wins; a chunk that only partially matches a longer alternative is never pre-empted by a shorter alternative matching further along — the scan waits for more bytes rather than framing on the wrong one.
Worked example. One schema framing both directions of a sniffed link, where only one direction carries a 2-byte trailer:
[
{ "type": "start", "bytes": ["0x50 0x00", "0xD0 0x00"], "key": "kind" },
{ "type": "skip", "count": 5 },
{ "type": "length", "key": "len", "size": 2, "endian": "le" },
{ "type": "read", "count": "len" },
{
"type": "read",
"count": 2,
"if": { "type": "eval", "receiver": "kind", "operator": "==", "datatype": "INT", "value": 1 }
}
]kind is bound to 0 when 0x50 0x00 matches and 1 when 0xD0 0x00 matches, re-bound fresh on every frame. The final guarded read only fires for the second alternative, so a request frame and a response frame with different trailers are both framed correctly by the same schema.
skip — discard bytes
Consumes a number of bytes without including them in the output. Use it to discard header bytes the downstream nodes don’t need.
| Field | Type | Description |
|---|---|---|
count | Count | Required. Number of bytes to discard. See Counts below. Never included in the output, regardless of emit on other steps. |
if | Condition | Optional. Run this step only when a condition holds. See Conditional steps below. |
length — read a byte count
Reads a fixed-width integer from the stream and binds it to a name (key), so a later step’s count can reference it. This is how a length-prefixed protocol declares how many payload bytes follow.
| Field | Type | Description |
|---|---|---|
key | text | Required. Name this value is bound under, for use in a later step’s expression. Letters, digits, and underscore only; must not start with a digit. |
size | NUMBER | Required. Width of the field in bytes: 1, 2, 3, or 4. |
endian | Option | Required. Byte order used to decode the field: be (big-endian — the first byte read is the most significant byte) or le (little-endian — the first byte read is the least significant byte). |
adjustment | NUMBER | A signed correction added to the decoded value before it is bound — useful when the field excludes trailing bytes such as a CRC. Defaults to 0. |
emit | BOOLEAN | Whether the field’s bytes are included in the emitted packet. Defaults to true. |
if | Condition | Optional. Run this step only when a condition holds. See Conditional steps below. |
A schema can have more than one length step — for example, a protocol whose header carries a parameter length and, separately, a data length. Each is bound under its own key and can be read independently by a later read step.
If the decoded value plus adjustment is negative, the packet is discarded with a warning and the reader resynchronizes.
read — append bytes
Reads a number of bytes and includes them in the output. This is where the payload described by an earlier length step is actually consumed.
| Field | Type | Description |
|---|---|---|
count | Count | Required. Number of bytes to read. See Counts below. |
if | Condition | Optional. Run this step only when a condition holds. See Conditional steps below. |
until — read to end marker
Reads and includes bytes until a fixed byte sequence appears, for protocols that terminate packets with a marker (for example, ASCII protocols ending in \r\n).
| Field | Type | Description |
|---|---|---|
bytes | Byte list | Required. The end marker to scan for. Same format as start’s bytes. |
emit | BOOLEAN | Whether the end marker itself is included in the emitted packet. Defaults to true. |
escape | Object | Optional. See Escaping below. |
if | Condition | Optional. Run this step only when a condition holds. See Conditional steps below. |
listen — flush on size or timeout
Passively collects every byte that arrives, with no framing at all. A packet is emitted once max_bytes bytes have been collected, or when timeout(ms) elapses — in which case whatever has been collected so far (if anything) is flushed as a packet. This is the mode to use when the device’s protocol has no framing, or when you just want to observe the raw stream.
A listen step must be the only step in the schema — it cannot be combined with any other step, and for that reason cannot carry an if guard either (there is no earlier length step for it to depend on).
| Field | Type | Description |
|---|---|---|
max_bytes | NUMBER | Required. Number of bytes to collect before emitting a packet. Must be greater than 0. |
Counts
skip, length, and read all take a byte count, which can be given two ways:
- A fixed number — a literal count of bytes, known ahead of time. Must be at least
1(aread/skipof0bytes is rejected when the schema is compiled). - An expression — a string naming one or more
keys bound by earlierlengthorstartsteps in the same schema, optionally combined with arithmetic (+,-,*,/) and constants — for example"len","total - hlen", or"words * 2". An expression must reference at least akey; a string that only contains arithmetic on constants (like"1 - 1") is rejected, since it could let a schema emit empty packets forever. Akeycan only be referenced by a step that comes after the step that binds it — a forward reference fails when the schema is compiled.
A malformed schema (a bad expression, a forward reference, a duplicate key, and so on) is caught when the node starts, before it reads any data.
Output framing (emit)
Every step that consumes bytes from the stream decides, independently, whether those bytes also become part of the packet sent on output:
start,length,until— carry an optionalemitfield,trueby default. Set it tofalseto consume and validate those bytes (scan for the magic word, decode the length field, scan for the end marker) without including them in the output.skip— has noemitfield. Its bytes are always discarded; that’s the whole point of the step.read,listen— have noemitfield either, but for the opposite reason: they exist specifically to append bytes, so their bytes are always included.
This is how you strip a header the downstream nodes don’t need — set emit: false on the steps that read it, and only the payload reaches output. For example, a protocol with a 1-byte start marker, a 1-byte reserved field, and a 1-byte length prefix, where only the payload itself should be forwarded:
[
{ "type": "start", "bytes": "0x7E", "emit": false },
{ "type": "skip", "count": 1 },
{ "type": "length", "key": "n", "size": 1, "endian": "le", "emit": false },
{ "type": "read", "count": "n" }
]The emitted packet here is just the n payload bytes — the marker, the reserved byte, and the length field are all dropped. Leaving every emit at its default reproduces the old modes’ behavior exactly, where the full frame (magic word, header, and payload) was always included.
Conditional steps (if)
Every step except listen can carry an optional if field, so a schema can branch on a value it read earlier in the same packet — for example, a header whose shape depends on a status byte it already decoded. if takes the same condition syntax as the Condition prefab and Bytes to Map’s per-field if: eval for a single check, or and/or to combine several.
A guard’s eval is more restricted here than in those other prefabs, because it has no named inputs to read — only the keys bound by length and start steps:
receiver(andvalue_receiver, if used instead ofvalue) must each name akeybound by alengthorstartstep that comes earlier in the same schema. A guard cannot read a key from a later step, its own step (astartstep binding its ownkeydoesn’t help either — it’s this very step), or reference anything else — a schema with no named inputs has nothing else to check.- A guard must reference at least one key. A guard with none (for example
{ "type": "and", "operands": [] }) is rejected when the schema is compiled — it would be a constant, so either dead weight or a step that can never run. - Comparing against a fixed
valuerequiresdatatypeto beINTorFLOAT— a captured value is always a number, so it could never equal aSTRING/BYTES/BOOLliteral. length: true(comparing the byte-length of a value, asConditionnormally allows) is rejected — a capture is a decoded number, not a string or byte array, so it has no length to compare.
When a step’s guard evaluates to false, that step is skipped entirely and consumes no bytes — whatever bytes it would have read are left for the next step to consume instead, they are not dropped from the stream.
Worked example. The S7comm protocol’s header carries two extra error-code bytes only when the message type (rosctr) is 2 (Ack) or 3 (Ack_Data) — types 1 (Job) and 7 (Userdata) don’t have them:
[
{ "type": "start", "bytes": "0x32" },
{ "type": "length", "key": "rosctr", "size": 1, "endian": "be" },
{ "type": "read", "count": 4 },
{ "type": "length", "key": "parlen", "size": 2, "endian": "be" },
{ "type": "length", "key": "datlen", "size": 2, "endian": "be" },
{
"type": "read",
"count": 2,
"if": {
"type": "or",
"operands": [
{ "type": "eval", "receiver": "rosctr", "operator": "==", "datatype": "INT", "value": 2 },
{ "type": "eval", "receiver": "rosctr", "operator": "==", "datatype": "INT", "value": 3 }
]
}
},
{ "type": "read", "count": "parlen" },
{ "type": "read", "count": "datlen" }
]For a rosctr of 1 or 7, the guarded read is skipped, so the two bytes right after datlen are read by the following read "parlen" step instead of being consumed as error-code bytes — the frame still comes out correctly sized, just without those two bytes tagged separately.
A guard (or a later read/skip count expression) can only rely on a key if every path that reaches
it is guaranteed to have bound it. If the length or start step that binds a key is itself guarded — so
a frame can leave that key uncaptured — reading it later resynchronizes the frame instead of framing it
(logged as a warning), rather than continuing with a stale value. Reserve guards for steps whose keys
come from unconditional length/start steps.
Escaping byte-stuffed protocols
An until step can carry an optional escape config, for protocols that byte-stuff their end marker inside the payload (for example SLIP, which escapes 0xC0 as 0xDB 0xDC) so the marker never accidentally appears mid-packet.
| Field | Type | Description |
|---|---|---|
byte | Byte | Required. The escape byte. Whenever it appears, it and the byte immediately after it are always consumed as a pair and never checked against the end marker — so an end marker that appears escaped inside the payload does not terminate the packet early. Must differ from the first byte of the end marker. |
decode | BOOLEAN | If true, an escaped pair is replaced in the output by its map translation. If false (default), the pair is passed through unchanged in the output — escaping only prevents a false marker match, it doesn’t transform the bytes. |
map | List | Translations to apply when decode is true. Each entry is a from / to byte pair — the byte that follows the escape byte, and what to replace the whole two-byte sequence with. An escaped byte with no matching entry is passed through unchanged. |
Examples
Each example below reproduces one of the old four modes as a schema, and includes the same public example workflow the old mode used — its reader node now runs the schema shown, with everything else (peripheral, connections) unchanged. To try one in your own company, click Copy JSON to Clipboard in the top-left corner of the embedded workflow, then paste it under Blueprints > Import.
LISTEN mode
Collects every byte and flushes every 1,000 ms or whenever 1,024 bytes have accumulated — the whole schema is a single listen step.
| Constant | Value |
|---|---|
timeout(ms) | 1000 |
[
{ "type": "listen", "max_bytes": 1024 }
]Serial
FIXED mode
Scans for the magic word 0xAA 0xBB, then reads a 5-byte payload.
| Constant | Value |
|---|---|
timeout(ms) | 1000 |
[
{ "type": "start", "bytes": "0xAA 0xBB" },
{ "type": "read", "count": 5 }
]Serial
DYNAMIC mode
Scans for the magic word 0xAA 0xBB, reads a 1-byte offset gap, decodes a 2-byte big-endian length field, adds an adjustment of 2, then reads that many payload bytes.
| Constant | Value |
|---|---|
timeout(ms) | 1000 |
[
{ "type": "start", "bytes": "0xAA 0xBB" },
{ "type": "read", "count": 1 },
{ "type": "length", "key": "len", "size": 2, "endian": "be", "adjustment": 2 },
{ "type": "read", "count": "len" }
]Worked example. With the bytes AA BB 00 01 2C 11 22 33 ... on the wire:
AA BBmatchesstart.- The
readof1byte consumes00(the old mode’s “offset” gap) and — sincereadalways emits — includes it in the output. - The
lengthstep reads01 2Cas big-endian:0x012C=300, plusadjustmentof2=302, bound tolen. - The final
readof"len"consumes the next302bytes. - Emitted packet:
AA BB 00 01 2C+ 302 bytes — identical to what the oldDYNAMICmode produced for the same input.
Serial
END mode
Scans for the start magic word 0xAA 0xBB, accumulates every byte until the end magic word 0xCC 0xDD appears.
| Constant | Value |
|---|---|
timeout(ms) | 1000 |
[
{ "type": "start", "bytes": "0xAA 0xBB" },
{ "type": "until", "bytes": "0xCC 0xDD" }
]Serial
Ports
- Inputs:
trigger(optional):ANY- Optional. When connected, the reader waits for a message here before starting each read cycle. The message content is ignored — any data type works. Applies the same way on Ethernet in bothTCPandMACRAWmode. - Outputs:
output:BYTES- Sends one message per completed packet.
Peripherals
serial- The Serial (UART) port to read from. The node only picks the channel; the bus’s own parameters (baud rate, data bits, etc.) are set once per channel in the Blueprint Editor’s Global Peripheral Configuration panel — see UART / Serial there for the exact fields. Thechannelis the UART channel defined on the device itself:0is UART Channel 0 and1is UART Channel 1, as configured in Hardware settings. If the channel you pick has no pins assigned there, the reader starts but never receives any bytes.spi- Used for Ethernet instead of a dedicated Ethernet channel: the onboard Ethernet module talks over SPI, so the node picks an SPI channel the same way an SPI-based sensor would. The channel’s clock rate is set once per channel in the SPI card of the Global Peripheral Configuration panel, and its pins are assigned as SPI Channel 0 (or1, once its pin mapping rolls out) in Hardware settings. If the SPI channel you pick has no pins assigned there, the reader starts but the connection never comes up. Everything about the network itself (DHCP/static addressing, the remote host to connect to) is configured on the node — see Network settings below.
Network settings (Ethernet)
Only applies to Ethernet — Serial has no network to configure. Unlike the bus parameters above, these settings live on the node itself rather than in a shared Global Peripheral Configuration card, so each node can in principle be configured differently.
The device has one Ethernet chip, so only one Ethernet connection can be active at a time — regardless of how many Ethernet Reader/Writer nodes reference it, and even across different SPI channels. Whichever node initializes first “wins”: its network settings are the ones that actually take effect, and every other Ethernet node silently shares that same connection instead of opening its own. Give every Ethernet node in a blueprint the same network settings to avoid surprises.
| Constant | Type | Description |
|---|---|---|
network_mode | Option | Required. TCP or MACRAW — picks which set of constants below applies. |
TCP — the device acts as a TCP client: it brings up its own network interface, then opens one TCP
connection to connect_ip:connect_port and talks to it.
| Constant | Type | Description |
|---|---|---|
init_type | Option | Required. How the device gets its own IP address: DHCP or STATIC. An unrecognized or missing value falls back to DHCP. |
device_ip | text | Static IP for the device. Only used when init_type is STATIC. Defaults to 0.0.0.0 if left blank. |
subnet_mask | text | Only used when init_type is STATIC. Defaults to 255.255.255.0 if left blank. |
gateway_ip | text | Only used when init_type is STATIC. Defaults to 0.0.0.0 if left blank. |
connect_ip | text | Required. The remote host (server) the device connects out to. |
connect_port | text | Required. The port on the remote host. |
If the connection drops, the device retries automatically — no blueprint change needed — but at most once every 15 seconds, so a flapping link shows gaps rather than a tight reconnect loop.
MACRAW — no IP stack, and no addressing constants at all — the device opens its socket in promiscuous mode and
hands over every raw Ethernet frame on the wire, not just ones addressed to the device. The schema
still applies on top of it — the only difference is that the byte stream being framed is these raw frames
(including their Ethernet headers) instead of a TCP stream, and any single frame larger than 256 bytes is
truncated with a warning. Use this mode only for protocols that need raw frame access — plain TCP covers
everything else.