DEV Community

UAV GNSS
UAV GNSS

Posted on

Parsing Septentrio SBF logs in Python: fix-quality analysis and RTK drop forensics

If you build robots that depend on RTK, you eventually hit the moment where the fix drops from RTK Fixed to RTK Float and nothing on your ground station explains why. The autopilot log says "float". The correction source looks healthy. The sky is clear.

The answer is almost always in the receiver's raw log. If that receiver is a Septentrio, the raw log is SBF (Septentrio Binary Format) — and it is very parseable once you know where the traps are.

This walks through parsing SBF in Python and turning it into a fix-quality + drop-event report you can actually reason about. Fair disclosure up front: we're UAV GNSS, a Septentrio receiver maker, and everything below is what we built into a small open-source parser after our first version quietly produced wrong coordinates.

What SBF is, in 30 seconds

SBF is a binary block format. Each block is framed by a 0xFA 0xFA sync marker, followed by a header (block ID, block length, TOW, GPS week) and a payload, and closed by a CRC-16/X25 checksum. Two record types do most of the work for RTK forensics:

  • PVTGeodetic — position, plus the fix mode and the satellite count (NrSV)
  • AttEuler — roll / pitch / heading and their accuracies (relevant if you run dual-antenna heading)

The fix mode is a small integer, and it is the field you care about most:

MODE_MAP = {
    0: 'No GNSS',
    1: 'Single',
    2: 'Differential',
    3: 'RTK Float',
    4: 'RTK Fixed',
}
Enter fullscreen mode Exit fullscreen mode

If you only extract one number from a receiver log all day, make it this one.

Three bugs that bite everyone writing an SBF parser

We rewrote ours after v1 emitted plausible-but-wrong latitude/longitude — the worst possible failure mode, because nothing crashes and your data looks fine.

1. Field offsets are not where you guess they are. In the PVTGeodetic payload we parse lat/lon/height at byte offsets 8 / 16 / 24, with mode at offset 6 and NrSV at 62. Pick wrong offsets and you still get numbers in a believable numeric range. Always sanity-check your first epoch against a known surveyed point or the receiver's own web interface readout before you trust a single row.

2. Resync at the byte level — never skip a fixed stride. If the sync marker doesn't match, scan forward byte by byte for the next 0xFA 0xFA instead of assuming an 8-byte block header and jumping. A dropped or partially-written block then self-heals instead of desynchronising the entire rest of the file.

3. Validate the CRC. SBF uses CRC-16/X25 (poly 0x1021 reflected, init 0xFFFF, xorout 0xFFFF):

def crc16_x25(data):
    crc = 0xFFFF
    for b in data:
        crc ^= b
        for _ in range(8):
            crc = (crc >> 1) ^ 0x8408 if crc & 1 else crc >> 1
    return crc ^ 0xFFFF
Enter fullscreen mode Exit fullscreen mode

With CRC checking on, corrupt blocks are counted and dropped rather than parsed into your CSV. In our own tamper test, flipping one byte in a 30-record log dropped the parsed record count from 30 to 29 — exactly the behaviour you want from a forensics tool.

One more that is easy to forget: timestamps. SBF gives you GPS week + time-of-week in milliseconds. Converting that to UTC means applying the leap-second offset (UTC = GPS - 18 s, valid through 2026) — otherwise your drop events are timestamped 18 seconds off and won't line up with your flight-controller log.

The workflow

The parser lives in our integration guide repo, alongside a generator that produces synthetic SBF logs with valid CRCs so you can test without flying anything:

# dump a CSV: tow, wn, mode, nrsv, lat, lon, height[, roll, pitch, heading]
python3 sbf-parser.py log.sbf --check-crc --utc -o out.csv

# or skip the CSV and get the drop report straight to the console
python3 sbf-parser.py log.sbf --check-crc --utc --analyze

# synthetic test log with two embedded 3-second RTK float drops
python3 make-sample-sbf.py --drops 10,20
Enter fullscreen mode Exit fullscreen mode

--analyze prints a fix-quality summary followed by every drop event, defined as a contiguous run of non-RTK-Fixed epochs following a fixed one:

Fix-quality summary (30 epochs):
  RTK Fixed     27  ( 90.0%)
  RTK Float      3  ( 10.0%)

Drop events (RTK Fixed -> degraded):
  #1  2026-08-27T01:46:15Z: RTK Fixed -> RTK Float for 3.0 s
      (NrSV at onset 12 -> min 6)
Enter fullscreen mode Exit fullscreen mode

That mini-report is the whole point: duration tells you how severe the event was, onset satellite count vs minimum during the drop tells you whether satellites disappeared, and an unrecovered run gets flagged as still degraded at the end of the log instead of being silently truncated.

Reading a drop report like a diagnostician

Three completely different faults produce the same "it went to float" symptom on your GCS. The log separates them:

What the log shows Most likely cause What to change
NrSV collapses across many constellations, degrades in one specific location, recovers when you move away RF interference desensitising the front end (power lines, electric fences, nearby 4G/5G sites) Antenna placement first; then receiver front-end capability
NrSV stays high, fix degrades only under canopy or beside structures, recovers as soon as you clear them Multipath — delayed reflections corrupting carrier-phase Raise the antenna, add a proper ground plane, move it off motor/ESC wiring
NrSV is healthy but the fix degrades for a stretch and returns Corrections were lost or stale (NTRIP dropout, radio link, cellular handover) Log correction age; check the link, not the receiver

In practice you correlate three things on the same timeline: fix mode, satellite count, and correction age. Fix mode alone tells you that you lost it; the other two tell you why.

Why this matters for receiver choice

The third row in that table is a link problem, and no receiver on earth fixes it. But the first row is a hardware differentiator. Receivers differ enormously in how much in-band interference they tolerate before the front end is desensitised: consumer-grade modules typically manage on the order of 25 dB of suppression, while our AIM+ receivers are specified at 40–60 dB. Same sky, same corrections, same wiring — different outcome when you drive past a pylon or work under a transmission line.

You cannot see that difference in a datasheet comparison table. You can see it in an SBF drop report, which is why we instrument it.

If you want the written-up field guide for the satellite-count-collapse pattern, we keep one here: Satellite count drop and RTK fix loss — SBF analyzer guide, and the receiver range that logs this data is here.

Over to you

Do you capture raw SBF/UBX on your vehicles, or only the autopilot's fix-status field? And when you analyse a drop, what do you plot first — satellite count, C/N0, or correction age? Curious what patterns other people are chasing.

Top comments (0)