Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "rtklib-ffi"
description = "Rust wrapper for RTKLIB"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
authors = ["Kevin Webb", "Joseph Fox-Rabinovitz"]
repository = "https://github.com/kpwebb/rtklib-ffi"
Expand Down Expand Up @@ -32,7 +32,7 @@ tle = ["rtklib-sys/tle"]
bitflags = "2"
hifitime = { version = "4", optional = true }
num_enum = "0.7.3"
rtklib-sys = { path = "rtklib-sys/", version = "0.3.0" }
rtklib-sys = { path = "rtklib-sys/", version = "0.4.0" }
strum = { version = "0.27", features = ["derive"], optional = true }
thiserror = "2"

Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ in `rtklib-ffi` cover a subset - see the coverage tables below for current statu
| `hifitime` | Conversions between `GpsTime` and `hifitime::Epoch` |
| `net` | Network streaming |
| `ppk` | Post-processed kinematic positioning via `postpos()` and solution I/O |
| `receivers` | All supported hardware receiver decoders: BINEX, Hemisphere Crescent, Javad/Topcon, NovAtel OEM, NVS, Septentrio SBF, SkyTraq, Swift Navigation SBP, Trimble RT17, u-blox UBX, and Unicore |
| `receivers` | All supported hardware receiver decoders: Advanced Navigation ANPP, BINEX, Hemisphere Crescent, Javad/Topcon, NovAtel OEM, NVS, Septentrio SBF, SkyTraq, Swift Navigation SBP, Trimble RT17, u-blox UBX, and Unicore |
| `rtcm` | RTCM3 message decoding |
| `strum` | `Display` for enums via the `strum` crate |
| `tle` | TLE satellite tracking |
Expand All @@ -21,7 +21,7 @@ in `rtklib-ffi` cover a subset - see the coverage tables below for current statu

```toml
[dependencies]
rtklib-ffi = { version = "0.3", features = ["ppk"] }
rtklib-ffi = { version = "0.4", features = ["ppk"] }
```

```rust
Expand Down Expand Up @@ -344,6 +344,11 @@ Generic frame decoders used by all receiver-specific decoders.
- [ ] `decode_gal_fnav` / `decode_gal_inav`: decode Galileo F/NAV and I/NAV messages
- [ ] `decode_irn_nav`: decode NavIC navigation message

**`rcv/adnav.c`**

- [x] `init_anpp` / `free_anpp`: initialize/free Advanced Navigation ANPP struct
- [x] `input_anpp` / `input_anppf`: Advanced Navigation ANPP decoder

**`rcv/binex.c`**

- [ ] `input_bnx` / `input_bnxf`: BINEX decoder
Expand Down
2 changes: 1 addition & 1 deletion rtklib-sys/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rtklib-sys"
version = "0.3.0"
version = "0.4.0"
rust-version = "1.82.0"
edition = "2018"
authors = ["Kevin Webb", "Joseph Fox-Rabinovitz"]
Expand Down
10 changes: 8 additions & 2 deletions rtklib-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,22 @@ fn main() {

// These files are needed by both ppk and conv. convrnx.c calls pntpos for
// auto-position estimation; pntpos.c calls into preceph.c and ionex.c;
// rinex.c and sbas.c are referenced unconditionally by convrnx.c.
// rinex.c is referenced unconditionally by convrnx.c.
#[cfg(any(feature = "ppk", feature = "conv"))]
{
build.file("rtklib/src/rinex.c");
build.file("rtklib/src/ephemeris.c");
build.file("rtklib/src/sbas.c");
build.file("rtklib/src/pntpos.c");
build.file("rtklib/src/preceph.c");
build.file("rtklib/src/ionex.c");
}

// sbas.c provides igpband1/igpband2, referenced unconditionally by
// septentrio.c's SBAS packet handlers, and is also used by ppk/conv
// for SBAS corrections.
#[cfg(any(feature = "receivers", feature = "ppk", feature = "conv"))]
build.file("rtklib/src/sbas.c");

#[cfg(feature = "ppk")]
{
build.file("rtklib/src/postpos.c");
Expand All @@ -85,6 +90,7 @@ fn main() {
{
build.file("rtklib/src/rcvraw.c");
build.include("rtklib/src");
build.file("rtklib/src/rcv/adnav.c");
build.file("rtklib/src/rcv/binex.c");
build.file("rtklib/src/rcv/crescent.c");
build.file("rtklib/src/rcv/javad.c");
Expand Down
32 changes: 32 additions & 0 deletions src/conv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ pub enum StreamFmt {
/// Unicore.
#[cfg_attr(feature = "strum", strum(to_string = "STRFMT_UNICORE"))]
Unicore = ffi::STRFMT_UNICORE,
/// Advanced Navigation Packet Protocol.
#[cfg_attr(feature = "strum", strum(to_string = "STRFMT_ANPP"))]
Anpp = ffi::STRFMT_ANPP,
/// RINEX observation or navigation file.
#[cfg_attr(feature = "strum", strum(to_string = "STRFMT_RINEX"))]
Rinex = ffi::STRFMT_RINEX,
Expand Down Expand Up @@ -240,6 +243,35 @@ impl RnxOpt {
self
}

/// Set receiver-specific decoder options.
///
/// Forwarded into the decoder's `raw->opt` string at session start. Format
/// is a space-separated list of dash-prefixed flags whose meaning depends
/// on the receiver format.
///
/// Notable examples:
///
/// - **ANPP**: `-RCVR<n>` selects the antenna by zero-based receiver number
/// (default 0).
/// - **SBF (Septentrio)**: `-AUX1` or `-AUX2` selects an auxiliary antenna
/// (default is the main antenna); `-NO_MEAS2` skips type-2 sub-blocks.
/// - **BINEX**: `-EPHALL` keeps every ephemeris (default discards
/// duplicates by IODE); `-GALFNAV` / `-GALINAV` pick the Galileo nav
/// message variant.
/// - **NVS**: `-tadj=<seconds>` adjusts time tags; `-EPHALL` as above.
/// - **Crescent (Hemisphere)**: `-TTCORR`, `-ENAGLO`, `-EPHALL`.
/// - **Tersus**: signal-priority overrides like `-GL1P`, `-GL2X`, `-RL2C`,
/// `-EL1B`, plus `-EPHALL`.
/// - **Swift Navigation SBP**: `-OBSALL` retains observations flagged as
/// non-final.
///
/// Strings longer than the underlying 256-byte buffer (including the NUL
/// terminator) are truncated.
pub fn with_rcvopt(mut self, opts: &str) -> Self {
copy_osstr(&mut self.0.rcvopt, opts);
self
}

/// Include ionosphere correction parameters in the navigation output.
pub fn with_outiono(mut self, enable: bool) -> Self {
self.0.outiono = enable as i32;
Expand Down
65 changes: 65 additions & 0 deletions src/receiver/adnav.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! Advanced Navigation Packet Protocol (ANPP) receiver decoder.
//!
//! ```no_run
//! use rtklib_ffi::receiver::{AnppDecoder, DecodeStatus};
//!
//! let mut decoder = AnppDecoder::try_new(0).unwrap();
//! # let anpp_bytes: Vec<u8> = vec![];
//!
//! for &byte in &anpp_bytes {
//! let Some(status) = decoder.decode(byte) else { continue; };
//! match status {
//! DecodeStatus::Observation => {
//! let obs = decoder.observations();
//! // process observations...
//! }
//! DecodeStatus::Ephemeris => {
//! let sat = decoder.ephemeris_sat();
//! // handle ephemeris update for satellite sat...
//! }
//! _ => {}
//! }
//! }
//! ```

