SBF reader (Septentrio Binary Format)¶
Overview¶
The Septentrio Binary Format (SBF) is the proprietary high-rate binary telemetry
output of Septentrio GNSS receivers (AsteRx SB3, mosaic-X5, PolaRx, etc.).
SbfReader in canvod-readers decodes SBF streams and produces two complementary
xarray.Dataset objects from a single file scan.
All blocks with the same receiver time stamp (TOW, WNc) belong to one epoch
(RefGuide-4.14.0, Section 4.1.3). The reader groups them that way, so the
MeasExtra, SatVisibility, PVT and status values of an epoch are stored with
the observations of that same epoch. One decoder serves iter_epochs(),
to_ds() and to_ds_and_auxiliary(); they give the same values.
No ephemeris download needed
The SBF reader differs from Rnxv3Obs (RINEX) in one fundamental respect:
satellite geometry is embedded in the binary stream.
No SP3 ephemeris download is required for quick-look analysis:
the receiver firmware reports azimuth and elevation for every satellite
it has an almanac or broadcast ephemeris for.
Decoded SBF blocks¶
-
ReceiverSetup
Receiver serial number, firmware version, station name. Populates global dataset attributes.
-
ReceiverTime
Receiver time and its synchronization with GNSS time. Grouped with the epoch; epochs need only the WNc and TOW of MeasEpoch.
-
ChannelStatus
Per-satellite tracking and PVT-usage status bit fields (
tracking_status_raw,pvt_status_raw), main antenna. -
MeasEpoch
Per-epoch GNSS observations: \(C/N_0\) (SNR), pseudorange, carrier phase, Doppler. Primary source for the observations dataset.
-
PVTGeodetic
Navigation solution: position fix, fix type, number of SVs used, horizontal and vertical accuracy estimates, age of corrections.
-
DOP
Dilution of Precision (PDOP, HDOP, VDOP) per epoch.
-
ReceiverStatus
CPU load, board temperature, error bit-field
rx_error. -
SatVisibility
Per-satellite azimuth and elevation for each satellite above the horizon with an almanac or broadcast ephemeris, and which of the two the receiver used. Converted to geographic azimuth \(\varphi\) and polar angle \(\theta\).
-
MeasExtra
Extra per-signal quality: multipath and smoothing corrections, code and carrier noise variance, lock time, CN0HighRes.
-
QualityInd
Receiver quality scores (0-10): overall, GNSS signals, RF power, CPU headroom, scintillation (firmware \(\geq\) 4.15.1).
-
RFStatus
Spoofing and navigation-message-authentication (NMA) flags.
Output datasets¶
Observations dataset: to_ds()¶
Identical structure to Rnxv3Obs.to_ds(), a drop-in replacement:
| Property | Value |
|---|---|
| Dimensions | (epoch, sid) |
epoch coordinate |
datetime64[ns], GPS time (attribute time_system: GPS) |
sid coordinate |
"SV\|Band\|Code" string (for example G07\|L1\|C) |
| Data variables | SNR, Pseudorange, Phase, Doppler, SSI, Smoothing, HalfCycle (select with keep_data_vars) |
| Validation | Passes validate_dataset() |
SNR includes the MeasExtra CN0HighRes value of the same epoch where
MeasExtra is logged (0.03125 dB-Hz resolution instead of 0.25 dB-Hz).
SSI is derived from SNR (RINEX 3.04 banding); SBF has no native SSI.
to_ds_and_auxiliary() also returns the observables before the receiver's
corrections: SNR_raw, Pseudorange_unsmoothed, Pseudorange_raw,
Phase_raw (NaN where MeasExtra is not logged). store_raw_observables=False
leaves them out, and to_ds() never includes them. A run passes
processing.params.store_sbf_raw_observables (default true).
Observations that are dropped
SVID 62 marks a GLONASS satellite whose slot number the receiver does not know (RefGuide-4.14.0, Section 4.1.9). It has no RINEX satellite code, and several such satellites would share one identifier, so its observations are not stored.
Metadata dataset: sbf_obs¶
The second dataset returned by to_ds_and_auxiliary(), under the key
"sbf_obs". It has the same epoch and sid coordinates as the
observations dataset and carries receiver geometry and quality monitoring
signals. The pipeline stores it under {group}/metadata/sbf_obs in the
Icechunk store, in the same commit as the observations of the same files:
a file whose observations are skipped as already stored adds no sbf_obs
either, so sbf_obs covers exactly the stored observation files. An
overwrite run replaces a file's sbf_obs together with its observations. Set
processing.params.store_sbf_metadata: false to not store it; broadcast
geometry still works, since it uses sbf_obs in memory.
Epoch-level values (coordinates on epoch):
| Variable | SBF Source | CF units |
Description |
|---|---|---|---|
pdop |
DOP block | 1 |
Position Dilution of Precision |
hdop |
DOP block | 1 |
Horizontal DOP |
vdop |
DOP block | 1 |
Vertical DOP |
n_sv |
PVTGeodetic | 1 |
Number of SVs used in fix |
h_accuracy_m |
PVTGeodetic | m |
2DRMS horizontal accuracy (~95 %) |
v_accuracy_m |
PVTGeodetic | m |
\(2\sigma\) vertical accuracy (~95 %) |
pvt_mode |
PVTGeodetic | 1 |
Fix type (see flag table, -1 unknown) |
mean_corr_age_s |
PVTGeodetic | s |
Age of differential corrections |
cpu_load |
ReceiverStatus | percent |
Receiver CPU utilisation (-1 unknown) |
temperature_c |
ReceiverStatus | degC |
Receiver temperature |
rx_error |
ReceiverStatus | 1 |
Error bit-field (see bitmask table) |
qual_overall |
QualityInd | 1 |
Overall quality score (0-10, -1 unknown) |
qual_gnss_main |
QualityInd | 1 |
GNSS signals, main antenna |
qual_rf_main |
QualityInd | 1 |
RF power level, main antenna |
qual_cpu |
QualityInd | 1 |
CPU headroom |
qual_scintillation |
QualityInd | 1 |
Scintillation score (firmware \(\geq\) 4.15.1) |
spoofing_flag |
RFStatus | 1 |
1 = signals may not be authentic (-1 unknown) |
nma_fail_flag |
RFStatus | 1 |
1 = non-authentic navigation message detected (NMA, -1 unknown) |
Every variable that stores -1 for unknown declares it as _FillValue, so tools that follow CF conventions read it as missing: cpu_load, pvt_mode, the qual_* variables, the two RFStatus flags, and the per-signal broadcast_angle_source and rise_set.
Per-signal variables (dimensions: epoch × sid):
| Variable | SBF Source | CF units |
Description |
|---|---|---|---|
broadcast_theta |
SatVisibility | rad |
Polar angle \(\theta\) (0 = overhead, \(\pi/2\) = horizon) |
broadcast_phi |
SatVisibility | rad |
Geographic azimuth \(\varphi\) (0 = North, clockwise) |
broadcast_angle_source |
SatVisibility | 1 |
Orbit data behind \(\theta\) and \(\varphi\): 1 = almanac, 2 = ephemeris, -1 unknown |
rise_set |
SatVisibility | 1 |
1 = rising, 0 = setting, -1 unknown |
mp_correction_m |
MeasExtra | m |
Pseudorange multipath correction |
smoothing_corr_m |
MeasExtra | m |
Hatch-filter smoothing correction on pseudorange |
code_var |
MeasExtra | m^2 |
Code-phase noise variance |
carrier_var |
MeasExtra | mcycles^2 |
Carrier-phase noise variance |
lock_time_s |
MeasExtra | s |
Continuous carrier tracking duration (reset at slip) |
cum_loss_cont |
MeasExtra | 1 |
Cycle-slip counter (modulo 256; \(\Delta \neq 0 \rightarrow\) slip) |
car_mp_corr_cycles |
MeasExtra | cycles |
Carrier-phase multipath correction |
cn0_highres_correction |
MeasExtra | dB-Hz |
CN0HighRes sub-quantisation correction (applied to SNR automatically) |
tracking_status_raw |
ChannelStatus | 1 |
Tracking status bit field, 2 bits per signal (main antenna) |
pvt_status_raw |
ChannelStatus | 1 |
PVT usage bit field, 2 bits per signal (main antenna) |
Field decoding formulas
All transformations from raw SBF integers to physical units are documented in detail, including the signal-dependent \(C/N_0\) formula, the 40-bit pseudorange reconstruction, and the carrier-phase wavelength conversion:
PVT mode flags¶
pvt_mode uses CF flag_values / flag_meanings attributes, a fixed set of mutually exclusive values:
| Value | Fix type |
|---|---|
0 |
No solution |
1 |
StandAlone: autonomous from broadcast ephemeris |
2 |
Differential GNSS (DGNSS) |
3 |
Fixed location |
4 |
RTK with fixed ambiguities |
5 |
RTK with float ambiguities |
6 |
SBAS-aided |
7 |
Moving-base RTK with fixed ambiguities |
8 |
Moving-base RTK with float ambiguities |
10 |
Precise Point Positioning (PPP) |
In the dataset
meta_ds["pvt_mode"].attrs
# {
# "long_name": "PVT solution mode",
# "units": "1",
# "flag_values": [0, 1, 2, 3, 4, 5, 6, 7, 8, 10],
# "flag_meanings": "no_pvt stand_alone differential fixed_location "
# "rtk_fixed_ambiguities rtk_float_ambiguities sbas_aided "
# "moving_base_rtk_fixed_ambiguities "
# "moving_base_rtk_float_ambiguities ppp",
# "source": "SBF PVTGeodetic block (Block 4007), reported by receiver firmware",
# ...
# }
rx_error bitmask¶
rx_error is a bit field: multiple flags may be set simultaneously.
Test a specific flag with (rx_error & flag_mask) != 0.
| Bit mask | Flag meaning |
|---|---|
8 (bit 3) |
Software warning or error |
16 (bit 4) |
Watchdog expired since power-on |
32 (bit 5) |
Antenna overcurrent |
64 (bit 6) |
Output data congestion |
256 (bit 8) |
Missed external events |
512 (bit 9) |
CPU load above 90 % |
1024 (bit 10) |
Invalid configuration |
2048 (bit 11) |
Out of geofence |
Source: RefGuide-4.14.0, ReceiverStatus, field RxError, p.398.
Decoding the bitmask
import numpy as np
rx_error = meta_ds["rx_error"].values # int32 array (epoch,)
software = (rx_error & 8) != 0 # bit 3
antenna = (rx_error & 32) != 0 # bit 5
cpu_overload = (rx_error & 512) != 0 # bit 9
print(f"Epochs with software errors: {software.sum()}")
print(f"Epochs with antenna issues: {antenna.sum()}")
rx_error = 48 means both bit 4 (watchdog) and bit 5 (antenna) are set simultaneously.
Coordinate conventions¶
Polar angle θ (theta)¶
broadcast_theta and broadcast_phi are stored in radians; the tables
below give degrees for readability.
| \(\theta\) | Meaning |
|---|---|
| \(0^\circ\) | Satellite directly overhead (zenith) |
| \(90^\circ\) | Satellite at the horizon |
Elevation mask
A typical elevation mask of \(5^\circ\) to \(10^\circ\) corresponds to \(\theta < 80^\circ\) to \(85^\circ\). VOD analyses commonly restrict to \(\theta \leq 70^\circ\) (elevation \(\geq 20^\circ\)) to limit multipath.
Azimuth φ (phi)¶
The stored value is the geographic (compass) azimuth:
- \(0^\circ\) = North · \(90^\circ\) = East · \(180^\circ\) = South · \(270^\circ\) = West (clockwise)
- The SBF
Azimuthfield (steps of \(0.01^\circ\)), converted to radians
Mathematical convention
This is NOT the spherical-coordinate azimuthal angle, which is measured counterclockwise from East. To convert:
CF-convention metadata attributes¶
Every variable in the metadata dataset carries the CF attributes long_name and
units, plus source, comment and references, which say where each value comes
from. None carries a standard_name: the CF standard name table has no name for these
quantities.
meta_ds["pdop"].attrs
# {
# "long_name": "Position Dilution of Precision",
# "units": "1",
# "source": "SBF DOP block (Block 4001), reported by receiver firmware",
# "comment": "PDOP = √(Qxx + Qyy + Qzz), where Q is the position covariance ...",
# "references": "Septentrio AsteRx SB3 ProBase Firmware v4.14.0 Reference Guide, ...",
# }
meta_ds["broadcast_theta"].attrs
# {
# "long_name": "Satellite polar angle reported by the receiver",
# "short_name": "θ_B",
# "units": "rad",
# "source": "SBF SatVisibility block (Block 4012), reported by receiver firmware",
# "comment": "Polar angle from vertical: 0 = overhead, π/2 = horizon. ...",
# "references": "Septentrio AsteRx SB3 ProBase Firmware v4.14.0 Reference Guide, ...",
# }
meta_ds["rx_error"].attrs
# {
# "long_name": "Receiver error status bit field",
# "units": "1",
# "flag_masks": [8, 16, 32, 64, 256, 512, 1024, 2048],
# "flag_meanings": "software watchdog antenna congestion missedevent cpuoverload "
# "invalidconfig outofgeofence",
# "source": "SBF ReceiverStatus block, reported by receiver firmware",
# ...
# }
meta_ds["pvt_mode"].attrs
# {
# "long_name": "PVT solution mode",
# "units": "1",
# "flag_values": [0, 1, 2, 3, 4, 5, 6, 7, 8, 10],
# "flag_meanings": "no_pvt stand_alone differential fixed_location "
# "rtk_fixed_ambiguities rtk_float_ambiguities sbas_aided "
# "moving_base_rtk_fixed_ambiguities "
# "moving_base_rtk_float_ambiguities ppp",
# "source": "SBF PVTGeodetic block (Block 4007), reported by receiver firmware",
# ...
# }
Usage¶
from pathlib import Path
from canvod.readers.sbf import SbfReader
reader = SbfReader(fpath=Path("rref001a00.25_"))
# Inspect header
print(reader.header.rx_name) # receiver name from ReceiverSetup
print(reader.header.rx_version) # e.g. "4.14.4"
print(reader.num_epochs) # number of MeasEpoch blocks
print(reader.systems) # ["E", "G", "R", ...]
# Observations only
obs_ds = reader.to_ds(keep_data_vars=["SNR", "Pseudorange", "Phase", "Doppler"])
# Observations and metadata
obs_ds, aux = reader.to_ds_and_auxiliary()
meta_ds = aux["sbf_obs"]
# One binary scan, two datasets
obs_ds, aux_dict = reader.to_ds_and_auxiliary(
keep_data_vars=["SNR", "Pseudorange"],
)
meta_ds = aux_dict["sbf_obs"]
import xarray as xr
readers = [SbfReader(fpath=f) for f in sorted(sbf_dir.glob("*.sbf"))]
obs_list, meta_list = [], []
for r in readers:
obs, aux = r.to_ds_and_auxiliary(keep_data_vars=["SNR"])
obs_list.append(obs)
meta_list.append(aux["sbf_obs"])
daily_obs = xr.concat(obs_list, dim="epoch", join="outer").sortby("epoch")
daily_meta = xr.concat(meta_list, dim="epoch", join="outer").sortby("epoch")
import numpy as np
# All GPS signals
gps = daily_obs.sel(sid=[s for s in daily_obs.sid.values if s.startswith("G")])
# GPS L1 C/A only
l1c = daily_obs.sel(
sid=(daily_obs.system == "G") & (daily_obs.band == "L1") & (daily_obs.code == "C")
)
# Polar angle filter: elevation ≥ 20° → theta ≤ 70°
theta_mask = daily_meta["broadcast_theta"] <= np.deg2rad(70)
snr_high_el = daily_obs["SNR"].where(theta_mask)
Combined scan API: to_ds_and_auxiliary()¶
Why a combined scan?
The pipeline always calls to_ds_and_auxiliary(), which reads and parses
the binary file once and builds both datasets from the same decoded
observations.
┌─────────────────────────────────────────────────┐
│ SBF file │
│ MeasEpoch PVTGeodetic DOP SatVisibility … │
└───────────────────────┬─────────────────────────┘
│ single parser.read() pass,
│ blocks grouped by time stamp
┌───────────────┴────────────────┐
▼ ▼
MeasEpoch + MeasExtra SatVisibility, DOP, PVT,
(SNR, PR, phase, Doppler) status, quality (same epoch)
│ │
▼ ▼
obs_ds (epoch × sid) meta_ds (epoch × sid)
key: "sbf_obs"
Return type:
tuple[xr.Dataset, dict[str, xr.Dataset]]
# obs_ds {"sbf_obs": meta_ds}
Source format identification¶
Every reader exposes a source_format property used by the store and viewer
to identify the data origin. Every reader must define it; SbfReader
returns "sbf":
reader = SbfReader(fpath=Path("station.25_"))
reader.source_format # → "sbf"
This value is written as a root-level Zarr attribute (source_format) on first
ingest, and as identity.source_format in the store's metadata. The store viewer uses it to select the correct display labels and to
detect whether sbf_obs metadata is available.
Broadcast ephemeris: SBF as geometry source¶
The SBF SatVisibility block provides satellite azimuth and elevation computed
by the receiver firmware, per satellite either from its almanac or from its
broadcast ephemeris (broadcast_angle_source). This makes SBF files a
self-contained ephemeris source: no SP3/CLK download needed.
The SbfBroadcastProvider (an EphemerisProvider implementation) extracts
theta/phi from the sbf_obs auxiliary dataset and aligns them to observation
epochs and SIDs. It uses only angles computed from the broadcast ephemeris;
almanac-based angles become NaN, and a file without any ephemeris-based angle
raises an error:
# Automatic in the orchestrator when ephemeris_source = "broadcast"
# and reader_format = "sbf"
# Manual usage:
obs_ds, aux = reader.to_ds_and_auxiliary(keep_data_vars=["SNR"])
sbf_obs = aux["sbf_obs"]
# theta/phi are already in sbf_obs; no coordinate transform needed;
# keep the ephemeris-based angles only, as SbfBroadcastProvider does
from_ephemeris = sbf_obs["broadcast_angle_source"] == 2
theta = sbf_obs["broadcast_theta"].where(from_ephemeris) # polar angle (rad)
phi = sbf_obs["broadcast_phi"].where(from_ephemeris) # geographic azimuth (rad)
When to use broadcast vs agency final
The receiver reports the angles in steps of \(0.01^\circ\). Almanac-based angles
are not used: on the 2025-001 test data they differed from the
ephemeris-based angles by at most \(0.03^\circ\) for GPS and Galileo, but by up to
\(0.26^\circ\) for GLONASS and up to \(104^\circ\) for BeiDou. Use
ephemeris_source: "broadcast" for immediate processing without internet.
See Ephemeris Sources for details.
Store Integration¶
The orchestrator writes the SBF metadata dataset to the Icechunk store alongside observations, enabling retrospective quality analysis.
from canvod.store import MyIcechunkStore
store = MyIcechunkStore(store_path)
# Write metadata (called automatically by orchestrator)
store.write_sbf_metadata(receiver_name, sbf_obs_ds)
# Read back
meta_ds = store.read_sbf_metadata(receiver_name)
# Check existence
if store.sbf_metadata_exists(receiver_name):
meta_ds = store.read_sbf_metadata(receiver_name)
The metadata is stored at {receiver}/metadata/sbf_obs in the Zarr hierarchy.
Satellite catalog enrichment¶
Combine SBF observations with IGS satellite metadata to add SVN, block type, TX power, mass, and orbital plane as coordinates:
from canvod.readers.gnss_specs import SatelliteCatalog
catalog = SatelliteCatalog.load()
enriched = catalog.enrich_dataset(obs_ds)
# Now filter by satellite generation
gps3 = enriched.sel(sid=enriched.coords["block"].str.startswith("GPS-III"))
See Satellite Catalog for the full API.
Differences vs. RINEX (Rnxv3Obs)¶
| Aspect | RINEX v3 (Rnxv3Obs) |
SBF (SbfReader) |
|---|---|---|
| Format | Plain text | Binary |
| File extension | .rnx |
.sbf |
| Header | Structured text | ReceiverSetup block |
| Geometry (\(\theta\), \(\varphi\)) | Requires SP3 download | Embedded in file |
| Metadata | Header only | Full PVT + quality monitoring |
source_format |
"rinex3" |
"sbf" |
to_ds() |
✓ | ✓ |
iter_epochs() |
✓ | ✓ |
to_ds_and_auxiliary() |
Returns {} aux |
Returns {"sbf_obs": meta_ds} |
| Broadcast ephemeris | Not supported (needs a navigation file) | Built-in via SatVisibility |
| SID discovery | Header-based (all declared SVs) | Observation-based (tracked SVs only) |
| SNR quantization | ~0.001 dB | 0.25 dB-Hz; 0.03125 dB-Hz with MeasExtra |
Time conversion¶
SBF time stamps are GPS time: GPS week (WNc) and time of week (TOW,
milliseconds), with no leap seconds (RefGuide-4.14.0, Section 2.3, p.53).
SbfReader keeps them in GPS time, like RINEX files and the orbit and
clock products:
where GPS epoch = 1980-01-06 00:00:00. A file named for a GPS day starts at 00:00:00 of that day. Stores written by earlier versions hold UTC epochs, 18 s earlier (23:59:42 of the previous day for the first file of a day).
GLONASS FDMA frequencies¶
GLONASS signals use Frequency Division Multiple Access (FDMA). The center frequency depends on the frequency number \(K = \text{FreqNr} - 8\) (RefGuide-4.14.0: \(K \in \{-7, \dots, +13\}\); RefGuide-4.15.1: \(K \in \{-7, \dots, +6\}\)):
SbfReader takes FreqNr from bits 3-7 of the ObsInfo field of each
MeasEpoch Type1 sub-block (RefGuide-4.14.0, p.262), so every GLONASS
observation carries its own frequency number, from the first epoch of the
file on. Type2 sub-blocks take it from their Type1 sub-block.
References¶
- Septentrio AsteRx SB3 ProBase Firmware v4.14.0 Reference Guide
- Septentrio AsteRx SB3 ProBase Firmware v4.15.1 Reference Guide
- CF Conventions v1.11:
flag_masks,flag_meanings,flag_values - RINEX 3.04 signal nomenclature (used verbatim for SID strings)
- SBF Field Decoding Reference: all formulas with firmware page citations