@coremarine/septentrio-sbf-nodered 2.0.0
Node-RED component to read Septentrio SBF receiver output as CMA
@coremarine/septentrio-sbf-nodered
Node-RED node for Septentrio GNSS receivers speaking SBF (Septentrio Binary Format). It turns the receiver's binary output into CMA, the same JSON shape every CoreMarine device parser emits.
Wrapper of @coremarine/septentrio-sbf. All 108 blocks of the AsteRx SB3 Pro+ firmware 4.10.1 reference guide are decoded. Node >= 22, Node-RED >= 4.
Install
From the Node-RED palette manager, or:
npm install @coremarine/septentrio-sbf-nodered
The node appears in the CoreMarine palette category as Septentrio SBF parser.
Configuration
| Field | Meaning |
|---|---|
| Name | Optional node label |
| Firmware | Default, corrected by the receiver (default) or 4.10.1 |
| Memory | On by default — append each input to the buffer instead of replacing it |
The firmware selects which knowledge base describes each block. You rarely need to pin it:
ReceiverSetup (5902) reports the receiver's real version and the parser adopts it, so the device
is normally the authority on itself. A version this build does not model is reported, never
substituted.
Leave memory on for a live stream. SBF framing is length-prefixed rather than terminated, so a block only decodes once its last byte has arrived — with memory off, any block split across two messages is lost.
msg API
Send payload to parse. Every other key is optional; a key you do not send is absent from the
output too, and on a bad request the corresponding output key holds an error string.
| Key | In | Out |
|---|---|---|
payload |
on sbf: a Buffer, a base64 string or a byte array · on nmea: the sentence as a string |
CMA[] |
memory |
{ command: 'get' } / { command: 'set', payload: boolean } |
{ memory, bytes } |
protocol |
{ command: 'get' } / { command: 'set', payload: 'sbf' } |
{ protocol, protocols } |
firmware |
{ command: 'get' } / { command: 'set', payload: '4.10.1' } |
{ firmware, firmwares, reported?, leapSeconds? } |
ids |
any truthy value | string[] — all 108 block numbers |
definition |
4007 or { id, protocol? } |
SBFSentenceDefinition[], one per revision |
fake |
5938 or { id, protocol?, options? } |
a real wire frame, as a Buffer |
Full details, tables and warnings are in the node's help panel inside Node-RED.
Input — what a string means depends on the protocol
On nmea, payload is the sentence as a string. NMEA 0183 is an ASCII text protocol, so that is
its natural form and it is what the examples use:
msg.payload = '$PSSN,HRP,104751.00,230324,23.455,1.954,0.0125,0.123,0.0234,0.03765,11,0,4.56453,W*20\r\n'
On sbf, payload is normally a Buffer — SBF is binary and has no text form. A base64 string
is also accepted, and that is deliberate rather than generous: every raw in the CMA output is
base64, so it is this package's own vocabulary for bytes. It closes the diagnostic loop — copy a raw
out of a debug node, inject it back, and you are re-parsing the exact frame that misbehaved. Base64 is
validated strictly, so an ASCII string is rejected with a message rather than silently parsed as
garbage.
A Buffer or a byte array works on both, and that matters: a serial, TCP or file node emits bytes whichever protocol the receiver was configured for, so a byte-fed flow keeps working when you switch the protocol. A byte array covers the JSON-only paths (an HTTP body, a stored fixture).
Three conventions worth knowing
id is the block NUMBER — '5938', not 'AttEuler'. The human name is in metadata.name. The
number is what the wire carries and what Septentrio's own documentation indexes by.
Values are the datasheet's, unscaled. A field documented in units of 0.01 m keeps
value: 812 with units: '0.01 m', and the engineering value is in that field's metadata as
{ value: 8.12, units: 'm' }. Angles in radians or semi-circles work the same way. Bitfields and
enums keep their integer value, with the decode in metadata.
The timestamp is the receiver's clock. payload[i].timestamp is the block's own GNSS time, not
this machine's — a GNSS receiver is disciplined to atomic time. Leap seconds are learned from the
device itself (ReceiverTime.DeltaLS). The navigation-message blocks are the exception: they are
stamped with when the satellite transmitted the bits, which can be far in the past, so those keep
the parse time. metadata.timestamp always has received and parsed.
Diagnostics
ids and definition let a flow ask the running node what it expects — field names, types, units,
Do-Not-Use values and descriptions, one entry per revision, because a receiver generation only
sends the fields its revision defines.
That matters because these receivers sit on remote installations with restricted internet access for years: asking the deployed node settles a question that would otherwise need the datasheets.
Seven blocks are opaque — the five Meas3* and the two PVTSupport* — because Septentrio
publishes no field layout for them at all. Their bodies are emitted as bytes rather than invented
into fields.
Fake frames
fake builds a real wire frame from the same field table the parser reads, with a real CRC and a
real Length, so wiring it back into payload round-trips. It is deterministic — the same
request returns the same bytes forever, which is what makes it committable into a flow:
msg.fake = 5938
// -> a 44-byte Buffer: a valid AttEuler frame
msg.fake = { id: 4007, options: { revision: 2, fields: { NrSV: 12 } } }
// -> a 96-byte PVTGeodetic revision 2, with NrSV forced to 12
Two things a flow must handle
A field value can be null — the receiver marked it Do-Not-Use. Its metadata then says
{ doNotUse: true, value: <the sentinel> }. null means "not available" and must never be read
as zero: a Do-Not-Use course-over-ground is not a heading of north, and a receiver in
heading-and-pitch attitude mode has no roll solution at all.
Sentences can carry errors. Nothing the parser receives is dropped silently:
- A bad CRC or a truncated body still decodes every field it can, plus
errorssaying what is wrong. Flagged data beats none. - Bytes that cannot start a block come back as a garbage sentence —
idandprotocolare'unknown',payloadis empty, andrawkeeps the discarded bytes. Adjacent junk is coalesced into one report, so a noisy line does not flood the flow. - A block number this build does not know still produces a sentence, with
payload: [],metadata.name: 'unknown', the body inmetadata.body— and noerrors, because nothing is wrong. That is what keeps a newer firmware's blocks from vanishing.
Check errors and id === 'unknown' before trusting a reading.
Examples
Import examples/septentrio-sbf-examples.json from the Node-RED menu → Import → Examples, or open
it directly:
pnpm run septentrio-sbf:nodered:examples
Nine groups, every frame a real one from a Septentrio receiver: decoded blocks (AttEuler,
PVTGeodetic, ReceiverTime), the Do-Not-Use case, failed and garbage input (a corrupted CRC, line
noise), memory with a frame split in two, firmware and protocol channels, diagnostics, fake
frames, and both protocols on one node — switch to nmea, then feed it a proprietary $PSSN
sentence and a standard one to see SEPTENTRIO SBF / SEPTENTRIO NMEA / NMEA in the output.
It uses only built-in node types, so it imports cleanly with no extra contrib nodes.
Every inject was verified by booting a real Node-RED against the flow file and firing it — fire
ReceiverTime and then firmware: get to watch leapSeconds appear, learned from the device.
Development
pnpm run septentrio-sbf:nodered:build # tsup -> CJS + copy html/icons
pnpm run septentrio-sbf:nodered:test # node:test — unit + real headless node-red
pnpm run septentrio-sbf:nodered:dev # local Node-RED on a scratch flow
Upgrading from 1.x
The output is now CMA rather than SBFResponse, and the msg API is entirely different. The node
type is unchanged (cma-septentrio-parser), so existing flows keep their node — but the
messages in and out of it must be rewritten.
1.x drove the node with a single msg.command string. There is now one msg key per channel, so
a message can reconfigure and feed data at once:
| 1.x | 2.0.0 |
|---|---|
command: 'addData' + a payload buffer, then command: 'getData' |
just send payload — the parsed CMA[] comes back on payload |
command: 'getFirmware' / 'setFirmware' |
firmware: { command, payload? } |
command: 'getMemory' / 'setMemory' |
memory: { command, payload? } |
| — | protocol, ids, definition and fake are new |
SBFResponse with frame.header / frame.time / frame.body |
CMA: payload is the SBF body, header and time stamp in metadata |
name / number / version |
metadata.name / id / metadata.revision |
| a bad CRC, a wrong length or an unknown block was dropped silently | every one of them is reported — see above |
| values were raw integers | value + units are the datasheet's, with the converted value in metadata |
| the sentence date was wrong by years (SBF sends milliseconds; the old code passed them to a seconds API, on the GPS scale rather than UTC) | timestamp is the receiver's own UTC, checked against its own ReceiverTime block |
| Node >= 18, Node-RED >= 3 | Node >= 22, Node-RED >= 4 |
Six real bugs in the 1.x library were fixed on the way, including the timestamp above and
AttEuler's three rate fields being reported on the wrong axes. The library's
README lists them.
License
MIT