use super::{DecodeStatus, RawReceiver};
use crate::{util::copy_osstr, DecoderInitError};
use rtklib_sys::rtklib as ffi;
use std::{convert::TryFrom, ops::Deref};

/// Advanced Navigation ANPP receiver data decoder.
pub struct AnppDecoder(RawReceiver);

impl AnppDecoder {
/// Create a new ANPP decoder that emits observations from the antenna
/// identified by `receiver_num`.
///
/// Advanced Navigation systems can support multiple antennae in packet 60,
/// identified by a zero-based receiver number. Packets whose
/// `receiver_number` does not match `receiver_num` are dropped silently.
///
/// Internally this writes `-RCVR<n>` into the underlying `raw_t.opt`
/// field, the same string-option mechanism the C decoder uses for tools
/// like convbin (`-ro "-RCVR1"`).
///
/// Returns `Err` if RTKLIB cannot allocate internal buffers.
pub fn try_new(receiver_num: u8) -> Result<Self, DecoderInitError> {
let mut decoder = Self(RawReceiver::init(ffi::STRFMT_ANPP as i32)?);
copy_osstr(&mut decoder.0.0.opt, format!("-RCVR{receiver_num}"));
Ok(decoder)
}

/// Feed one byte into the ANPP decoder.
///
/// Returns `None` if the byte did not complete a recognized message.
pub fn decode(&mut self, byte: u8) -> Option<DecodeStatus> {
let ret = unsafe { ffi::input_anpp(self.0.0.as_mut(), byte) };
DecodeStatus::try_from(ret).ok()
}
}

impl Deref for AnppDecoder {
type Target = RawReceiver;

fn deref(&self) -> &RawReceiver { &self.0 }
}
2 changes: 2 additions & 0 deletions src/receiver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ use std::{
slice::from_raw_parts,
};

pub mod adnav;
pub mod septentrio;
pub use adnav::*;
pub use septentrio::*;

