{"_id":"@coremarine/septentrio-sbf","_rev":"5-30c9eb4c95ca014d4ad00b769899ef69","name":"@coremarine/septentrio-sbf","dist-tags":{"latest":"2.0.0"},"versions":{"1.0.0":{"name":"@coremarine/septentrio-sbf","version":"1.0.0","keywords":["coremarine","core-marine","septentrio","sbf","gnss","gps","binary","protocol"],"author":{"name":"CoreMarine"},"license":"MIT","_id":"@coremarine/septentrio-sbf@1.0.0","maintainers":[{"name":"valentina-rubane","email":"vru@core-marine.com"},{"name":"crisconru","email":"crisconru@gmail.com"},{"name":"coremarinedev","email":"dev@core-marine.com"}],"homepage":"https://github.com/core-marine-dev/devices/packages/septentrio-sbf#readme","bugs":{"url":"https://github.com/core-marine-dev/devices/issues"},"dist":{"shasum":"8ee05ce22db649ca97fd69ba379f02a8bb2f2a0e","tarball":"https://registry.npmjs.org/@coremarine/septentrio-sbf/-/septentrio-sbf-1.0.0.tgz","fileCount":7,"integrity":"sha512-gig/GdxZMM3mdyqRM0Usqx3mF2v/OuaHqX+9kLsXzfvHhoVwN9jHFKAAOj65s4683ObXTy7k8MPER/m9/mCBfg==","signatures":[{"sig":"MEYCIQDxYFrBUEC2zbm8qfHh5dnmMkB1NiZmNalEVzYSWjLhBwIhANBL3Q92fkEckKYNJq8bp4vuJKDKQUkeaiuxaion0NXJ","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}],"unpackedSize":90849},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">= 18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"654be3be64fc9f42b1b9ef818dbb6794ee16bb32","scripts":{"lint":"ts-standard","test":"vitest","build":"npm run types && npm run format && tsup","types":"mkdir -p ../../node_modules/@types/gpstime && cp gpstime.d.ts ../../node_modules/@types/gpstime/index.d.ts","format":"ts-standard --fix","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"coremarinedev","email":"dev@core-marine.com"},"repository":{"url":"git+https://github.com/core-marine-dev/devices.git","type":"git"},"_npmVersion":"10.5.2","description":"It is a library to parse SBF data. SBF is a private binary protocol of Septentrio trademark which uses in its GNSS devices","directories":{},"sideEffects":false,"ts-standard":{"ignore":["tests","vitest.config.ts","tsup.config.ts","gpstime.d.ts","examples/*"]},"_nodeVersion":"20.13.1","dependencies":{"crc":"^4.3.2","gpstime":"^1.0.3"},"eslintConfig":{"parserOptions":{"project":"./tsconfig.json"}},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/septentrio-sbf_1.0.0_1716208384193_0.4087983263839605","host":"s3://npm-registry-packages"}},"1.0.1":{"name":"@coremarine/septentrio-sbf","version":"1.0.1","keywords":["coremarine","core-marine","septentrio","sbf","gnss","gps","binary","protocol"],"author":{"name":"CoreMarine"},"license":"MIT","_id":"@coremarine/septentrio-sbf@1.0.1","maintainers":[{"name":"valentina-rubane","email":"vru@core-marine.com"},{"name":"crisconru","email":"crisconru@gmail.com"},{"name":"coremarinedev","email":"dev@core-marine.com"}],"homepage":"https://github.com/core-marine-dev/devices/tree/main/packages/septentrio-sbf","bugs":{"url":"https://github.com/core-marine-dev/devices/issues"},"dist":{"shasum":"ba9305781bdfc3682e33dd8d4460971906aa45f3","tarball":"https://registry.npmjs.org/@coremarine/septentrio-sbf/-/septentrio-sbf-1.0.1.tgz","fileCount":7,"integrity":"sha512-45r6eW5HbY4hGtGQrL7oivhJEaVvx1+5XBHqsKyLuhfGcAD80gGYG+CwOgMys4VCPV0dcuXRd6dTAcp1GfnvMA==","signatures":[{"sig":"MEQCIClAaUXCcQ8ocFDACyjP6Pi+OOSfroVZX+sUIqUbeZasAiAPNKjJ11DAp1FBlPTzxHR4SF9yeyNO91RWN0nzEM+L1g==","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}],"unpackedSize":90852},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">= 18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"b3208594121eeff3b37ea2c1e117f11b0a4b82cc","scripts":{"lint":"ts-standard","test":"vitest","build":"npm run types && npm run format && tsup","types":"mkdir -p ../../node_modules/@types/gpstime && cp gpstime.d.ts ../../node_modules/@types/gpstime/index.d.ts","format":"ts-standard --fix","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"coremarinedev","email":"dev@core-marine.com"},"repository":{"url":"git+https://github.com/core-marine-dev/devices.git","type":"git"},"_npmVersion":"10.5.2","description":"It is a library to parse SBF data. SBF is a private binary protocol of Septentrio trademark which uses in its GNSS devices","directories":{},"sideEffects":false,"ts-standard":{"ignore":["tests","vitest.config.ts","tsup.config.ts","gpstime.d.ts","examples/*"]},"_nodeVersion":"20.13.1","dependencies":{"crc":"^4.3.2","gpstime":"^1.0.3"},"eslintConfig":{"parserOptions":{"project":"./tsconfig.json"}},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/septentrio-sbf_1.0.1_1716274173181_0.2705683096991147","host":"s3://npm-registry-packages"}},"2.0.0":{"name":"@coremarine/septentrio-sbf","version":"2.0.0","author":"CoreMarine","description":"Library to parse SBF (Septentrio Binary Format) data from Septentrio GNSS receivers, with every documented block decoded","license":"MIT","homepage":"https://github.com/core-marine-dev/devices/tree/main/packages/septentrio-sbf","repository":{"type":"git","url":"git+https://github.com/core-marine-dev/devices.git","directory":"packages/septentrio-sbf"},"bugs":{"url":"https://github.com/core-marine-dev/devices/issues"},"keywords":["cma","coremarine","core-marine","septentrio","sbf","asterx","gnss","gps","rtk","binary","protocol"],"type":"module","main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"sideEffects":false,"engines":{"node":">=22"},"dependencies":{"crc":"^4.3.2","@coremarine/nmea-parser":"^6.0.0"},"devDependencies":{},"scripts":{"protocols":"node ../../scripts/yaml-to-ts.mjs ./protocols/septentrio.yml ./src/septentrio-nmea.ts --name=SEPTENTRIO_SENTENCES --type=ProtocolsFileContent --type-from=@coremarine/nmea-parser","build":"pnpm run protocols && pnpm run format && tsup","lint":"eslint","format":"eslint --fix","test":"pnpm run protocols && vitest","test:coverage":"pnpm run protocols && vitest run --coverage"},"_nodeVersion":"24.18.0","_id":"@coremarine/septentrio-sbf@2.0.0","dist":{"integrity":"sha512-vu3Nj4ToLdIHy/ugQ+oX35NHwScURLiq6p3Ou9lZ2yzTVxsaQ9Rc4SuosexGxg/FjNsl+7Cbjk0cW8Er67zNzg==","shasum":"ba8a9daeae1491a78d34cfe3abd7464a13546312","tarball":"https://registry.npmjs.org/@coremarine/septentrio-sbf/-/septentrio-sbf-2.0.0.tgz","fileCount":7,"unpackedSize":639900,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@coremarine%2fseptentrio-sbf@2.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDcqnrH7op7KKivPZjbCjEwpw0RFqT/1ZBNN2eShryaIwIgMH2WmB7dYdqa5PCyshH5Cv6kquBGM2LjM3JReWYG+bI="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:5ba166a5-59d5-4187-aa41-0ac56a060894"}},"directories":{},"maintainers":[{"name":"coremarinedev","email":"dev@core-marine.com"},{"name":"crisconru","email":"crisconru@gmail.com"},{"name":"egr666","email":"egr@core-marine.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/septentrio-sbf_2.0.0_1785601952876_0.26221752220469186"},"_hasShrinkwrap":false}},"time":{"created":"2024-05-20T12:33:04.065Z","modified":"2026-08-01T16:32:33.488Z","1.0.0":"2024-05-20T12:33:04.348Z","1.0.1":"2024-05-21T06:49:33.365Z","2.0.0":"2026-08-01T16:32:33.089Z"},"bugs":{"url":"https://github.com/core-marine-dev/devices/issues"},"author":"CoreMarine","license":"MIT","homepage":"https://github.com/core-marine-dev/devices/tree/main/packages/septentrio-sbf","keywords":["cma","coremarine","core-marine","septentrio","sbf","asterx","gnss","gps","rtk","binary","protocol"],"repository":{"type":"git","url":"git+https://github.com/core-marine-dev/devices.git","directory":"packages/septentrio-sbf"},"description":"Library to parse SBF (Septentrio Binary Format) data from Septentrio GNSS receivers, with every documented block decoded","maintainers":[{"name":"coremarinedev","email":"dev@core-marine.com"},{"name":"crisconru","email":"crisconru@gmail.com"},{"name":"egr666","email":"egr@core-marine.com"}],"readme":"# Septentrio SBF parser\n\n![npm (scoped)](https://img.shields.io/npm/v/%40coremarine/septentrio-sbf) [![publish](https://github.com/core-marine-dev/devices/actions/workflows/septentrio-sbf.yml/badge.svg)](https://github.com/core-marine-dev/devices/actions/workflows/septentrio-sbf.yml) ![npm](https://img.shields.io/npm/dy/%40coremarine/septentrio-sbf)\n\n**Septentrio SBF Parser** is a library to parse SBF data from Septentrio GNSS receivers.\n\n> SBF (Septentrio Binary Format) is Septentrio's own binary protocol, used by its GNSS receivers. Unlike NMEA it is **binary and length-prefixed**: every block starts with the sync bytes `0x24 0x40` (`$@`), then a CRC, a block ID, a total length, and a GPS time stamp — see §4.1 of any Septentrio reference guide.\n\n**All 108 blocks of the AsteRx SB3 Pro+ firmware 4.10.1 reference guide are decoded** — every block in all 16 categories of its Appendix B, each transcribed from the datasheet table with its units, Do-Not-Use values and field descriptions. See [Blocks](#blocks).\n\n**Nothing you feed the parser is dropped silently.** A bad CRC, a truncated body, a block number this build does not know, or bytes that are not SBF at all — each comes back as a sentence saying so, never as an empty array. See [Failed, unmodelled and garbage blocks](#failed-unmodelled-and-garbage-blocks).\n\n> The parser output is the unified **CMA** format shared by every CoreMarine device parser — see [`docs/CMA.md`](../../docs/CMA.md).\n\n## Install\n\n```bash\nnpm i @coremarine/septentrio-sbf\n```\n\nShips ESM + CJS + types. Runs on node, deno, bun and the web: there are no `node:*` imports and no `Buffer` in the parse path — input is a `Uint8Array` (or, on the `nmea` protocol, the sentence as a `string`) and every `raw` is Base64. Requires Node `>=22`.\n\n## How to use it\n\n```typescript\nimport { SeptentrioParser } from '@coremarine/septentrio-sbf'\n\n// every option is optional\nconst parser = new SeptentrioParser()\n// const parser = new SeptentrioParser({ memory: true, bufferLimit: 65535, firmware: '4.10.1' })\n```\n\n### Parse SBF data\n\nFeed bytes and drain the parsed blocks as `CMA[]`:\n\n```typescript\nimport type { CMA } from '@coremarine/septentrio-sbf'\n\n// one-shot\nconst output: CMA[] = parser.parseData(bytes)\n\n// streaming: add chunks, then drain\nparser.addData(chunk1)\nparser.addData(chunk2)\nconst drained: CMA[] = parser.parseData()   // returns + clears the queued blocks\n```\n\n- `addData(data: string | Uint8Array): void` — parse immediately and queue the results.\n- `parseData(data?: string | Uint8Array): CMA[]` — optionally add `data`, then return **and clear** the\n  queue.\n\nOn `sbf` the input is bytes (a string is encoded byte-per-character). On `nmea` it is the sentence as\na string, and bytes work too — see [NMEA — the second protocol](#nmea--the-second-protocol).\n\nA block split across two chunks is held on the internal buffer and parsed once its tail arrives; a\nlone trailing `0x24` is treated as a possible half-received sync, not as junk.\n\n### Output — the CMA shape\n\n```typescript\ninterface CMA {\n  raw: string              // the whole block, BASE64 (it is binary)\n  timestamp: number        // epoch ms — the RECEIVER's own GNSS time when it has one (see Timestamps)\n  id: string               // the SBF block NUMBER as a string, e.g. '5938'\n  protocol: { name: 'SEPTENTRIO SBF', version: string }        // version = the firmware knowledge base in use\n  payload: Field[]         // the SBF BODY only, one entry per datasheet row, in datasheet order\n  metadata: {\n    name: string           // the block NAME, e.g. 'AttEuler' ('unknown' if not modelled)\n    revision: number       // from ID bits 13-15; it is not in the body, so it lives here\n    crc: Field             // header + time-stamp values, each Field-shaped ({ raw, name, type, value })\n    length: Field\n    tow: Field             // GPS time-of-week, in its own datasheet units (0.001 s)\n    wnc: Field             // continuous GPS week count\n    timestamp: { received: number, parsed: number, sentence?: number }\n    payload?: Record<string, unknown>   // values aggregated from >= 2 fields (e.g. a position triple)\n    subBlocks?: Field[][]  // sub-block fields grouped by occurrence, positionally\n    padding?: { raw: string, bytes: number }         // §4.1.5: value undefined, never decoded\n    body?: { raw: string, bytes: number }            // an opaque or unmodelled body\n    revisionDecoded?: number                         // set when the frame's revision is newer than ours\n    [key: string]: unknown\n  }\n  errors?: string[]        // present ONLY when something is wrong\n  description?: string\n}\n\ninterface Field {\n  raw: string              // this field's own bytes, Base64\n  name: string             // the datasheet's own field name\n  type: 'char' | 'string' | 'int8' | 'int16' | 'int32'\n      | 'uint8' | 'uint16' | 'uint32' | 'float32' | 'float64'\n  value: string | number | null                      // null = Do-Not-Use, or unreadable\n  units?: string           // the DATASHEET's unit, unscaled ('0.01 m', 'semi-circles', 'rad')\n  description?: string\n  errors?: string[]\n  metadata?: Record<string, unknown>                 // decoded bitfields, enum labels, converted values\n}\n```\n\nThree conventions are worth knowing before you read a payload:\n\n1. **`id` is the block number, the name is in `metadata.name`.** `'5938'`, not `'AttEuler'` — the\n   number is what the wire carries and what Septentrio's own documentation indexes by.\n2. **`value` and `units` are the datasheet's, unscaled.** A field documented as `0.01 m` keeps\n   `value: 812, units: '0.01 m'`, and the engineering value goes to `metadata` as\n   `{ value: 8.12, units: 'm' }`. The datasheet stays the single source of truth, and nothing has to\n   guess what scale a consumer wanted. Angles in radians or semi-circles work the same way — the\n   converted degrees are in `metadata`.\n3. **A bitfield, mask or enum keeps its integer `value`;** the decoded meaning is in that field's\n   `metadata`. CMA has no bitfield type and none is invented.\n\n<details>\n  <summary>Example — a real <code>AttEuler</code> (5938) frame, trimmed</summary>\n\n```json\n{\n  \"raw\": \"JEC0kzIXLADQkPEW2AgHAAEAAADPsS5DPVAQwfkCldDvlEa++QKV0AxBoD4=\",\n  \"timestamp\": 1685616912000,\n  \"id\": \"5938\",\n  \"protocol\": { \"name\": \"SEPTENTRIO SBF\", \"version\": \"4.10.1\" },\n  \"payload\": [\n    { \"raw\": \"Bw==\", \"name\": \"NrSV\", \"type\": \"uint8\", \"value\": 7 },\n    { \"raw\": \"AA==\", \"name\": \"Error\", \"type\": \"uint8\", \"value\": 0,\n      \"metadata\": { \"mainAux1Baseline\": \"NO_ERROR\", \"mainAux2Baseline\": \"NO_ERROR\", \"attitudeNotRequested\": false } },\n    { \"raw\": \"AQA=\", \"name\": \"Mode\", \"type\": \"uint16\", \"value\": 1,\n      \"metadata\": { \"label\": \"HEADING_PITCH_FLOAT\" } },\n    { \"raw\": \"z7EuQw==\", \"name\": \"Heading\", \"type\": \"float32\", \"value\": 174.69456481933594, \"units\": \"deg\" },\n    { \"raw\": \"PVAQwQ==\", \"name\": \"Pitch\", \"type\": \"float32\", \"value\": -9.0195894241333, \"units\": \"deg\" },\n    { \"raw\": \"+QKV0A==\", \"name\": \"Roll\", \"type\": \"float32\", \"value\": null, \"units\": \"deg\",\n      \"metadata\": { \"doNotUse\": true, \"value\": -20000000000 } },\n    { \"raw\": \"75RGvg==\", \"name\": \"PitchDot\", \"type\": \"float32\", \"value\": -0.19392751157283783, \"units\": \"deg/s\" },\n    { \"raw\": \"DEGgPg==\", \"name\": \"HeadingDot\", \"type\": \"float32\", \"value\": 0.3129962682723999, \"units\": \"deg/s\" }\n  ],\n  \"metadata\": {\n    \"name\": \"AttEuler\",\n    \"revision\": 0,\n    \"crc\":    { \"raw\": \"tJM=\",     \"name\": \"CRC\",    \"type\": \"uint16\", \"value\": 37812 },\n    \"length\": { \"raw\": \"LAA=\",     \"name\": \"Length\", \"type\": \"uint16\", \"value\": 44, \"units\": \"bytes\" },\n    \"tow\":    { \"raw\": \"0JDxFg==\", \"name\": \"TOW\",    \"type\": \"uint32\", \"value\": 384930000, \"units\": \"0.001 s\" },\n    \"wnc\":    { \"raw\": \"2Ag=\",     \"name\": \"WNc\",    \"type\": \"uint16\", \"value\": 2264, \"units\": \"week\" },\n    \"payload\": { \"attitude\": { \"heading\": 174.69456481933594, \"pitch\": -9.0195894241333, \"roll\": null, \"units\": \"deg\" } },\n    \"timestamp\": { \"received\": 1785510182944, \"parsed\": 1785510182945, \"sentence\": 1685616912000 }\n  },\n  \"description\": \"GNSS attitude expressed as Euler angles at the time given by TOW and WNc, in the receiver time frame\"\n}\n```\n\nThis receiver is in attitude mode 1 — heading and pitch only — so `Roll` and `RollDot` are at their\nDo-Not-Use value and read `null`, with `metadata.doNotUse` saying **why** they are null and which\nsentinel matched. A `null` that means \"not available\" is never confused with a measured zero.\n\n</details>\n\n### Timestamps — the receiver's clock wins\n\n`metadata.timestamp` means what it does for every CoreMarine parser: `received` when `addData` was\ncalled, `parsed` when the block was decoded, and `sentence` the block's own time. SBF then does one\nthing the text parsers do not:\n\n**`cma.timestamp` is the block's own GNSS time, not the host clock.** Every SBF block carries TOW +\nWNc, and a GNSS receiver's clock is disciplined to atomic time — so for blocks stamped by the\nreceiver it is strictly better than the machine that happened to read the serial port. Leap seconds\ncome from the device itself (`ReceiverTime.DeltaLS`) when that block is in the output, falling back to\na built-in table otherwise.\n\nTwo exceptions, both deliberate:\n\n- **Signal-in-space blocks are not promoted.** The raw navigation pages (§4.2.2) and decoded\n  navigation messages (§4.2.3-4.2.8) are stamped with when the *satellite transmitted the bits*, which\n  can be far in the past. Promoting that would move a sentence's time backwards. Their\n  `metadata.timestamp.sentence` is still filled in; `cma.timestamp` stays the parse time.\n- **A block whose TOW/WNc are Do-Not-Use** — normal for a few seconds after start-up — has no\n  `sentence` time at all, so `cma.timestamp` is the parse time.\n\n```typescript\nconst [block] = parser.parseData(bytes)\nblock.timestamp                        // 1687513492000 -> 2023-06-23T09:44:52.000Z, from the receiver\nblock.metadata.timestamp.parsed        // when this process decoded it\nparser.parser.leapSeconds              // 18, learned from the device (undefined until ReceiverTime arrives)\n```\n\n## Failed, unmodelled and garbage blocks\n\nEvery byte you feed in either decodes, comes back reported, or stays on the buffer as an incomplete\ntail. **A problem is signalled by the optional `errors: string[]`** — that is the only check you need:\n\n```typescript\nfor (const block of parser.parseData(bytes)) {\n  if (block.id === 'unknown') { /* not SBF at all — inspect block.raw */ }\n  else if (block.errors !== undefined) { /* decoded, but flag it: errors says what is wrong */ }\n  else if (block.payload.length === 0 && block.metadata.name === 'unknown') { /* known frame, unmodelled block */ }\n  else { /* clean */ }\n}\n```\n\nThere are four tiers:\n\n| tier | what it is | shape |\n| --- | --- | --- |\n| **decoded** | CRC valid, block modelled | full `payload`, no `errors` |\n| **identified** | CRC valid, block number **not** in this firmware's knowledge base | real `id`, real timestamp, body at `metadata.body`, `payload: []`, `metadata.name: 'unknown'`, and **no `errors`** |\n| **failed** | CRC mismatch, or a body shorter than its own definition | decoded as far as possible **plus** `errors` |\n| **garbage** | bytes that cannot begin a block | `id: 'unknown'`, `payload: []`, the junk in `raw`, `errors` |\n\n**\"Identified but not modelled\" is not an error**, and it is what makes this library forward-safe: a\nreceiver on a newer firmware that emits a block this build has never heard of still produces a\nsentence with the right number, the right time and the bytes in `raw`, rather than vanishing. Today\nevery block of 4.10.1 is modelled, so you will only see this tier from a newer firmware.\n\n```typescript\n// a valid frame carrying block number 4999, which 4.10.1 does not define\nparser.parseData(frame)\n// -> [{ id: '4999', payload: [], metadata: { name: 'unknown', body: { raw: '…', bytes: 30 }, … } }]\n//    no errors: nothing is wrong, we simply have no table for it\n```\n\n**A CRC failure never stops the decode** — the payload is usually still readable, and a flagged block\nis more useful than a dropped one:\n\n```typescript\n// an AttEuler frame with its CRC deliberately corrupted\nconst [block] = parser.parseData(corrupted)\nblock.errors        // ['Invalid CRC: computed 55888, received 4660']\nblock.payload       // all 10 fields, decoded anyway\nblock.metadata.name // 'AttEuler' — still identified\n```\n\nGarbage is **coalesced**, so a noisy line produces one report rather than a flood:\n\n```typescript\nparser.parseData(new Uint8Array([1, 2, 3, 0xff, 0xfe]))\n// -> [{ raw: 'AQID//4=', id: 'unknown', payload: [],\n//       protocol: { name: 'unknown', version: 'unknown' },\n//       errors: ['Unparseable data: 5 byte(s) before a valid block'], … }]\n```\n\n| input | result |\n| --- | --- |\n| valid block, modelled | CMA, no `errors` |\n| valid block, unknown number | CMA, `payload: []`, `metadata.name: 'unknown'`, **no** `errors` |\n| CRC mismatch | full CMA + `Invalid CRC: computed X, received Y` |\n| body shorter than its table | fields up to the cut + `Body truncated: field X needs bytes a-b of n` |\n| revision newer than we model | decoded at our **highest known** revision + `metadata.revisionDecoded` |\n| `Length` not a multiple of 4, or out of range | not treated as a block — becomes garbage |\n| bytes before a valid block | **garbage**, coalesced into one report |\n| incomplete trailing block | **pending** on the buffer — never an error, it may still complete |\n| pending bytes exceed `bufferLimit` | **garbage** + `Buffer limit exceeded`, buffer reset |\n\nThat last row matters more for a binary protocol than a text one: `0x24 0x40` occurs inside block\nbodies all the time, so a wrong device on the line can open a \"block\" that never completes. The limit\nturns that into a visible report instead of a buffer that grows forever.\n\n## Blocks\n\nAll **108** blocks of the AsteRx SB3 Pro+ firmware 4.10.1 reference guide (Appendix B), in the 16\ncategories of its §4.2:\n\n| §4.2 category | blocks | |\n| --- | --- | --- |\n| 1 Measurement | 8 | `MeasEpoch` `MeasExtra` `Meas3Ranges`\\* `Meas3CN0HiRes`\\* `Meas3Doppler`\\* `Meas3PP`\\* `Meas3MP`\\* `EndOfMeas` |\n| 2 Navigation Page | 15 | the raw broadcast bits: `GPSRawCA` `GPSRawL2C` `GPSRawL5` `GLORawCA` `GALRawFNAV` `GALRawINAV` `GEORawL1` `GEORawL5` `BDSRaw` `BDSRawB1C` `BDSRawB2a` `QZSRawL1CA` `QZSRawL2C` `QZSRawL5` `NAVICRaw` |\n| 3 GPS Decoded | 4 | `GPSNav` `GPSAlm` `GPSIon` `GPSUtc` |\n| 4 GLONASS Decoded | 3 | `GLONav` `GLOAlm` `GLOTime` |\n| 5 Galileo Decoded | 6 | `GALNav` `GALAlm` `GALIon` `GALUtc` `GALGstGps` `GALSARRLM` |\n| 6 BeiDou Decoded | 4 | `BDSNav` `BDSAlm` `BDSIon` `BDSUtc` |\n| 7 QZSS Decoded | 2 | `QZSNav` `QZSAlm` |\n| 8 SBAS L1 Decoded | 14 | `GEOMT00` `GEOPRNMask` `GEOFastCorr` `GEOIntegrity` `GEOFastCorrDegr` `GEONav` `GEODegrFactors` `GEONetworkTime` `GEOAlm` `GEOIGPMask` `GEOLongTermCorr` `GEOIonoDelay` `GEOServiceLevel` `GEOClockEphCovMatrix` |\n| 9 GNSS Position, Velocity, Time | 15 | `PVTCartesian` `PVTGeodetic` `PosCovCartesian` `PosCovGeodetic` `VelCovCartesian` `VelCovGeodetic` `DOP` `PosCart` `PosLocal` `PosProjected` `BaseVectorCart` `BaseVectorGeod` `PVTSupport`\\* `PVTSupportA`\\* `EndOfPVT` |\n| 10 GNSS Attitude | 4 | `AttEuler` `AttCovEuler` `AuxAntPositions` `EndOfAtt` |\n| 11 Receiver Time | 2 | `ReceiverTime` `xPPSOffset` |\n| 12 External Event | 5 | `ExtEvent` `ExtEventPVTCartesian` `ExtEventPVTGeodetic` `ExtEventBaseVectGeod` `ExtEventAttEuler` |\n| 13 Differential Correction | 3 | `DiffCorrIn` `BaseStation` `RTCMDatum` |\n| 14 L-Band Demodulator | 2 | `LBandTrackerStatus` `LBandBeams` |\n| 15 Status | 14 | `ChannelStatus` `ReceiverStatus` `SatVisibility` `InputLink` `OutputLink` `NTRIPClientStatus` `NTRIPServerStatus` `IPStatus` `DynDNSStatus` `QualityInd` `DiskStatus` `RFStatus` `P2PPStatus` `CosmosStatus` |\n| 16 Miscellaneous | 7 | `ReceiverSetup` `RxMessage` `Commands` `Comment` `BBSamples` `ASCIIIn` `EncapsulatedOutput` |\n\n**\\* the seven `opaque` blocks.** Septentrio publishes **no field layout** for these — the reference\nguide says so in as many words for each Meas3 block (\"The detailed definition of this block is not\navailable in this document\") and for the two PVTSupport blocks (\"internal parameters for maintenance\nand support\"). Rather than invent fields, their bodies are published as bytes at `metadata.body`, with\nthe whole frame in `raw`. Nothing is lost: the bytes are all there for whoever does have the\ndefinition. If you need the Meas3 observables, either log `MeasEpoch` + `MeasExtra` instead — the same\nmeasurements, larger frames, fully decoded here — or run Septentrio's own RxTools decoder over `raw`.\n\n`parser.sentenceIds` is the list at runtime, and\n[`getSentenceDefinition`](#introspection-ask-the-parser-what-it-knows) will tell you what any of them\ncontains.\n\n### Sub-blocks\n\nMany blocks repeat a nested record N times (`AuxAntPositions` per antenna, `ChannelStatus` per\nchannel, `MeasEpoch` per satellite and per signal). Those fields are **flattened into `payload` in wire\norder**, so every mandatory value stays in the mandatory place and every field keeps an honest `type`\n— and mirrored positionally at **`metadata.subBlocks: Field[][]`**, so a consumer can read occurrence\n*i* without doing offset arithmetic:\n\n```typescript\nconst [status] = parser.parseData(channelStatusFrame)\nstatus.payload.length                         // every field, flattened\n;(status.metadata.subBlocks as unknown[])     // one entry per occurrence\n```\n\nBecause the payload length varies with N, **definitions are keyed by block number + revision, never by\npayload length.**\n\n## Introspection: ask the parser what it knows\n\nThese parsers run on remote installations for years, so the deployed binary can answer questions that\nwould otherwise need the datasheets.\n\n```typescript\nimport type { Result, SBFSentenceDefinition, SBFError } from '@coremarine/septentrio-sbf'\n\nconst ids: string[] = parser.sentenceIds        // ['4027', '4000', …] — all 108\n\nconst result: Result<SBFSentenceDefinition[], SBFError[]> = parser.getSentenceDefinition(5938)\nif (result.success) {\n  // ONE ENTRY PER REVISION, oldest first — a receiver generation only sends the\n  // fields its revision defines, so seeing them side by side is the point\n  for (const revision of result.value) {\n    revision.revision   // 0, 1, 2, …\n    revision.timestamp  // 'receiver' | 'external' | 'sis'\n    revision.payload    // field definitions: name, type, units, doNotUse, reserved, description\n  }\n}\n```\n\n`getSentenceDefinition(5938)` returns one entry, of which the first three payload rows are:\n\n```json\n{\n  \"id\": \"5938\",\n  \"name\": \"AttEuler\",\n  \"protocol\": { \"name\": \"SEPTENTRIO SBF\", \"version\": \"4.10.1\" },\n  \"revision\": 0,\n  \"timestamp\": \"receiver\",\n  \"payload\": [\n    { \"name\": \"NrSV\", \"type\": \"uint8\", \"doNotUse\": 255,\n      \"description\": \"The average over all antennas of the number of satellites currently included in the attitude calculations\" },\n    { \"name\": \"Error\", \"type\": \"uint8\",\n      \"description\": \"Bit field: bits 0-1 Main-Aux1 baseline error code, bits 2-3 Main-Aux2, bit 7 set when GNSS-based attitude was not requested\" },\n    { \"name\": \"Heading\", \"type\": \"float32\", \"units\": \"deg\", \"doNotUse\": -20000000000, \"description\": \"Heading\" }\n  ]\n}\n```\n\nA sub-block row nests instead, carrying its `count` (the field holding N, or a literal size) and its\nown `fields`.\n\n### Fake blocks, for tests and example flows\n\n`getFakeSentence` builds a real wire frame from the same field table the parser reads — with a real\nCRC and a real `Length`, so `parseData(getFakeSentence(id))` round-trips.\n\n**It is idempotent**: the same call returns the same bytes forever, because a fake frame is meant to be\ncommitted into a spec, an example flow or a bug report. Pass `{ random: true }` for varied (but still\nseeded, still reproducible) filler.\n\n```typescript\nconst fake = parser.getFakeSentence(5938)\nif (fake.success) {\n  const [block] = parser.parseData(fake.value)   // a clean AttEuler CMA\n}\n\n// pick a revision, set the time, override individual fields BY NAME\nparser.getFakeSentence(4007, undefined, { revision: 2, tow: 384930000, wnc: 2264, fields: { Mode: 4, NrSV: 12 } })\nparser.getFakeSentence(4013, undefined, { random: true })\n```\n\n### Errors are a Result, never a throw\n\nEvery fallible call returns `Result<T, SBFError[]>`. **The error side is an array** because one call\ncan be wrong for more than one reason, and each keeps its own `kind`:\n\n```typescript\nparser.getSentenceDefinition(1234)\n// { success: false, error: [{ kind: 'unknown-block', message: 'Block 1234 is not modelled for firmware 4.10.1' }] }\n\nparser.getFakeSentence(5938, '9.9.9')\n// { success: false, error: [{ kind: 'unknown-firmware', message: 'Firmware \"9.9.9\" is not supported; supported: 4.10.1' }] }\n```\n\n`SBFError['kind']` is `'unknown-block' | 'unknown-firmware' | 'unknown-protocol' | 'unknown-revision'`.\n\n**Nothing in this library throws.** Setters ignore invalid values rather than raising: an unsupported\n`firmware` keeps the current one, an unknown `protocol` keeps the active one.\n\n## The device facade\n\n`SeptentrioParser` is the **device** parser; `SBFParser` is the **protocol** parser. They are separate\nbecause a Septentrio receiver can be configured to emit SBF, NMEA or RTCM on the same port, so the\ndevice is not the same thing as the protocol. The facade *composes* a protocol parser rather than being\none:\n\n```typescript\nparser.protocol        // 'sbf' — the active protocol\nparser.protocols       // ['sbf', 'nmea'] — everything this device can speak\nparser.parser          // the active protocol parser, for anything protocol-specific\n```\n\nEverything protocol-specific lives on `.parser`, exposed as one getter rather than delegated method by\nmethod, so adding a protocol does not change this class's surface. Narrow it with `instanceof`:\n\n```typescript\nimport { SBFParser } from '@coremarine/septentrio-sbf'\n\nif (parser.parser instanceof SBFParser) {\n  parser.parser.firmware            // '4.10.1' — the knowledge base in use\n  parser.parser.reportedFirmware    // what the RECEIVER says it runs (see below)\n  parser.parser.leapSeconds         // the GPS-UTC offset learned from the device\n}\n```\n\nThe facade's `getSentenceDefinition` returns the **shared** `SentenceDefinition` shape, because it\nfronts more than one protocol. SBF's extra keys (`name`, `revision`, `timestamp`, `opaque`) come from\n`.parser` for the same reason everything else protocol-specific does.\n\nSwitching protocol **discards the buffer and any undrained sentences** — the bytes were being read\nunder different framing rules, so keeping them would be worse than dropping them.\n\n### NMEA — the second protocol\n\nA Septentrio box can be configured to emit NMEA 0183 instead of SBF. Select the protocol and feed it:\n\n```typescript\nconst parser = new SeptentrioParser({ protocol: 'nmea' })\nparser.parseData('$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')\n// -> CMA[], same shape as SBF\n```\n\n**On `nmea` the input is the sentence as a string** — NMEA 0183 is ASCII text, this layer composes\n`@coremarine/nmea-parser`, and text is the natural form. **Bytes are accepted too**, on both\nprotocols, and that is deliberate: a serial port emits bytes whichever protocol the receiver was\nconfigured for, so a byte-fed pipeline keeps working when you switch. On `sbf` bytes are the only\nform that means anything — a string there is encoded byte-per-character, which is only useful if\nyou already had bytes in a string.\n\nYou get every sentence `@coremarine/nmea-parser` knows (`GGA`, `RMC`, `GNS`, `GSA`, `GST`, `GSV`, `HDT`,\n`VTG`, `ZDA`, `GBS`, `GRS`, `GLL`, `ROT`, `TXT`, …) plus the **six proprietary `$PSSN` sentences** from\nAppendix C.1 of the reference guide:\n\n| id | sentence | what it carries |\n| --- | --- | --- |\n| `PSSNHRP` | Heading, Roll, Pitch | attitude with a standard deviation per axis |\n| `PSSNRBD` | Rover-Base Direction | azimuth/elevation of the base from the rover |\n| `PSSNRBP` | Rover-Base Position | baseline as north/east/up |\n| `PSSNRBV` | Rover-Base Velocity | rate of change of that baseline |\n| `PSSNTFM` | Coordinate Transformation | which RTCM transformation messages were used |\n| `PSSNSNC` | NTRIP Client Status | per-connection status — see below |\n\nAll six arrive as `$PSSN,<SUBTYPE>,…`, with the subtype in the FIRST FIELD rather than the id, so the id\nis resolved to `PSSN<SUBTYPE>` before decoding. `submessage_id` stays in the payload because it is a\nreal wire field.\n\n**Three protocol names come out of this package**, because it is the only one parsing two wire formats:\n\n| `protocol.name` | what it labels |\n| --- | --- |\n| `SEPTENTRIO SBF` | every binary SBF block |\n| `SEPTENTRIO NMEA` | the six proprietary `$PSSN` sentences |\n| `NMEA` (or `TRIMBLE` / `LEICA`) | standard sentences, straight from `@coremarine/nmea-parser` |\n\nSo `protocol.name.startsWith('SEPTENTRIO')` means \"proprietary to this device, either wire format\",\nand the standard sentences stay labelled exactly as the NMEA parser labels them everywhere else.\n\n**How far each one is verified.** A definition transcribed from a datasheet table parses a real\nsentence cleanly even when the field order is wrong — right count, right checksum, values just landing\nunder the wrong names — so this distinction matters:\n\n| sentence | evidence |\n| --- | --- |\n| `PSSNHRP` | **real receiver output** — captures from four unrelated receivers (a Septentrio X5, a vessel in mode 2, a no-fix log, a mosaic), all parsing with verifying checksums |\n| `PSSNTFM` | **real receiver output**, plus the worked example in Appendix C |\n| `PSSNSNC` | **real receiver output**, plus Appendix C — whose printed checksum `68` is a typo; the capture and this parser both compute `4C` |\n| `PSSNRBD` `PSSNRBP` `PSSNRBV` | **datasheet tables only.** No public capture was found. An independent implementation ([`dtc-pronto/dgps-ros`](https://github.com/dtc-pronto/dgps-ros)) transcribed the same guide and agrees field for field, which rules out a transcription slip here — but not an error in the guide itself. |\n\nThe verified ones are pinned as fixtures in `tests/nmea.test.ts`, copied verbatim with their checksums.\n\nTwo traps worth knowing:\n\n- **`PSSNHRP` modes 1, 2 and 5 carry NO roll.** The field arrives empty and stays `null` — never `0`,\n  which would read as \"perfectly level\" instead of \"not measured\". A real mode-2 capture confirms it:\n  fields 5 and 8 — roll and its standard deviation — are the empty ones, and nothing else is.\n- **`PSSNTFM`'s values ARE RTCM message numbers** (`1021`, `1023`, `1025`, …), and `null` means *none of\n  that group was used*, not zero.\n\nThe NMEA parser itself is reachable through `.parser` for its own extras — `addSentences(yaml)` to teach\nit your own sentences, `getSentencesByProtocol()`, and so on.\n\n#### `PSSNSNC` — the one sentence whose payload is nested\n\n`SNC` does not look like NMEA. Its payload is a bracket group holding three scalars followed by **one\nsub-group per NTRIP connection**, so the number of comma-separated fields changes from message to\nmessage:\n\n```\n$PSSN,SNC,[0,379359000,1840,[1,2,0,0]]*68\n```\n\nSince a field list of varying length cannot be described as a fixed definition, this sentence is decoded\nin code and shaped deliberately: **the payload is always TWO fields**, whatever the connection count.\n\n```jsonc\n{\n  \"id\": \"PSSNSNC\",\n  \"payload\": [\n    { \"raw\": \"SNC\", \"name\": \"submessage_id\", \"type\": \"string\", \"value\": \"SNC\" },\n    {\n      \"raw\": \"[0,379359000,1840,[1,2,0,0]]\",\n      \"name\": \"ntrip_client_status\",\n      \"type\": \"string\",\n      \"value\": \"[0,379359000,1840,[1,2,0,0]]\",\n      \"metadata\": {\n        \"fields\": [\n          { \"raw\": \"0\", \"name\": \"message_revision\", \"type\": \"uint8\", \"value\": 0 },\n          { \"raw\": \"379359000\", \"name\": \"time_of_week\", \"type\": \"uint32\", \"value\": 379359000, \"units\": \"ms\" },\n          { \"raw\": \"1840\", \"name\": \"week_number\", \"type\": \"uint16\", \"value\": 1840 }\n        ],\n        \"submessages\": [\n          [\n            { \"raw\": \"1\", \"name\": \"cd_index\", \"type\": \"uint8\", \"value\": 1 },\n            { \"raw\": \"2\", \"name\": \"status\", \"type\": \"uint8\", \"value\": 2 },\n            { \"raw\": \"0\", \"name\": \"error_code\", \"type\": \"uint8\", \"value\": 0 },\n            { \"raw\": \"0\", \"name\": \"info\", \"type\": \"uint8\", \"value\": 0 }\n          ]\n        ]\n      }\n    }\n  ]\n}\n```\n\n**So read `metadata`, not `value`, for this sentence.** `payload[1].value` is the bracket text exactly as\nit arrived — kept byte-faithful so the checksum still verifies against it — while the decoded, typed\nvalues live in `metadata.fields` (the outer scalars) and `metadata.submessages` (one `Field[]` per NTRIP\nconnection, so `submessages[i]` is connection *i*). That is the same shape SBF uses for repeated groups\nin `metadata.subBlocks`.\n\nTwo limitations, stated plainly:\n\n- **The reference guide never says whether consecutive sub-groups are comma-separated.** The decoder\n  parses bracket depth rather than the comma split, so `],[` and `][` give identical results and the\n  question does not arise — but it means the behaviour is inferred, not documented.\n- **An unbalanced group is refused, not guessed.** A truncated `SNC` stays a generic `PSSN` sentence with\n  its fields unnamed. Nothing is dropped: `raw` and every field are still emitted.\n\nThe same data is also available on the SBF side as the `NTRIPClientStatus` block (4053), fully modelled,\nif you would rather not deal with the nesting at all.\n\nIf you only ever speak SBF, `SBFParser` is a `DeviceParser<Uint8Array>` too and can be used directly.\nType against the interface rather than a concrete class:\n\n```typescript\nimport type { DeviceParser } from '@coremarine/septentrio-sbf'\n\nconst parse = (parser: DeviceParser<Uint8Array>, chunk: Uint8Array) => parser.parseData(chunk)\n```\n\n### The firmware is learned from the device\n\n`firmware` selects which knowledge base decodes the blocks. You can set it, but you rarely should:\n**`ReceiverSetup` (5902) reports the receiver's real firmware**, and the parser adopts it when it\narrives.\n\nA firmware this build does **not** model is never silently substituted. The knowledge base stays where\nit is (inventing one would be worse), the reported version is exposed as `reportedFirmware`, and that\nblock gets an error saying what happened:\n\n```\nReceiver reports firmware \"4.99.9\", which this build does not model; decoding with 4.10.1\n```\n\nThe same block also identifies the box, at `metadata.payload.receiver`:\n\n```json\n{ \"name\": \"GRB0053\", \"product\": \"AsteRx SB3 Pro+\", \"serialNumber\": \"3238137\",\n  \"firmware\": \"4.10.1\", \"gnssFirmware\": \"6.10.3-ga4180cb379\", \"antenna\": \"Unknown\", \"marker\": \"SEPT\" }\n```\n\n## API\n\n| Member | Signature | Description |\n| --- | --- | --- |\n| `parseData` | `(data?: string \\| Uint8Array) => CMA[]` | Optionally add `data`, then return and clear the queued blocks. Bytes on `sbf`, the sentence as a string on `nmea` — either is accepted on both. |\n| `addData` | `(data: string \\| Uint8Array) => void` | Parse immediately and queue the results. |\n| `sentenceIds` | `string[]` | Every block number this parser can decode, describe or fabricate. |\n| `getSentenceDefinition` | `(id: number \\| string, protocol?: string) => Result<SentenceDefinition[], ParserError[]>` | What a sentence contains — for SBF, one entry **per revision**. Ask `.parser` for SBF's richer shape. |\n| `getFakeSentence` | `(id: number \\| string, protocol?: string, options?: FakeOptions) => Result<Uint8Array, ParserError[]>` | A real wire frame with a real CRC. Idempotent unless `{ random: true }`. `options` is SBF-only. |\n| `protocol` | `'sbf' \\| 'nmea'` (get/set) | The active protocol. Switching discards the buffer. |\n| `protocols` | `readonly ['sbf', 'nmea']` | Every protocol this device can speak. |\n| `parser` | `SBFParser \\| SeptentrioNMEAParser` | The active protocol parser. Narrow with `instanceof` for SBF's `leapSeconds` / `reportedFirmware`. |\n| `firmware` | `string` (get/set) | The knowledge base in use. An unsupported value is ignored. |\n| `memory` | `boolean` (get/set) | Carry a half-received block between calls. |\n| `bufferLimit` | `number` (get/set) | Max pending bytes before the buffer is reset with a garbage report. |\n| `buffer` | `Uint8Array` | The pending bytes, read only. |\n\nThe second argument of `getSentenceDefinition` / `getFakeSentence` is the **firmware**, because that is\nwhat selects the knowledge base — block 4007 is described by whichever firmware's table you ask for.\nOmitted, it is the one the parser is set to. An unsupported firmware is refused rather than answered\nfrom the wrong table.\n\n### Also exported\n\n| Export | Description |\n| --- | --- |\n| `SeptentrioParser`, `SBFParser`, `SeptentrioNMEAParser` | the device facade and the two protocol parsers |\n| `firmwares()`, `isFirmware(x)`, `blocksFor(fw)` | the supported firmwares and their block registries |\n| `decodeBody`, `createFakeFrame` | the table-driven engine and frame writer, for building on |\n| `toBase64`, `fromBase64` | cross-runtime Base64 over `Uint8Array` — every `raw` is Base64 |\n| `DEFAULT_FIRMWARE`, `PROTOCOL_NAME`, `SEPTENTRIO_PROTOCOLS` | the constants |\n| types | `CMA`, `Field`, `Result`, `DeviceParser`, `Metadata`, `Timestamp`, `Value`, `Type`, `SentenceDefinition`, `ParserError`, `BlockDefinition`, `FieldDefinition`, `SBFError`, `SBFSentenceDefinition`, `FakeOptions`, `TimestampKind`, … |\n\n## Notes\n\n`bufferLimit` defaults to `65535` bytes — the largest a single SBF block can be, since `Length` is a\n`uint16` — so one incomplete block always fits.\n\n**Do not lower it below the largest block your receiver emits.** SBF framing is length-prefixed, not\nterminated, so a block only decodes once its *last* byte has arrived; anything still pending when the\nlimit is passed is flushed as garbage. Real blocks get big — `Commands` runs past 1000 bytes and\n`ChannelStatus` past 900 on a receiver tracking a full sky — and a limit under that destroys perfectly\ngood blocks whenever they arrive in small enough chunks, which is exactly what a serial port does.\n\nBlocks are **described, not hand-decoded**: each block file declares its body as a table of field\ndefinitions in datasheet order, and one shared engine derives every byte offset, `raw` slice,\nlittle-endian read, Do-Not-Use check and padding boundary from it. Three consumers read the same table\n— the parser, `getFakeSentence` and `getSentenceDefinition` — so they cannot disagree. That is a\ndeliberate choice: hand-written offset chains are what produced the field-rotation and padding bugs in\n1.x.\n\n## Upgrading from 1.x\n\n**2.0.0 is a rewrite.** The output format, the constructor and the parse methods all changed, and 1.x\ncode will not run against it.\n\n| 1.x | 2.0.0 |\n| --- | --- |\n| `new SBFParser(firmware, memory)` — positional | `new SeptentrioParser({ firmware, memory, bufferLimit })` — object arg |\n| `availableFirmwares()` | `firmwares()` |\n| `addData(data: Buffer)` | `addData(data: Uint8Array)` |\n| `getFrames(): SBFResponse[]` | `parseData(data?): CMA[]` |\n| `SBFResponse { name, number, version, frame: { header, time, body }, buffer }` | `CMA` — see [Output](#output--the-cma-shape) |\n| throws on a bad firmware / non-`Buffer` input | never throws; `Result` or an ignored setter |\n| CRC-failed, wrong-length and unknown blocks **dropped silently** | all four tiers reported — see [Failed, unmodelled and garbage blocks](#failed-unmodelled-and-garbage-blocks) |\n| `Buffer` throughout | `Uint8Array` + `DataView`; every `raw` is Base64 |\n| 11 blocks | **all 108** |\n\n```typescript\n// 1.x\nconst parser = new SBFParser('4.10.1', true)\nparser.addData(Buffer.from(bytes))\nconst frames = parser.getFrames()\nconst heading = frames[0].frame.body.heading\n\n// 2.0.0\nconst parser = new SeptentrioParser({ firmware: '4.10.1' })\nconst blocks = parser.parseData(bytes)\nconst heading = blocks[0].payload.find((f) => f.name === 'Heading')?.value\n// or, aggregated: blocks[0].metadata.payload.attitude.heading\n```\n\nSix real bugs in the 1.x parser were found and fixed on the way, each now pinned by a spec against a\nreal frame:\n\n- **The sentence timestamp was wrong by years.** SBF's TOW is in *milliseconds*; it was passed to an\n  API documenting *seconds*, and on the GPS scale rather than UTC. `cma.timestamp` now equals the\n  receiver's own reported UTC, block for block.\n- **`AttEuler`'s three rate fields were rotated.** The datasheet order is `PitchDot`, `RollDot`,\n  `HeadingDot`; 1.x laid them out heading-first, so a working dual-antenna install reported every rate\n  on the wrong axis — including a roll *rate* on a frame with no roll solution.\n- **More than 6 padding bytes threw** an uncaught `RangeError` out of `addData`.\n- **A newer revision silently decoded as revision 0**, dropping every field the later revision added.\n  It now decodes at the highest known revision and says so via `metadata.revisionDecoded`.\n- **`PVTGeodetic` revision 2 never populated `padding`.**\n- **`DOP` ignored its documented Do-Not-Use of 0** (reporting \"no DOP available\" as a real DOP of 0),\n  and `xPPSOffset` overwrote `syncAge` with 0, inventing data the receiver had provided.\n\n`engines.node` is now `>=22`, and the `gpstime` dependency is gone — GPS-epoch and leap-second logic\nis internal, and works in the browser. Two runtime dependencies: `crc`, imported by its\n`crc/calculators/crc16xmodem` subpath so no `Buffer` polyfill is pulled in, and\n`@coremarine/nmea-parser`, which the `nmea` protocol composes. Neither is bundled; both stay external.\n","readmeFilename":""}