diff --git a/Cargo.lock b/Cargo.lock index b4369e9..d71ce21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,7 +487,7 @@ checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rtklib-ffi" -version = "0.3.0" +version = "0.4.0" dependencies = [ "bitflags", "hifitime", @@ -500,7 +500,7 @@ dependencies = [ [[package]] name = "rtklib-sys" -version = "0.3.0" +version = "0.4.0" dependencies = [ "bindgen", "cc", diff --git a/Cargo.toml b/Cargo.toml index d05206b..99cbd16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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" diff --git a/README.md b/README.md index aaf1979..205c3cb 100644 --- a/README.md +++ b/README.md @@ -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 | @@ -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 @@ -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 diff --git a/rtklib-sys/Cargo.toml b/rtklib-sys/Cargo.toml index f013ac0..56839b4 100644 --- a/rtklib-sys/Cargo.toml +++ b/rtklib-sys/Cargo.toml @@ -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"] diff --git a/rtklib-sys/build.rs b/rtklib-sys/build.rs index 0378e59..8a6551a 100644 --- a/rtklib-sys/build.rs +++ b/rtklib-sys/build.rs @@ -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"); @@ -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"); diff --git a/rtklib-sys/rtklib b/rtklib-sys/rtklib index 28ad77c..b4f5fd4 160000 --- a/rtklib-sys/rtklib +++ b/rtklib-sys/rtklib @@ -1 +1 @@ -Subproject commit 28ad77c06ffc16b215858d6d5184d85333d44db9 +Subproject commit b4f5fd43cda661aec7baecb3add9076a46f8dc18 diff --git a/src/conv.rs b/src/conv.rs index ea328f7..c12ecf6 100644 --- a/src/conv.rs +++ b/src/conv.rs @@ -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, @@ -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` 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=` 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; diff --git a/src/receiver/adnav.rs b/src/receiver/adnav.rs new file mode 100644 index 0000000..f7fe533 --- /dev/null +++ b/src/receiver/adnav.rs @@ -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 = 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` 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 { + 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 { + 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 } +} diff --git a/src/receiver/mod.rs b/src/receiver/mod.rs index aedbeb6..bd26d56 100644 --- a/src/receiver/mod.rs +++ b/src/receiver/mod.rs @@ -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. diff --git a/src/util.rs b/src/util.rs index f3966e1..efc3204 100644 --- a/src/util.rs +++ b/src/util.rs @@ -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` 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(dst: &mut [i8; N], src: &OsStr) { - let src = src.as_bytes(); +#[cfg(any(feature = "conv", feature = "receivers"))] +pub(crate) fn copy_osstr(dst: &mut [i8; N], src: impl AsRef) { + 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); @@ -18,11 +19,13 @@ pub(crate) fn copy_osstr(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, 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>(paths: &[T]) -> Result { @@ -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 { let string = CString::new(path.as_bytes()).map_err(|_| path)?; Ok(Self { @@ -57,6 +61,7 @@ impl CStringArray { } /// Number of strings in the array. + #[cfg(feature = "ppk")] pub(crate) fn len(&self) -> usize { self.ptrs.len() } diff --git a/tests/anpp.rs b/tests/anpp.rs new file mode 100644 index 0000000..cdb00c9 --- /dev/null +++ b/tests/anpp.rs @@ -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); +} diff --git a/tests/sbf.rs b/tests/sbf.rs index bf49e3f..7ee948c 100644 --- a/tests/sbf.rs +++ b/tests/sbf.rs @@ -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; } } diff --git a/tests/snippet.anpp b/tests/snippet.anpp new file mode 100644 index 0000000..7f0fd62 Binary files /dev/null and b/tests/snippet.anpp differ