/// Shared raw receiver state wrapping the RTKLIB `raw_t` struct.
Expand Down
19 changes: 12 additions & 7 deletions src/util.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use std::{
ffi::{CString, OsStr},
os::unix::ffi::OsStrExt,
};
#[cfg(any(feature = "conv", feature = "ppk"))]
use std::ffi::CString;
#[cfg(any(feature = "conv", feature = "ppk", feature = "receivers"))]
use std::{ffi::OsStr, os::unix::ffi::OsStrExt};

/// Copy an `OsStr` into a fixed-size null-terminated `[i8; N]` C buffer.
/// Copy an `AsRef<OsStr>` into a fixed-size null-terminated `[i8; N]` C buffer.
/// Truncates silently if `src` is longer than `N - 1` bytes.
pub(crate) fn copy_osstr<const N: usize>(dst: &mut [i8; N], src: &OsStr) {
let src = src.as_bytes();
#[cfg(any(feature = "conv", feature = "receivers"))]
pub(crate) fn copy_osstr<const N: usize>(dst: &mut [i8; N], src: impl AsRef<OsStr>) {
let src = src.as_ref().as_bytes();
let n = src.len().min(N - 1);
unsafe {
std::ptr::copy_nonoverlapping(src.as_ptr() as *const i8, dst.as_mut_ptr(), n);
Expand All @@ -18,11 +19,13 @@ pub(crate) fn copy_osstr<const N: usize>(dst: &mut [i8; N], src: &OsStr) {
///
/// `as_ptr` holds pointers into each `CString`'s owned string buffer. Moving this
/// struct retains the validity of the pointers to the heap-allocated strings.
#[cfg(any(feature = "conv", feature = "ppk"))]
pub(crate) struct CStringArray {
_strings: Vec<CString>,
ptrs: Vec<*const i8>,
}

#[cfg(any(feature = "conv", feature = "ppk"))]
impl CStringArray {
/// Build from a slice of paths. Returns the offending path if one contains a NUL byte.
pub(crate) fn try_new<T: AsRef<OsStr>>(paths: &[T]) -> Result<Self, &OsStr> {
Expand All @@ -38,6 +41,7 @@ impl CStringArray {
}

/// Build from a single path. Returns the offending path if it contains a NUL byte.
#[cfg(feature = "conv")]
pub(crate) fn try_single(path: &OsStr) -> Result<Self, &OsStr> {
let string = CString::new(path.as_bytes()).map_err(|_| path)?;
Ok(Self {
Expand All @@ -57,6 +61,7 @@ impl CStringArray {
}

/// Number of strings in the array.
#[cfg(feature = "ppk")]
pub(crate) fn len(&self) -> usize {
self.ptrs.len()
}
Expand Down
41 changes: 41 additions & 0 deletions tests/anpp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//! Integration tests for the Advanced Navigation ANPP decoder.
#![cfg(feature = "receivers")]

use rtklib_ffi::receiver::{AnppDecoder, DecodeStatus};

const SNIPPET: &str = "tests/snippet.anpp";

/// Count decode-status events of a given variant when streaming the snippet
/// through an `AnppDecoder` configured for the given receiver number.
fn count_status(receiver_num: u8, target: DecodeStatus) -> usize {
let data = std::fs::read(SNIPPET).expect("failed to read snippet");
let mut decoder = AnppDecoder::try_new(receiver_num).expect("failed to init decoder");
data.iter()
.filter(|&&b| decoder.decode(b) == Some(target))
.count()
}

// The snippet starts and ends mid-packet (LRC of first/last 5 bytes is
// nonzero), exercising the sliding-window resync. Both antennae produce
// exactly one fully reassembled epoch each: antenna 0 from a 17-fragment
// run, antenna 1 from an 18-fragment run, with IMU packets (SystemState,
// UnixTime, RawSensors, etc.) interleaved between fragments and a single
// GPS PRN 20 ephemeris arriving between the two epochs.

#[test]
fn decode_anpp_snippet_antenna_0_observations() {
assert_eq!(count_status(0, DecodeStatus::Observation), 1);
}

#[test]
fn decode_anpp_snippet_antenna_1_observations() {
assert_eq!(count_status(1, DecodeStatus::Observation), 1);
}

#[test]
fn decode_anpp_snippet_one_ephemeris() {
// Ephemeris is not gated on receiver_num (it's a constellation-level
// packet), so both decoders see the same single GPS ephemeris.
assert_eq!(count_status(0, DecodeStatus::Ephemeris), 1);
assert_eq!(count_status(1, DecodeStatus::Ephemeris), 1);
}
5 changes: 2 additions & 3 deletions tests/sbf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ fn decode_sbf_all_blocks() {
let Some(status) = decoder.decode(byte) else {
continue;
};
match status {
DecodeStatus::Ephemeris => eph_count += 1,
_ => {}
if matches!(status, DecodeStatus::Ephemeris) {
eph_count += 1;
}
}

Expand Down
Binary file added tests/snippet.anpp
Binary file not shown.