From 3cac105e9d76961ddf370d30ab7140f5c6febb59 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Mon, 8 Sep 2025 20:47:21 +0200 Subject: [PATCH 01/14] Implement BINEX at the Ephemeris level Signed-off-by: Guillaume W. Bres --- src/binex/rnx2bin/nav.rs | 223 +--------------------------- src/navigation/ephemeris/binex.rs | 233 ++++++++++++++++++++++++++++++ src/navigation/ephemeris/mod.rs | 4 + 3 files changed, 240 insertions(+), 220 deletions(-) create mode 100644 src/navigation/ephemeris/binex.rs diff --git a/src/binex/rnx2bin/nav.rs b/src/binex/rnx2bin/nav.rs index 8ee035d2..c30f040f 100644 --- a/src/binex/rnx2bin/nav.rs +++ b/src/binex/rnx2bin/nav.rs @@ -1,11 +1,9 @@ use crate::{ navigation::{Ephemeris, NavKey}, - prelude::{Constellation, Epoch, Rinex, SV}, + prelude::Rinex, }; -use binex::prelude::{ - EphemerisFrame, GALEphemeris, GLOEphemeris, GPSEphemeris, Message, Meta, Record, SBASEphemeris, -}; +use binex::prelude::{Message, Meta, Record}; /// NAV Record Streamer pub struct Streamer<'a> { @@ -13,210 +11,6 @@ pub struct Streamer<'a> { ephemeris_iter: Box + 'a>, } -fn forge_gps_ephemeris_frame(_toc: &Epoch, sv: SV, eph: &Ephemeris) -> Option { - let clock_offset = eph.clock_bias as f32; - let clock_drift = eph.clock_drift as f32; - let clock_drift_rate = eph.clock_drift_rate as f32; - - let toe = eph.orbits.get("toe")?.as_f64() as u16; - - let cic = eph.orbits.get("cic")?.as_f64() as f32; - let crc = eph.orbits.get("crc")?.as_f64() as f32; - let cis = eph.orbits.get("cis")?.as_f64() as f32; - let crs = eph.orbits.get("crs")?.as_f64() as f32; - let cuc = eph.orbits.get("cuc")?.as_f64() as f32; - let cus = eph.orbits.get("cus")?.as_f64() as f32; - - let e = eph.orbits.get("e")?.as_f64(); - let m0_rad = eph.orbits.get("m0")?.as_f64(); - let i0_rad = eph.orbits.get("i0")?.as_f64(); - let sqrt_a = eph.orbits.get("sqrta")?.as_f64(); - let omega_rad = eph.orbits.get("omega")?.as_f64(); - let omega_0_rad = eph.orbits.get("omega0")?.as_f64(); - let omega_dot_rad_s = eph.orbits.get("oemgaDot")?.as_f64() as f32; - let i_dot_rad_s = eph.orbits.get("idot")?.as_f64() as f32; - let delta_n_rad_s = eph.orbits.get("delta_n")?.as_f64() as f32; - - let tgd = eph.orbits.get("tgd")?.as_f64() as f32; - let iode = eph.orbits.get("iode")?.as_u32() as i32; - let iodc = eph.orbits.get("iodc")?.as_u32() as i32; - - Some(EphemerisFrame::GPS(GPSEphemeris { - sv_prn: sv.prn, - iode, - iodc, - toe, - tow: 0, - toc: 0, - tgd, - clock_offset, - clock_drift, - clock_drift_rate, - delta_n_rad_s, - m0_rad, - e, - sqrt_a, - cic, - crc, - cis, - crs, - cuc, - cus, - omega_0_rad, - omega_rad, - i_dot_rad_s, - omega_dot_rad_s, - i0_rad, - ura_m: 0.0, - sv_health: 0, - uint2: 0, - })) -} - -fn forge_sbas_ephemeris_frame(_toc: &Epoch, sv: SV, eph: &Ephemeris) -> Option { - let sbas_prn = sv.prn; - - let clock_offset = eph.clock_bias; - let clock_drift = eph.clock_drift; - - let x_km = eph.orbits.get("satPosX")?.as_f64(); - let vel_x_km = eph.orbits.get("velX")?.as_f64(); - let acc_x_km = eph.orbits.get("accelX")?.as_f64(); - - let y_km = eph.orbits.get("satPosX")?.as_f64(); - let vel_y_km = eph.orbits.get("velY")?.as_f64(); - let acc_y_km = eph.orbits.get("accelY")?.as_f64(); - - let z_km = eph.orbits.get("satPosX")?.as_f64(); - let vel_z_km = eph.orbits.get("velZ")?.as_f64(); - let acc_z_km = eph.orbits.get("accelZ")?.as_f64(); - - let iodn = eph.orbits.get("iodn")?.as_u8(); - - Some(EphemerisFrame::SBAS(SBASEphemeris { - sbas_prn, - toe: 0, - tow: 0, - clock_offset, - clock_drift, - x_km, - vel_x_km, - acc_x_km, - y_km, - vel_y_km, - acc_y_km, - z_km, - vel_z_km, - acc_z_km, - uint1: 0, - ura: 0, - iodn, - })) -} - -fn forge_gal_ephemeris_frame(_toc: &Epoch, sv: SV, eph: &Ephemeris) -> Option { - let _sv_prn = sv.prn; - - let clock_offset = eph.clock_bias as f32; - let clock_drift = eph.clock_drift as f32; - let clock_drift_rate = eph.clock_drift_rate as f32; - - let cic = eph.orbits.get("cic")?.as_f64() as f32; - let crc = eph.orbits.get("crc")?.as_f64() as f32; - let cis = eph.orbits.get("cis")?.as_f64() as f32; - let crs = eph.orbits.get("crs")?.as_f64() as f32; - let cuc = eph.orbits.get("cuc")?.as_f64() as f32; - let cus = eph.orbits.get("cus")?.as_f64() as f32; - - let e = eph.orbits.get("e")?.as_f64(); - let m0_rad = eph.orbits.get("m0")?.as_f64(); - let i0_rad = eph.orbits.get("i0")?.as_f64(); - let sqrt_a = eph.orbits.get("sqrta")?.as_f64(); - let omega_rad = eph.orbits.get("omega")?.as_f64(); - let omega_0_rad = eph.orbits.get("omega0")?.as_f64(); - - let omega_dot_rad_s = eph.orbits.get("oemgaDot")?.as_f64() as f32; - let omega_dot_semi_circles = omega_dot_rad_s; - - let i_dot_rad_s = eph.orbits.get("idot")?.as_f64() as f32; - let idot_semi_circles_s = i_dot_rad_s; - - let delta_n_rad_s = eph.orbits.get("delta_n")?.as_f64() as f32; - let delta_n_semi_circles_s = delta_n_rad_s; - - Some(EphemerisFrame::GAL(GALEphemeris { - sv_prn: 0, - toe_week: 0, - tow: 0, - toe_s: 0, - bgd_e5a_e1_s: 0.0, - bgd_e5b_e1_s: 0.0, - iodnav: 0, - clock_drift_rate, - clock_drift, - clock_offset, - delta_n_semi_circles_s, - m0_rad, - e, - sqrt_a, - cic, - crc, - cis, - cuc, - cus, - crs, - omega_0_rad, - omega_rad, - i0_rad, - omega_dot_semi_circles, - idot_semi_circles_s, - sisa: 0.0, - sv_health: 0, - source: 0, - })) -} - -fn forge_glo_ephemeris_frame(eph: &Ephemeris) -> Option { - let clock_offset_s = eph.clock_bias; - let clock_rel_freq_bias = eph.clock_drift; - - let x_km = eph.orbits.get("satPosX")?.as_f64(); - let vel_x_km = eph.orbits.get("velX")?.as_f64(); - let acc_x_km = eph.orbits.get("accelX")?.as_f64(); - - let y_km = eph.orbits.get("satPosX")?.as_f64(); - let vel_y_km = eph.orbits.get("velY")?.as_f64(); - let acc_y_km = eph.orbits.get("accelY")?.as_f64(); - - let z_km = eph.orbits.get("satPosX")?.as_f64(); - let vel_z_km = eph.orbits.get("velZ")?.as_f64(); - let acc_z_km = eph.orbits.get("accelZ")?.as_f64(); - - Some(EphemerisFrame::GLO(GLOEphemeris { - slot: 0, - day: 0, - tod_s: 0, - clock_offset_s, - clock_rel_freq_bias, - t_k_sec: 0, - x_km, - vel_x_km, - acc_x_km, - y_km, - vel_y_km, - acc_y_km, - z_km, - vel_z_km, - acc_z_km, - sv_health: 0, - freq_channel: 0, - age_op_days: 0, - leap_s: 0, - tau_gps_s: 0.0, - l1_l2_gd: 0.0, - })) -} - impl<'a> Streamer<'a> { pub fn new(meta: Meta, rinex: &'a Rinex) -> Self { Self { @@ -230,19 +24,8 @@ impl<'a> Iterator for Streamer<'a> { type Item = Message; fn next(&mut self) -> Option { let (key, eph) = self.ephemeris_iter.next()?; + let frame = eph.to_binex(key.epoch, key.sv)?; - let frame = if key.sv.constellation.is_sbas() { - forge_sbas_ephemeris_frame(&key.epoch, key.sv, eph) - } else { - match key.sv.constellation { - Constellation::GPS => forge_gps_ephemeris_frame(&key.epoch, key.sv, eph), - Constellation::Galileo => forge_gal_ephemeris_frame(&key.epoch, key.sv, eph), - Constellation::Glonass => forge_glo_ephemeris_frame(eph), - _ => None, - } - }; - - let frame = frame?; Some(Message { meta: self.meta, record: Record::new_ephemeris_frame(frame), diff --git a/src/navigation/ephemeris/binex.rs b/src/navigation/ephemeris/binex.rs new file mode 100644 index 00000000..6fd0b261 --- /dev/null +++ b/src/navigation/ephemeris/binex.rs @@ -0,0 +1,233 @@ +use crate::{ + navigation::Ephemeris, + prelude::{Constellation, Epoch, SV}, +}; + +use binex::prelude::{EphemerisFrame, GALEphemeris, GLOEphemeris, GPSEphemeris, SBASEphemeris}; + +impl Ephemeris { + /// Encodes this [Ephemeris] to BINEX [EphemerisFrame], ready to encode. + /// We currently support GPS, QZSS, SBAS, Galileo and Glonass. + /// + /// ## Inputs + /// - toc: time of clock as [Epoch] + /// - sv: [SV] attached to this [Ephemeris] + /// + /// ## Output + /// - [EphemerisFrame]: all required fields must exist + /// so we can forge a frame. + pub fn to_binex(&self, toc: Epoch, sv: SV) -> Option { + match sv.constellation { + Constellation::GPS | Constellation::QZSS => { + let clock_offset = self.clock_bias as f32; + let clock_drift = self.clock_drift as f32; + let clock_drift_rate = self.clock_drift_rate as f32; + + let toe = self.orbits.get("toe")?.as_f64() as u16; + + let cic = self.orbits.get("cic")?.as_f64() as f32; + let crc = self.orbits.get("crc")?.as_f64() as f32; + let cis = self.orbits.get("cis")?.as_f64() as f32; + let crs = self.orbits.get("crs")?.as_f64() as f32; + let cuc = self.orbits.get("cuc")?.as_f64() as f32; + let cus = self.orbits.get("cus")?.as_f64() as f32; + + let sv_health = self.orbits.get("health")?.as_f64() as u16; + + let e = self.orbits.get("e")?.as_f64(); + let m0_rad = self.orbits.get("m0")?.as_f64(); + let i0_rad = self.orbits.get("i0")?.as_f64(); + let sqrt_a = self.orbits.get("sqrta")?.as_f64(); + let omega_rad = self.orbits.get("omega")?.as_f64(); + let omega_0_rad = self.orbits.get("omega0")?.as_f64(); + let omega_dot_rad_s = self.orbits.get("oemgaDot")?.as_f64() as f32; + + let i_dot_rad_s = self.orbits.get("idot")?.as_f64() as f32; + let delta_n_rad_s = self.orbits.get("delta_n")?.as_f64() as f32; + + let tgd = self.orbits.get("tgd")?.as_f64() as f32; + let iode = self.orbits.get("iode")?.as_u32() as i32; + let iodc = self.orbits.get("iodc")?.as_u32() as i32; + + Some(EphemerisFrame::GPS(GPSEphemeris { + sv_prn: sv.prn, + iode, + iodc, + toe, + tow: 0, // TODO + toc: 0, // TODO + tgd, + clock_offset, + clock_drift, + clock_drift_rate, + delta_n_rad_s, + m0_rad, + e, + sqrt_a, + cic, + crc, + cis, + crs, + cuc, + cus, + omega_0_rad, + omega_rad, + i_dot_rad_s, + omega_dot_rad_s, + i0_rad, + ura_m: 0.0, // TODO + sv_health, + uint2: 0, // TODO + })) + }, + Constellation::Glonass => { + let clock_offset_s = self.clock_bias; + let clock_rel_freq_bias = self.clock_drift; + + // let slot = self.orbits.get("channel")?.as_u8(); + let sv_health = self.orbits.get("health")?.as_u8(); + + let x_km = self.orbits.get("satPosX")?.as_f64(); + let vel_x_km = self.orbits.get("velX")?.as_f64(); + let acc_x_km = self.orbits.get("accelX")?.as_f64(); + + let y_km = self.orbits.get("satPosX")?.as_f64(); + let vel_y_km = self.orbits.get("velY")?.as_f64(); + let acc_y_km = self.orbits.get("accelY")?.as_f64(); + + let z_km = self.orbits.get("satPosX")?.as_f64(); + let vel_z_km = self.orbits.get("velZ")?.as_f64(); + let acc_z_km = self.orbits.get("accelZ")?.as_f64(); + + Some(EphemerisFrame::GLO(GLOEphemeris { + slot: 0, // TODO + day: 0, // TODO + tod_s: 0, // TODO + clock_offset_s, + clock_rel_freq_bias, + t_k_sec: 0, + x_km, + vel_x_km, + acc_x_km, + y_km, + vel_y_km, + acc_y_km, + z_km, + vel_z_km, + acc_z_km, + sv_health, + freq_channel: 0, + age_op_days: 0, + leap_s: 0, + tau_gps_s: 0.0, + l1_l2_gd: 0.0, + })) + }, + Constellation::Galileo => { + let _sv_prn = sv.prn; + + let clock_offset = self.clock_bias as f32; + let clock_drift = self.clock_drift as f32; + let clock_drift_rate = self.clock_drift_rate as f32; + + let cic = self.orbits.get("cic")?.as_f64() as f32; + let crc = self.orbits.get("crc")?.as_f64() as f32; + let cis = self.orbits.get("cis")?.as_f64() as f32; + let crs = self.orbits.get("crs")?.as_f64() as f32; + let cuc = self.orbits.get("cuc")?.as_f64() as f32; + let cus = self.orbits.get("cus")?.as_f64() as f32; + + let e = self.orbits.get("e")?.as_f64(); + let m0_rad = self.orbits.get("m0")?.as_f64(); + let i0_rad = self.orbits.get("i0")?.as_f64(); + let sqrt_a = self.orbits.get("sqrta")?.as_f64(); + let omega_rad = self.orbits.get("omega")?.as_f64(); + let omega_0_rad = self.orbits.get("omega0")?.as_f64(); + + let omega_dot_rad_s = self.orbits.get("oemgaDot")?.as_f64() as f32; + let omega_dot_semi_circles = omega_dot_rad_s; + + let i_dot_rad_s = self.orbits.get("idot")?.as_f64() as f32; + let idot_semi_circles_s = i_dot_rad_s; + + let delta_n_rad_s = self.orbits.get("delta_n")?.as_f64() as f32; + let delta_n_semi_circles_s = delta_n_rad_s; + + let sv_health = self.orbits.get("health")?.as_f64() as u16; + + Some(EphemerisFrame::GAL(GALEphemeris { + sv_prn: sv.prn, + toe_week: 0, // TODO + tow: 0, // TODO + toe_s: 0, // TODO + bgd_e5a_e1_s: 0.0, // TODO + bgd_e5b_e1_s: 0.0, // TODO + iodnav: 0, // TODO + clock_drift_rate, + clock_drift, + clock_offset, + delta_n_semi_circles_s, + m0_rad, + e, + sqrt_a, + cic, + crc, + cis, + cuc, + cus, + crs, + omega_0_rad, + omega_rad, + i0_rad, + omega_dot_semi_circles, + idot_semi_circles_s, + sisa: 0.0, // TODO + sv_health, + source: 0, // TODO + })) + }, + constellation => { + if constellation.is_sbas() { + let clock_offset = self.clock_bias; + let clock_drift = self.clock_drift; + + let x_km = self.orbits.get("satPosX")?.as_f64(); + let vel_x_km = self.orbits.get("velX")?.as_f64(); + let acc_x_km = self.orbits.get("accelX")?.as_f64(); + + let y_km = self.orbits.get("satPosX")?.as_f64(); + let vel_y_km = self.orbits.get("velY")?.as_f64(); + let acc_y_km = self.orbits.get("accelY")?.as_f64(); + + let z_km = self.orbits.get("satPosX")?.as_f64(); + let vel_z_km = self.orbits.get("velZ")?.as_f64(); + let acc_z_km = self.orbits.get("accelZ")?.as_f64(); + + let iodn = self.orbits.get("iodn")?.as_u8(); + + Some(EphemerisFrame::SBAS(SBASEphemeris { + sbas_prn: sv.prn, + toe: 0, + tow: 0, + clock_offset, + clock_drift, + x_km, + vel_x_km, + acc_x_km, + y_km, + vel_y_km, + acc_y_km, + z_km, + vel_z_km, + acc_z_km, + uint1: 0, // TODO + ura: 0, // TODO + iodn, + })) + } else { + None + } + }, + } + } +} diff --git a/src/navigation/ephemeris/mod.rs b/src/navigation/ephemeris/mod.rs index a5dd9617..521e0d50 100644 --- a/src/navigation/ephemeris/mod.rs +++ b/src/navigation/ephemeris/mod.rs @@ -20,6 +20,10 @@ use log::error; #[cfg_attr(docsrs, doc(cfg(feature = "nav")))] pub mod kepler; +#[cfg(feature = "binex")] +#[cfg_attr(docsrs, doc(cfg(feature = "binex")))] +pub mod binex; + #[cfg(feature = "nav")] use crate::prelude::nav::Almanac; From afb30662be3236a69b5526faf850724faf023d73 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Mon, 8 Sep 2025 21:10:04 +0200 Subject: [PATCH 02/14] improving BINEX support Signed-off-by: Guillaume W. Bres --- src/binex/bin2rnx.rs | 7 +++++ src/binex/rnx2bin/mod.rs | 6 ++++ src/navigation/ephemeris/binex.rs | 51 ++++++++++++++++++++++++++++++- src/tests/binex.rs | 42 +++++++++++++++++++++++++ src/tests/mod.rs | 3 ++ 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/tests/binex.rs diff --git a/src/binex/bin2rnx.rs b/src/binex/bin2rnx.rs index 17faaa2b..392aa183 100644 --- a/src/binex/bin2rnx.rs +++ b/src/binex/bin2rnx.rs @@ -17,18 +17,25 @@ use log::{error, info}; pub struct BIN2RNX<'a, R: Read> { /// True when collecting is feasible pub active: bool, + /// Collected size, for postponing mechanism size: usize, + /// Snapshot mode pub snapshot_mode: SnapshotMode, + /// Postponing option pub postponing: Postponing, + /// Deploy time deploy_t: Epoch, + /// BINEX [Decoder] decoder: Decoder<'a, R>, + /// Pending NAV [Rinex] nav_rinex: Rinex, + /// Pending OBS [Rinex] obs_rinex: Rinex, } diff --git a/src/binex/rnx2bin/mod.rs b/src/binex/rnx2bin/mod.rs index acf460a1..89baaacf 100644 --- a/src/binex/rnx2bin/mod.rs +++ b/src/binex/rnx2bin/mod.rs @@ -30,16 +30,22 @@ impl<'a> Iterator for TypeDependentStreamer<'a> { pub struct RNX2BIN<'a> { /// First [Epoch] or [Epoch] of publication t0: Epoch, + /// BINEX [Message] encoding [Meta] meta: Meta, + /// Header consumption State machine state: State, + /// RINEX [Header] snapshot header: &'a Header, + /// RINEX [TypeDependentStreamer] streamer: TypeDependentStreamer<'a>, + /// Assert (before deployment) whether the Header should not be serialized (no default!) pub skip_header: bool, + /// Define (before deployment) a custom message to be included in the announcement. pub custom_announce: Option, } diff --git a/src/navigation/ephemeris/binex.rs b/src/navigation/ephemeris/binex.rs index 6fd0b261..a3862a59 100644 --- a/src/navigation/ephemeris/binex.rs +++ b/src/navigation/ephemeris/binex.rs @@ -1,11 +1,60 @@ use crate::{ - navigation::Ephemeris, + navigation::{Ephemeris, OrbitItem}, prelude::{Constellation, Epoch, SV}, }; +use std::collections::HashMap; + use binex::prelude::{EphemerisFrame, GALEphemeris, GLOEphemeris, GPSEphemeris, SBASEphemeris}; impl Ephemeris { + /// Converts this BINEX [EphemerisFrame] to [Ephemeris], ready to format. + /// We support GPS, QZSS, Galileo, Glonass and SBAS frames. + /// + /// ## Inputs + /// - now: usually the [Epoch] of message reception + pub fn from_binex(now: Epoch, message: EphemerisFrame) -> Option<(SV, Self)> { + match message { + EphemerisFrame::GPS(serialized) => Some(( + SV::new(Constellation::GPS, serialized.sv_prn), + Self { + clock_bias: 0.0, + clock_drift: 0.0, + clock_drift_rate: 0.0, + orbits: HashMap::from_iter([("week".to_string(), OrbitItem::from(0.0f64))]), + }, + )), + EphemerisFrame::SBAS(serialized) => Some(( + SV::new(Constellation::SBAS, serialized.sbas_prn), + Self { + clock_bias: 0.0, + clock_drift: 0.0, + clock_drift_rate: 0.0, + orbits: HashMap::from_iter([("week".to_string(), OrbitItem::from(0.0f64))]), + }, + )), + EphemerisFrame::GLO(serialized) => Some(( + SV::new(Constellation::Glonass, serialized.slot), + Self { + clock_bias: 0.0, + clock_drift: 0.0, + clock_drift_rate: 0.0, + orbits: HashMap::from_iter([("week".to_string(), OrbitItem::from(0.0f64))]), + }, + )), + EphemerisFrame::GAL(serialized) => Some(( + SV::new(Constellation::Galileo, serialized.sv_prn), + Self { + clock_bias: 0.0, + clock_drift: 0.0, + clock_drift_rate: 0.0, + orbits: HashMap::from_iter([("week".to_string(), OrbitItem::from(0.0f64))]), + }, + )), + _ => None, + } + } + /// Encodes this [Ephemeris] to BINEX [EphemerisFrame], ready to encode. /// We currently support GPS, QZSS, SBAS, Galileo and Glonass. /// diff --git a/src/tests/binex.rs b/src/tests/binex.rs new file mode 100644 index 00000000..fea8e6aa --- /dev/null +++ b/src/tests/binex.rs @@ -0,0 +1,42 @@ +use crate::navigation::Ephemeris; +use crate::prelude::Rinex; + +use binex::prelude::Meta; + +#[test] +#[ignore] +fn nav_v3_to_binex() { + let rinex = Rinex::from_gzip_file("data/NAV/V3/ESBC00DNK_R_20201770000_01D_MN.rnx.gz").unwrap(); + + let meta = Meta::default(); + + let mut streamer = rinex.rnx2bin(meta); + + for message in streamer.iter() {} +} + +#[test] +#[ignore] +fn nav_v3_ephemeris() { + let rinex = Rinex::from_gzip_file("data/NAV/V3/ESBC00DNK_R_20201770000_01D_MN.rnx.gz").unwrap(); + + for (k, ephemeris) in rinex.nav_ephemeris_frames_iter() { + let serialized = ephemeris.to_binex(k.epoch, k.sv).unwrap_or_else(|| { + panic!("Failed to serialize {}({})", k.epoch, k.sv); + }); + + // mirror + let (decoded_sv, decoded) = + Ephemeris::from_binex(k.epoch, serialized).unwrap_or_else(|| { + panic!("Failed to decoded {}({}) BINEX frame", k.epoch, k.sv); + }); + + // testbench + assert_eq!(k.sv, decoded_sv, "{}({}) invalid SV", k.epoch, k.sv); + assert_eq!( + *ephemeris, decoded, + "{}({}) invalid content decoded", + k.epoch, k.sv + ); + } +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 398bed29..bf013c85 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -20,6 +20,9 @@ mod merge; #[cfg(feature = "clock")] mod clock; +#[cfg(feature = "binex")] +mod binex; + #[cfg(feature = "processing")] mod processing; From 844c25c7476a44e7461f9b7419d761861946f54d Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Mon, 8 Sep 2025 21:11:59 +0200 Subject: [PATCH 03/14] improving BINEX support Signed-off-by: Guillaume W. Bres --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 0968f544..d024f677 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,10 @@ other RINEX-like formats have their own parser: - [DORIS (special observations)](https://github.com/nav-solutions/doris) - Many pre-processing algorithms including Filter Designer - Several file operations: merging, splitting, time binning (batch) +- Several conversion methods (serdes operations) + - BINEX protocol (on `binex` feature) + - U-Blox protocol (on `ublox` feature) + - RTCM protocol (on `rtcm` feature) ## Warnings :warning: From 159e8468cde8c7cebcd7dbd654fdd52c37e5c370 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 13 Sep 2025 11:03:11 +0200 Subject: [PATCH 04/14] Upgrading RTCM support Signed-off-by: Guillaume W. Bres --- Cargo.toml | 8 +-- src/lib.rs | 2 +- src/rtcm/mod.rs | 69 +++++++++++++++++++- src/rtcm/nav.rs | 29 +++++++++ src/rtcm/rnx2rtcm.rs | 28 --------- src/rtcm/rtcm2rnx.rs | 54 ---------------- src/tests/mod.rs | 3 + src/tests/rtcm.rs | 147 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 250 insertions(+), 90 deletions(-) create mode 100644 src/rtcm/nav.rs delete mode 100644 src/rtcm/rnx2rtcm.rs delete mode 100644 src/rtcm/rtcm2rnx.rs create mode 100644 src/tests/rtcm.rs diff --git a/Cargo.toml b/Cargo.toml index 6fa3e074..aabcd90d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,22 +67,22 @@ clock = [] # ANTEX for accurate antenna characteristics: dedicated Iterators & methods. antex = [] -# BINEX RNX2BIN and BIN2RNX serdes +# RINEX2BIN serializer binex = [ "dep:binex" ] -# RTCM RTCM2RNX and RNX2RTCM serdes +# RINEX2RTCM serializer rtcm = [ "dep:rtcm-rs", ] -# RINEX to GNSS protos +# RINEX to GNSS binary protos serializer protos = [ "dep:gnss-protos", ] -# RINEX to UBX serializer and UBX helpers +# RINEX to UBX serializer and other UBX helpers ublox = [ "dep:ublox", "dep:gnss-protos", diff --git a/src/lib.rs b/src/lib.rs index 84c26e1f..53a110f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,7 +200,7 @@ pub mod prelude { #[cfg(feature = "rtcm")] #[cfg_attr(docsrs, doc(cfg(feature = "rtcm")))] - pub use crate::rtcm::RTCM2RNX; + pub use crate::rtcm::RNX2RTCM; #[cfg(feature = "ublox")] #[cfg_attr(docsrs, doc(cfg(feature = "ublox")))] diff --git a/src/rtcm/mod.rs b/src/rtcm/mod.rs index 1ad55b0e..5a252dd6 100644 --- a/src/rtcm/mod.rs +++ b/src/rtcm/mod.rs @@ -1,5 +1,68 @@ -//! RTCM serdes oprations +use crate::prelude::{Rinex, RinexType}; -mod rtcm2rnx; +mod nav; +use nav::Streamer as NavStreamer; -pub use rtcm2rnx::RTCM2RNX; +use rtcm_rs::msg::message::Message; + +/// RINEX type dependent record streamer +enum TypeDependentStreamer<'a> { + /// NAV frames streamer + NAV(NavStreamer<'a>), +} + +/// [RNX2UBX] can serialize a [Rinex] structure as a stream of UBX frames. +/// It implements [Read] which lets you stream data bytes into your own buffer. +pub struct RNX2RTCM<'a> { + type_dependent: TypeDependentStreamer<'a>, +} + +impl<'a> Iterator for RNX2RTCM<'a> { + type Item = Message; + + fn next(&mut self) -> Option { + match &mut self.type_dependent { + TypeDependentStreamer::NAV(streamer) => streamer.next(), + } + } +} + +impl Rinex { + /// Obtain a [RNX2RTCM] streamer to serialize this [Rinex] structure into a stream of RTCM [Message]s. + /// You can then use the Iterator implementation to iterate each messages. + /// + /// RINEX NAV (V3) example: + /// ``` + /// use std::io::Read; + /// use rinex::prelude::Rinex; + /// + /// // NAV(V3) files will generate + /// let rinex = Rinex::from_gzip_file("data/NAV/V3/ESBC00DNK_R_20201770000_01D_MN.rnx.gz") + /// .unwrap(); + /// + /// // deploy + /// let mut streamer = rinex.rnx2rtcm() + /// .unwrap(); // supported for this type + /// + /// // consume entirely + /// loop { + /// match streamer.next() { + /// Some(message) => { + /// }, + /// None => { + /// // end of stream + /// // RINEX file has been consumed entirely + /// break; + /// }, + /// } + pub fn rnx2rtcm<'a>(rinex: &'a Rinex) -> Option> { + let type_dependent = match rinex.header.rinex_type { + RinexType::NavigationData => TypeDependentStreamer::NAV(NavStreamer::new(rinex)), + _ => { + return None; + }, + }; + + Some(RNX2RTCM { type_dependent }) + } +} diff --git a/src/rtcm/nav.rs b/src/rtcm/nav.rs new file mode 100644 index 00000000..833bb284 --- /dev/null +++ b/src/rtcm/nav.rs @@ -0,0 +1,29 @@ +use crate::{ + navigation::{Ephemeris, NavKey}, + prelude::{Constellation, Rinex}, +}; + +use rtcm_rs::msg::message::Message; + +pub struct Streamer<'a> { + /// Iterator + ephemeris_iter: Box + 'a>, +} + +impl<'a> Streamer<'a> { + /// Builds a new [Streamer] dedicated to NAV RINEX streaming. + pub fn new(rinex: &'a Rinex) -> Self { + Self { + ephemeris_iter: rinex.nav_ephemeris_frames_iter(), + } + } +} + +impl<'a> Iterator for Streamer<'a> { + type Item = Message; + + /// Try to serialize a new RTCM [Message] from this [Streamer]. + fn next(&mut self) -> Option { + None + } +} diff --git a/src/rtcm/rnx2rtcm.rs b/src/rtcm/rnx2rtcm.rs deleted file mode 100644 index 4314d33a..00000000 --- a/src/rtcm/rnx2rtcm.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! RINEX to BINEX serialization -use std::io::Read; - -use crate::prelude::Rinex; - -use rtcm_rs::msg::message::Message; - -/// RNX2RTCM can serialize a RINEX to a stream of RTCM Messages. -pub struct RNX2RTCM { - /// RTCM encoder - encoder: Encoder, -} - -impl Iterator for RNX2RTCM { - type Item = Option; - fn next(&mut self) -> Option { - None - } -} - -impl RNX2BIN { - /// Creates a new [RNX2BIN]. - pub fn new(w: W) -> Self { - Self { - encoder: Encoder::new(r), - } - } -} diff --git a/src/rtcm/rtcm2rnx.rs b/src/rtcm/rtcm2rnx.rs deleted file mode 100644 index 29728071..00000000 --- a/src/rtcm/rtcm2rnx.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! RTCM to RINEX deserialization -use std::io::Read; - -use rtcm_rs::next_msg_frame as next_rtcm_msg_frame; - -/// RTCM2RNX can deserialize a RTCM stream to RINEX Tokens. -pub struct RTCM2RNX { - /// internal buffer - buf: Vec, - /// True when EOS has been reached - eos: bool, - /// pointer - ptr: usize, - /// [Read]able interface - reader: R, -} - -impl Iterator for RTCM2RNX { - type Item = Option<()>; - fn next(&mut self) -> Option { - if !self.eos { - if self.ptr < self.buf.len() { - // try filling with new bytes - let size = self.reader.read(&mut self.buf).ok()?; - if size == 0 { - self.eos = true; - } - } - } else { - if self.ptr == 0 { - // done consuming the last bytes - return None; - } - } - - match next_rtcm_msg_frame(&self.buf[self.ptr..]) { - (_, Some(_)) => {}, - (_, None) => {}, - } - - None - } -} - -impl RTCM2RNX { - pub fn new(r: R) -> Self { - Self { - ptr: 0, - reader: r, - eos: false, - buf: Vec::with_capacity(1024), - } - } -} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index ba97f4e1..608fd297 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -29,6 +29,9 @@ mod meteo; #[cfg(feature = "nav")] mod nav; +#[cfg(feature = "rtcm")] +mod rtcm; + #[cfg(feature = "ublox")] mod ublox; diff --git a/src/tests/rtcm.rs b/src/tests/rtcm.rs new file mode 100644 index 00000000..5f0691dc --- /dev/null +++ b/src/tests/rtcm.rs @@ -0,0 +1,147 @@ +use std::io::Read; + +use crate::{ + navigation::Ephemeris, + prelude::{Constellation, Rinex}, +}; + +// NAV (V3) to RTCM +#[test] +#[cfg(feature = "nav")] +fn esbcdnk_ephv3_to_rtcm() { + let rinex = Rinex::from_gzip_file("data/NAV/V3/ESBC00DNK_R_20201770000_01D_MN.rnx.gz").unwrap(); + + for (k, ephemeris) in rinex.nav_ephemeris_frames_iter() { + match k.sv.constellation { + Constellation::GPS => {}, + Constellation::QZSS => {}, + Constellation::BeiDou => {}, + Constellation::Galileo => {}, + _ => {}, // not supported yet + } + } +} + +// GLO (V2) to RTCM +#[test] +#[ignore] +#[cfg(feature = "nav")] +fn glo_v2_to_rtcm() { + let rinex = Rinex::from_file("data/NAV/V2/dlf10010.21g").unwrap(); + + for (k, ephemeris) in rinex.nav_ephemeris_frames_iter() { + match k.sv.constellation { + Constellation::Glonass => {}, + constellation => { + panic!("found invalid {} constellation", constellation); + }, + } + } +} + +// RNX2RTCM (NAV V3) +#[test] +#[ignore] +fn esbcdnk_nav3_to_ubx() { + let mut total_msg = 0; + let mut total_size = 0; + let mut total_mga_gps_eph = 0; + let mut total_mga_bds_eph = 0; + let mut total_mga_gal_eph = 0; + let mut total_mga_glo_eph = 0; + + let mut buffer = [0; 2048]; + + let rinex = Rinex::from_gzip_file("data/NAV/V3/ESBC00DNK_R_20201770000_01D_MN.rnx.gz").unwrap(); + + let mut streamer = rinex.rnx2ubx(); + + let mut parser = Parser::default(); // tester + + loop { + match streamer.read(&mut buffer) { + Ok(0) => { + break; + }, + Ok(size) => { + total_size += size; + let mut iter = parser.consume_ubx(&buffer); + + loop { + match iter.next() { + Some(message) => match message { + Ok(packet) => match packet { + PacketRef::MgaGpsEph(_) => { + total_mga_gps_eph += 1; + }, + PacketRef::MgaBdsEph(_) => { + total_mga_bds_eph += 1; + }, + PacketRef::MgaGalEph(_) => { + total_mga_gal_eph += 1; + }, + PacketRef::MgaGloEph(_) => { + total_mga_glo_eph += 1; + }, + msg => { + panic!("unexpected UBX message found: {:?}", msg); + }, + }, + Err(e) => { + panic!("invalid UBX content identified: {}", e); + }, + }, + None => break, + } + total_msg += 1; + } + }, + Err(_) => {}, + } + } + + assert!(total_size > 0); + assert!(total_msg > 0); + + // assert_eq!(total_mga_gps_eph, 253 + 15); // TODO: this fails, should be GPS+QZSS from test #1 + assert_eq!(total_mga_bds_eph, 360); + // assert_eq!(total_mga_gal_eph, 806); + + println!("ESCDNK-NAV (V3): {:8} bytes", total_size); + println!("ESCDNK-NAV (V3): {:8} messages", total_msg); + println!( + "ESCDNK-NAV (V3): {:8} MGA-GPS-EPH frames", + total_mga_gps_eph + ); + println!( + "ESCDNK-NAV (V3): {:8} MGA-GAL-EPH frames", + total_mga_glo_eph + ); + println!( + "ESCDNK-NAV (V3): {:8} MGA-BDS-EPH frames", + total_mga_bds_eph + ); + println!( + "ESCDNK-NAV (V3): {:8} MGA-GLO-EPH frames", + total_mga_glo_eph + ); +} + +// MGA-TIM-XXX +#[test] +fn esbcdnk_timv4_to_ubx_mga() { + let mut total_msg = 0; + let mut total_size = 0; + + let mut buffer = [0; 2048]; + + let rinex = Rinex::from_gzip_file("data/NAV/V4/BRD400DLR_S_20230710000_01D_MN.rnx.gz").unwrap(); + + for (k, time_offset) in rinex.nav_system_time_frames_iter() { + match k.sv.constellation { + Constellation::GPS => {}, + Constellation::Galileo => {}, + _ => {}, + } + } +} From 1c8aa8f341c045c50c4af9fa64fff18a08b5e00c Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 13 Sep 2025 11:03:19 +0200 Subject: [PATCH 05/14] fix minor ublox issues Signed-off-by: Guillaume W. Bres --- src/tests/ublox.rs | 3 +-- src/ublox/mod.rs | 7 +++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/tests/ublox.rs b/src/tests/ublox.rs index adc9c65d..fa0ce7a4 100644 --- a/src/tests/ublox.rs +++ b/src/tests/ublox.rs @@ -124,7 +124,6 @@ fn esbcdnk_ephv3_to_ubx_mga() { } }, _ => {}, // not supported yet - _ => {}, // not supported yet } } @@ -188,7 +187,7 @@ fn glo_v2_to_ubx_mga() { println!("UBX-MGA-EPH: {:4} GLO frames", glo); } -// UBX2RINEX (NAV V3) +// RNX2UBX (NAV V3) #[test] #[ignore] fn esbcdnk_nav3_to_ubx() { diff --git a/src/ublox/mod.rs b/src/ublox/mod.rs index 6b9136b5..fb6f2159 100644 --- a/src/ublox/mod.rs +++ b/src/ublox/mod.rs @@ -23,8 +23,11 @@ impl<'a> TypeDependentStreamer<'a> { impl Rinex { /// Obtain a [RNX2UBX] streamer to serialize this [Rinex] into a stream of U-Blox [PacketRef]s. - /// You can then use the Iterator implementation to iterate each messages. - /// The stream is RINEX format dependent, and we currently only truly support NAV RINEX. + /// Unlike other streamers (RTCM, BINEX..), the UBX streamer can only operate on a buffer. + /// Conveniently, [RNX2UBX] implements [Read] and [BufRead] to let you stream all the supported messages + /// into your own buffer. + /// + /// The stream content is RINEX dependent, and we currently only truly support NAV RINEX. /// /// RINEX NAV (V3) example: /// ``` From c64d967590c76752decfe168f90849703b91e941 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 13 Sep 2025 11:39:31 +0200 Subject: [PATCH 06/14] Ephemeris to RTCM helpers Signed-off-by: Guillaume W. Bres --- src/navigation/ephemeris/mod.rs | 5 + src/navigation/ephemeris/rtcm.rs | 200 +++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 src/navigation/ephemeris/rtcm.rs diff --git a/src/navigation/ephemeris/mod.rs b/src/navigation/ephemeris/mod.rs index 8ecb81ac..314d1509 100644 --- a/src/navigation/ephemeris/mod.rs +++ b/src/navigation/ephemeris/mod.rs @@ -28,8 +28,13 @@ pub mod binex; use crate::prelude::nav::Almanac; #[cfg(feature = "ublox")] +#[cfg_attr(docsrs, doc(cfg(feature = "ublox")))] mod ublox; +#[cfg(feature = "rtcm")] +#[cfg_attr(docsrs, doc(cfg(feature = "rtcm")))] +mod rtcm; + #[cfg(feature = "nav")] use anise::{ astro::AzElRange, diff --git a/src/navigation/ephemeris/rtcm.rs b/src/navigation/ephemeris/rtcm.rs new file mode 100644 index 00000000..e223c20f --- /dev/null +++ b/src/navigation/ephemeris/rtcm.rs @@ -0,0 +1,200 @@ +use std::collections::HashMap; + +use hifitime::prelude::Unit; + +use crate::{ + navigation::{Ephemeris, OrbitItem}, + prelude::{Constellation, Epoch, SV}, +}; + +use rtcm_rs::msg::{Msg1019T, Msg1020T}; + +#[cfg(doc)] +use crate::prelude::Rinex; + +impl Ephemeris { + /// Converts this [Ephemeris] to [Constellation::GPS] [Msg1019T] + /// ## Input + /// - epoch: [Epoch] of message reception. + /// - sv: attached satellite as [SV] which must a [Constellation::GPS] vehicle. + /// + /// ## Output + /// - [Msg1019T] + pub fn to_rtcm_msg1019(&self, epoch: Epoch, sv: SV) -> Option { + if sv.constellation != Constellation::GPS { + return None; // invalid API usage + } + + let (toc_week, toc_week_nanos) = epoch.to_time_of_week(); + let toc_s = (toc_week_nanos as f32) * 1.0E-9; + + let toe = self.toe(sv)?; + let toe_week_nanos = toe.to_time_of_week().1; + let toe_s = (toe_week_nanos as f32) * 1.0E-9; + + let gps_satellite_id = sv.prn; + + let ura_index = self.get_orbit_f64("accuracy").unwrap_or_default() as u8; + let idot_sc_s = self.get_orbit_f64("idot")?; + let iodc = self.get_orbit_f64("iodc")? as u16; + let crs_m = self.get_orbit_f64("crs")? as f32; + let delta_n_sc_s = self.get_orbit_f64("deltaN")? as f32; + let m0_sc = self.get_orbit_f64("m0")?; + let cic_rad = self.get_orbit_f64("cic")? as f32; + let cis_rad = self.get_orbit_f64("cis")? as f32; + let cuc_rad = self.get_orbit_f64("cuc")? as f32; + let cus_rad = self.get_orbit_f64("cus")? as f32; + let eccentricity = self.get_orbit_f64("e")?; + let sqrt_a_sqrt_m = self.get_orbit_f64("sqrta")?; + let i0_sc = self.get_orbit_f64("i0")?; + let iode = self.get_orbit_f64("iode")? as u8; + let crc_m = self.get_orbit_f64("crc")? as f32; + let omega_sc = self.get_orbit_f64("omega")?; + let omegadot_sc_s = self.get_orbit_f64("omegaDot")?; + let omega0_sc = self.get_orbit_f64("omega0")?; + let tgd_s = self.tgd()?.to_unit(Unit::Second) as f32; + let sv_health_ind = self.get_orbit_f64("health")? as u8; + let l2_p_data_flag = self.get_orbit_f64("l2p")? as u8; + let fit_interval_ind = self.get_orbit_f64("fitInt")? as u8; + + let code_on_l2_ind = 0; // TODO + + Some(Msg1019T { + gps_satellite_id, + gps_week_number: toc_week as u16, + ura_index, + code_on_l2_ind, + idot_sc_s, + toc_s, + iode, + omega0_sc, + omegadot_sc_s, + af2_s_s2: self.clock_drift_rate as f32, + af1_s_s: self.clock_drift as f32, + af0_s: self.clock_bias, + iodc, + crs_m, + delta_n_sc_s, + m0_sc, + cuc_rad, + eccentricity, + cus_rad, + sqrt_a_sqrt_m, + toe_s, + cic_rad, + i0_sc, + crc_m, + omega_sc, + cis_rad, + tgd_s, + sv_health_ind, + l2_p_data_flag, + fit_interval_ind, + }) + } + + /// Converts this [Ephemeris] to Glonass [Msg1020T] + /// ## Input + /// - epoch: [Epoch] of message reception + /// - sv: attached satellite as [SV] which must a [Constellation::Glonass] vehicle. + /// + /// ## Output + /// - [Msg1020T] + pub fn to_rtcm_msg1001(&self, epoch: Epoch, sv: SV) -> Option { + if sv.constellation != Constellation::Glonass { + return None; // invalid API usage + } + + let toe = self.toe(sv)?; + + let tweek_seconds = toe.to_time_of_week().1 * 1_000_000_000; + + let tk_h = 0; // TODO + let tk_min = 0; // TODO + let tk_s = 0; // TODO + + let glo_satellite_freq_chan_number = 0; // TODO + let glo_alm_health_flag = 0; // TODO + let glo_alm_health_avail_flag = 0; // TODO + + let glo_eph_health_flag = 0; // TODO + let p1_ind = 0; // TODO + let p2_flag = 0; // TODO + let p3_flag = 0; // TODO + let additional_data_flag = 0; // TODO + + let gamma_n = 0.0; // TODO + let tb_min = 0; // TODO + let tau_c_s = 0.0; // TODO + let tau_n_s = 0.0; // TODO + + let xn_km = 0.0; // TODO + let yn_km = 0.0; // TODO + let zn_km = 0.0; // TODO + + let xn_first_deriv_km_s = 0.0; // TODO + let yn_first_deriv_km_s = 0.0; // TODO + let zn_first_deriv_km_s = 0.0; // TODO + + let xn_second_deriv_km_s2 = 0.0; // TODO + let yn_second_deriv_km_s2 = 0.0; // TODO + let zn_second_deriv_km_s2 = 0.0; // TODO + + let en_d = 0; // TODO + let na_d = 0; // TODO + + let glo_m_m_ind = 0; // TODO + let glo_m_p_ind = 0; // TODO + let glo_m_ft_ind = 0; // TODO + let glo_m_nt_d = 0; // TODO + let glo_m_m_d = 0; // TODO + let glo_m_delta_tau_n_s = 0.0; // TODO + let glo_m_p4_flag = 0; // TODO + let glo_m_n4_year = 0; // TODO + let glo_m_tau_gps_s = 0.0; // TODO + let glo_m_3str_ln_flag = 0; // TODO + let glo_m_5str_ln_flag = 0; // TODO + let reserved_353_7 = 0; // TODO + + Some(Msg1020T { + glo_satellite_id: sv.prn, + glo_satellite_freq_chan_number, + glo_alm_health_flag, + glo_alm_health_avail_flag, + p1_ind, + tk_h, + tk_min, + tk_s, + glo_eph_health_flag, + p2_flag, + tb_min, + xn_first_deriv_km_s, + xn_km, + xn_second_deriv_km_s2, + yn_first_deriv_km_s, + yn_km, + yn_second_deriv_km_s2, + zn_first_deriv_km_s, + zn_km, + zn_second_deriv_km_s2, + p3_flag, + gamma_n, + glo_m_p_ind, + glo_m_3str_ln_flag, + tau_n_s, + glo_m_delta_tau_n_s, + en_d, + glo_m_p4_flag, + glo_m_ft_ind, + glo_m_nt_d, + glo_m_m_ind, + additional_data_flag, + na_d, + tau_c_s, + glo_m_n4_year, + glo_m_tau_gps_s, + glo_m_5str_ln_flag, + reserved_353_7, + }) + } +} From 5ca6d3306b741b844c534f4e306cb6e2e8ad30a1 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 13 Sep 2025 12:18:06 +0200 Subject: [PATCH 07/14] Ephemeris to RTCM helpers Signed-off-by: Guillaume W. Bres --- src/navigation/ephemeris/rtcm.rs | 349 +++++++++++++++++++++++++++++-- 1 file changed, 335 insertions(+), 14 deletions(-) diff --git a/src/navigation/ephemeris/rtcm.rs b/src/navigation/ephemeris/rtcm.rs index e223c20f..27166bca 100644 --- a/src/navigation/ephemeris/rtcm.rs +++ b/src/navigation/ephemeris/rtcm.rs @@ -7,30 +7,35 @@ use crate::{ prelude::{Constellation, Epoch, SV}, }; -use rtcm_rs::msg::{Msg1019T, Msg1020T}; +use rtcm_rs::msg::{ + Msg1019T, + Msg1020T, + Msg1042T, //Msg1043T, + Msg1044T, + Msg1045T, + Msg1046T, +}; #[cfg(doc)] use crate::prelude::Rinex; impl Ephemeris { - /// Converts this [Ephemeris] to [Constellation::GPS] [Msg1019T] + /// Converts this [Ephemeris] to [Msg1019T] [Constellation::GPS] ephemeris message. /// ## Input - /// - epoch: [Epoch] of message reception. + /// - toc: Time of Clock as [Epoch] /// - sv: attached satellite as [SV] which must a [Constellation::GPS] vehicle. /// /// ## Output - /// - [Msg1019T] - pub fn to_rtcm_msg1019(&self, epoch: Epoch, sv: SV) -> Option { + /// - [Msg1019T] GPS ephemeris message. + pub fn to_rtcm_gps_msg1019(&self, toc: Epoch, sv: SV) -> Option { if sv.constellation != Constellation::GPS { return None; // invalid API usage } - let (toc_week, toc_week_nanos) = epoch.to_time_of_week(); - let toc_s = (toc_week_nanos as f32) * 1.0E-9; + let (toc_week, toc_week_nanos) = toc.to_time_of_week(); - let toe = self.toe(sv)?; - let toe_week_nanos = toe.to_time_of_week().1; - let toe_s = (toe_week_nanos as f32) * 1.0E-9; + let toc_s = (toc_week_nanos as f32) * 1.0E-9; + let toe_s = self.toe(sv)?.duration.to_unit(Unit::Second) as f32; let gps_satellite_id = sv.prn; @@ -93,14 +98,14 @@ impl Ephemeris { }) } - /// Converts this [Ephemeris] to Glonass [Msg1020T] + /// Converts this [Ephemeris] to [Msg1020T] [Constellation::Glonass] ephemeris message. /// ## Input - /// - epoch: [Epoch] of message reception + /// - toc: Time of Clock as [Epoch] /// - sv: attached satellite as [SV] which must a [Constellation::Glonass] vehicle. /// /// ## Output - /// - [Msg1020T] - pub fn to_rtcm_msg1001(&self, epoch: Epoch, sv: SV) -> Option { + /// - [Msg1020T] Glonass ephemeris message. + pub fn to_rtcm_glo_msg1020(&self, toc: Epoch, sv: SV) -> Option { if sv.constellation != Constellation::Glonass { return None; // invalid API usage } @@ -197,4 +202,320 @@ impl Ephemeris { reserved_353_7, }) } + + /// Converts this [Ephemeris] to [Msg1045T] [Constellation::Galileo] ephemeris message. + /// ## Input + /// - toc: Time of Clock as [Epoch] + /// - sv: attached satellite as [SV] which must a [Constellation::Galileo] vehicle. + /// + /// ## Output + /// - [Msg1045T] Galileo ephemeris message. + pub fn to_rtcm_gal_msg1045(&self, toc: Epoch, sv: SV) -> Option { + if sv.constellation != Constellation::Galileo { + return None; // invalid API usage + } + + let (toc_week, toc_nanos) = toc.to_time_of_week(); + + let toc_s = (toc_nanos as f32) * 1.0E-9; + let toe_s = self.toe(sv)?.duration.to_unit(Unit::Second) as f32; + + let crc_m = self.get_orbit_f64("crc")? as f32; + let crs_m = self.get_orbit_f64("crs")? as f32; + let cic_rad = self.get_orbit_f64("cic")? as f32; + let cis_rad = self.get_orbit_f64("cis")? as f32; + let cuc_rad = self.get_orbit_f64("cuc")? as f32; + let cus_rad = self.get_orbit_f64("cus")? as f32; + let delta_n_sc_s = self.get_orbit_f64("deltaN")? as f32; + let eccentricity = self.get_orbit_f64("e")?; + let i0_sc = self.get_orbit_f64("i0")?; + let m0_sc = self.get_orbit_f64("m0")?; + let idot_sc_s = self.get_orbit_f64("idot")? as f32; + let omega0_sc = self.get_orbit_f64("omega0")?; + let omega_sc = self.get_orbit_f64("omega")?; + let omegadot_sc_s = self.get_orbit_f64("omegaDot")?; + let sqrt_a_sqrt_m = self.get_orbit_f64("sqrta")?; + + let iodnav = 0; // TODO + let bgd_e1_e5a_s = 0.0; // TODO + let e5a_data_validity_flag = 0; // TODO + let e5a_sig_health_ind = 0; // TODO + let reserved_489_7 = 0; // TODO + let sisa_e1_e5a_index = 0; // TODO + + Some(Msg1045T { + af0_s: self.clock_bias, + af1_s_s: self.clock_drift, + af2_s_s2: self.clock_drift_rate as f32, + bgd_e1_e5a_s, + cic_rad, + cis_rad, + cuc_rad, + cus_rad, + crs_m, + crc_m, + eccentricity, + delta_n_sc_s, + e5a_data_validity_flag, + e5a_sig_health_ind, + gal_satellite_id: sv.prn, + gal_week_number: toc_week as u16, + i0_sc, + m0_sc, + idot_sc_s, + iodnav, + omega0_sc, + omega_sc, + omegadot_sc_s, + reserved_489_7, + sisa_e1_e5a_index, + sqrt_a_sqrt_m, + toc_s, + toe_s, + }) + } + + /// Converts this [Ephemeris] to [Msg1046T] [Constellation::Galileo] ephemeris message. + /// ## Input + /// - toc: Time of Clock as [Epoch] + /// - sv: attached satellite as [SV] which must a [Constellation::Galileo] vehicle. + /// + /// ## Output + /// - [Msg1045T] Galileo ephemeris message. + pub fn to_rtcm_gal_msg1046(&self, toc: Epoch, sv: SV) -> Option { + if sv.constellation != Constellation::Galileo { + return None; // invalid API usage + } + + let (toc_week, toc_nanos) = toc.to_time_of_week(); + + let toc_s = (toc_nanos as f32) * 1.0e-9; + let toe_s = self.toe(sv)?.duration.to_unit(Unit::Second) as f32; + + let crc_m = self.get_orbit_f64("crc")? as f32; + let crs_m = self.get_orbit_f64("crs")? as f32; + let cic_rad = self.get_orbit_f64("cic")? as f32; + let cis_rad = self.get_orbit_f64("cis")? as f32; + let cuc_rad = self.get_orbit_f64("cuc")? as f32; + let cus_rad = self.get_orbit_f64("cus")? as f32; + let i0_sc = self.get_orbit_f64("i0")?; + let m0_sc = self.get_orbit_f64("m0")?; + let idot_sc_s = self.get_orbit_f64("idot")? as f32; + let eccentricity = self.get_orbit_f64("e")?; + let delta_n_sc_s = self.get_orbit_f64("deltaN")? as f32; + let omega_sc = self.get_orbit_f64("omega")?; + let omegadot_sc_s = self.get_orbit_f64("omegaDot")?; + let omega0_sc = self.get_orbit_f64("omega0")?; + let sqrt_a_sqrt_m = self.get_orbit_f64("sqrta")?; + + let bgd_e1_e5a_s = 0.0; // TODO + let bgd_e1_e5b_s = 0.0; // TODO + + let iodnav = 0; // TODO + let sisa_e1_e5b_index = 0; // TODO + let reserved_502_2 = 0; // TODO + let e1_b_data_validity_flag = 0; // TODO + let e1_b_sig_health_ind = 0; // TODO + let e5b_data_validity_flag = 0; // TODO + let e5b_sig_health_ind = 0; // TODO + + Some(Msg1046T { + af0_s: self.clock_bias, + af1_s_s: self.clock_drift, + af2_s_s2: self.clock_drift_rate as f32, + bgd_e1_e5a_s, + bgd_e1_e5b_s, + cic_rad, + cis_rad, + cuc_rad, + cus_rad, + crc_m, + crs_m, + gal_satellite_id: sv.prn, + gal_week_number: toc_week as u16, + i0_sc, + m0_sc, + delta_n_sc_s, + e1_b_data_validity_flag, + e1_b_sig_health_ind, + e5b_data_validity_flag, + e5b_sig_health_ind, + eccentricity, + idot_sc_s, + iodnav, + omega0_sc, + omega_sc, + omegadot_sc_s, + reserved_502_2, + sisa_e1_e5b_index, + sqrt_a_sqrt_m, + toc_s, + toe_s, + }) + } + + /// Converts this [Ephemeris] to [Msg1042T] [Constellation::BeiDou] ephemeris message. + /// ## Input + /// - toc: Time of Clock as [Epoch] + /// - sv: attached satellite as [SV] which must a [Constellation::BeiDou] vehicle. + /// + /// ## Output + /// - [Msg1042T] BDS ephemeris message. + pub fn to_rtcm_bds_msg1042(&self, toc: Epoch, sv: SV) -> Option { + if sv.constellation != Constellation::BeiDou { + return None; // invalid API usage + } + + let (toc_week, toc_nanos) = toc.to_time_of_week(); + + let toc_s = (toc_nanos as f32) * 1.0e-9; + let toe_s = self.toe(sv)?.duration.to_unit(Unit::Second) as f32; + + let aodc = 0; // TODO + let aode = 0; // TODO + + let crc_m = self.get_orbit_f64("crc")? as f32; + let crs_m = self.get_orbit_f64("crs")? as f32; + let cic_rad = self.get_orbit_f64("cic")? as f32; + let cis_rad = self.get_orbit_f64("cis")? as f32; + let cuc_rad = self.get_orbit_f64("cuc")? as f32; + let cus_rad = self.get_orbit_f64("cus")? as f32; + let delta_n_sc_s = self.get_orbit_f64("deltaN")? as f32; + let i0_sc = self.get_orbit_f64("i0")?; + let m0_sc = self.get_orbit_f64("m0")?; + let idot_sc_s = self.get_orbit_f64("idot")?; + let eccentricity = self.get_orbit_f64("e")?; + let omega_sc = self.get_orbit_f64("omega")?; + let omegadot_sc_s = self.get_orbit_f64("omegaDot")?; + let omega0_sc = self.get_orbit_f64("omega0")?; + let sqrt_a_sqrt_m = self.get_orbit_f64("sqrta")?; + + let sv_health_flag = 0; // TODO + let tgd1_s = self.tgd()?.to_unit(Unit::Second) as f32; + let tgd2_s = tgd1_s; // TODO + let ura_index = 0; // TODO + + Some(Msg1042T { + a0_s: self.clock_bias, + a1_s_s: self.clock_drift, + a2_s_s2: self.clock_drift_rate as f32, + aodc, + aode, + bds_satellite_id: sv.prn, + bds_week_number: toc_week as u16, + cic_rad, + cis_rad, + crc_m, + crs_m, + cuc_rad, + cus_rad, + delta_n_sc_s, + eccentricity, + i0_sc, + m0_sc, + idot_sc_s, + omega0_sc, + omega_sc, + omegadot_sc_s, + sqrt_a_sqrt_m, + sv_health_flag, + tgd1_s, + tgd2_s, + toc_s, + toe_s, + ura_index, + }) + } + + /// Converts this [Ephemeris] to [Msg1044T] [Constellation::QZSS] ephemeris message. + /// ## Input + /// - epoch: [Epoch] of message reception. + /// - sv: attached satellite as [SV] which must a [Constellation::QZSS] vehicle. + /// + /// ## Output + /// - [Msg1044T] QZSS ephemeris message. + pub fn to_rtcm_qzss_msg1044(&self, epoch: Epoch, sv: SV) -> Option { + if sv.constellation != Constellation::GPS { + return None; // invalid API usage + } + + let (toc_week, toc_week_nanos) = epoch.to_time_of_week(); + let toc_s = (toc_week_nanos as f32) * 1.0E-9; + + let toe_s = self.toe(sv)?.duration.to_unit(Unit::Second) as f32; + + let idot_sc_s = self.get_orbit_f64("idot")?; + let iodc = self.get_orbit_f64("iodc")? as u16; + let crs_m = self.get_orbit_f64("crs")? as f32; + let delta_n_sc_s = self.get_orbit_f64("deltaN")? as f32; + let m0_sc = self.get_orbit_f64("m0")?; + let cic_rad = self.get_orbit_f64("cic")? as f32; + let cis_rad = self.get_orbit_f64("cis")? as f32; + let cuc_rad = self.get_orbit_f64("cuc")? as f32; + let cus_rad = self.get_orbit_f64("cus")? as f32; + let eccentricity = self.get_orbit_f64("e")?; + let sqrt_a_sqrt_m = self.get_orbit_f64("sqrta")?; + let i0_sc = self.get_orbit_f64("i0")?; + let iode = self.get_orbit_f64("iode")? as u8; + let crc_m = self.get_orbit_f64("crc")? as f32; + let omega_sc = self.get_orbit_f64("omega")?; + let omegadot_sc_s = self.get_orbit_f64("omegaDot")?; + let omega0_sc = self.get_orbit_f64("omega0")?; + let tgd_s = self.tgd()?.to_unit(Unit::Second) as f32; + let sv_health_ind = self.get_orbit_f64("health")? as u8; + let l2_p_data_flag = self.get_orbit_f64("l2p")? as u8; + let fit_interval_ind = self.get_orbit_f64("fitInt")? as u8; + + let code_on_l2_ind = 0; // TODO + let ura_index = 0; // TODO + + Some(Msg1044T { + qzss_satellite_id: sv.prn, + qzss_week_number: toc_week as u16, + ura_index, + code_on_l2_ind, + idot_sc_s, + toc_s, + iode, + omega0_sc, + omegadot_sc_s, + af2_s_s2: self.clock_drift_rate as f32, + af1_s_s: self.clock_drift as f32, + af0_s: self.clock_bias, + iodc, + crs_m, + delta_n_sc_s, + m0_sc, + cuc_rad, + eccentricity, + cus_rad, + sqrt_a_sqrt_m, + toe_s, + cic_rad, + i0_sc, + crc_m, + omega_sc, + cis_rad, + tgd_s, + sv_health_ind, + fit_interval_ind, + }) + } + + // /// Converts this [Ephemeris] to [Msg1043T] [Constellation::SBAS] ephemeris message. + // /// ## Input + // /// - epoch: [Epoch] of message reception. + // /// - sv: attached satellite as [SV] which must a [Constellation::SBAS] vehicle. + // /// + // /// ## Output + // /// - [Msg1043T] SBAS ephemeris message. + // pub fn to_rtcm_sbas_msg1043(&self, epoch: Epoch, sv: SV) -> Option { + // if !sv.constellation.is_sbas() { + // return None; // invalid API usage + // } + + // Some(Msg1043T { + + // }) + // } } From d56c64db6da0b21f5f302531d5639128e7acc5f2 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 13 Sep 2025 12:23:24 +0200 Subject: [PATCH 08/14] Ephemeris to RTCM helpers Signed-off-by: Guillaume W. Bres --- src/rtcm/nav.rs | 31 ++++++++++++++++++++++++++++++- src/ublox/mod.rs | 2 -- src/ublox/nav.rs | 2 -- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/rtcm/nav.rs b/src/rtcm/nav.rs index 833bb284..9133c63f 100644 --- a/src/rtcm/nav.rs +++ b/src/rtcm/nav.rs @@ -24,6 +24,35 @@ impl<'a> Iterator for Streamer<'a> { /// Try to serialize a new RTCM [Message] from this [Streamer]. fn next(&mut self) -> Option { - None + loop { + let (key, eph) = self.ephemeris_iter.next()?; + + match key.sv.constellation { + Constellation::GPS => { + let msg1019 = eph.to_rtcm_gps_msg1019(key.epoch, key.sv)?; + return Some(Message::Msg1019(msg1019)); + }, + Constellation::QZSS => { + let msg1044 = eph.to_rtcm_qzss_msg1044(key.epoch, key.sv)?; + return Some(Message::Msg1044(msg1044)); + }, + Constellation::Galileo => { + // TODO may have 2 forms + let msg1045 = eph.to_rtcm_gal_msg1045(key.epoch, key.sv)?; + return Some(Message::Msg1045(msg1045)); + }, + Constellation::Glonass => { + let msg1020 = eph.to_rtcm_glo_msg1020(key.epoch, key.sv)?; + return Some(Message::Msg1020(msg1020)); + }, + Constellation::BeiDou => { + let msg1042 = eph.to_rtcm_bds_msg1042(key.epoch, key.sv)?; + return Some(Message::Msg1042(msg1042)); + }, + _ => { + // Not supported yet + }, + } + } } } diff --git a/src/ublox/mod.rs b/src/ublox/mod.rs index fb6f2159..f4fc1482 100644 --- a/src/ublox/mod.rs +++ b/src/ublox/mod.rs @@ -3,8 +3,6 @@ use crate::prelude::Rinex; mod nav; use nav::Streamer as NavStreamer; -use ublox::PacketRef; - #[cfg(doc)] use ublox::Parser; diff --git a/src/ublox/nav.rs b/src/ublox/nav.rs index fd3ecb06..7fa19867 100644 --- a/src/ublox/nav.rs +++ b/src/ublox/nav.rs @@ -3,8 +3,6 @@ use crate::{ prelude::{Constellation, Rinex}, }; -use ublox::PacketRef; - use std::io::{Error, ErrorKind}; pub struct Streamer<'a> { From 0db1ce9cba7755a2b96b0b16b36dbe829e493271 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 13 Sep 2025 13:01:59 +0200 Subject: [PATCH 09/14] Ephemeris to RTCM helpers Signed-off-by: Guillaume W. Bres --- src/navigation/ephemeris/mod.rs | 49 +++++++++++ src/navigation/ephemeris/rtcm.rs | 35 ++++---- src/rtcm/nav.rs | 10 +-- src/tests/rtcm.rs | 140 ++++++++++--------------------- 4 files changed, 115 insertions(+), 119 deletions(-) diff --git a/src/navigation/ephemeris/mod.rs b/src/navigation/ephemeris/mod.rs index 314d1509..c9f822b6 100644 --- a/src/navigation/ephemeris/mod.rs +++ b/src/navigation/ephemeris/mod.rs @@ -412,3 +412,52 @@ impl Ephemeris { } } } + +impl std::fmt::Display for Ephemeris { + /// Format and Displays this [Ephemeris] conveniently + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if let Some(t_tm) = self.get_orbit_f64("t_tm") { + write!(f, "T_tm={}s, ", t_tm as u32)?; + } + + if let Some(iode) = self.get_orbit_f64("iode") { + write!(f, "IODE={}, ", iode as u8)?; + } + + if let Some(crc) = self.get_orbit_f64("crc") { + if let Some(crs) = self.get_orbit_f64("crs") { + write!(f, "Cr=(cos={:.5E}m sin={:.5E}m), ", crc, crs)?; + } + } + + if let Some(cic) = self.get_orbit_f64("cic") { + if let Some(cis) = self.get_orbit_f64("cis") { + write!(f, "Ci=(cos={:.5E}rad sin={:.5E}rad), ", cic, cis)?; + } + } + + if let Some(cuc) = self.get_orbit_f64("cuc") { + if let Some(cus) = self.get_orbit_f64("cus") { + write!(f, "Cu=(cos={:.5E}rad sin={:.5E}rad), ", cuc, cus)?; + } + } + + if let Some(omega) = self.get_orbit_f64("omega") { + if let Some(omega_dot) = self.get_orbit_f64("omegaDot") { + if let Some(omega0) = self.get_orbit_f64("omega0") { + write!( + f, + "Omega=({:.5E}cs {:.5E}cs/s), Omega0={:.5E}, ", + omega, omega_dot, omega0 + )?; + } + } + } + + if let Some(ura) = self.get_orbit_f64("accuracy") { + write!(f, "URA={}, ", ura as u8)?; + } + + Ok(()) + } +} diff --git a/src/navigation/ephemeris/rtcm.rs b/src/navigation/ephemeris/rtcm.rs index 27166bca..3cdc1290 100644 --- a/src/navigation/ephemeris/rtcm.rs +++ b/src/navigation/ephemeris/rtcm.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use hifitime::prelude::Unit; +use hifitime::prelude::{Duration, Unit}; use crate::{ navigation::{Ephemeris, OrbitItem}, @@ -27,7 +27,7 @@ impl Ephemeris { /// /// ## Output /// - [Msg1019T] GPS ephemeris message. - pub fn to_rtcm_gps_msg1019(&self, toc: Epoch, sv: SV) -> Option { + pub fn to_rtcm_gps1019(&self, toc: Epoch, sv: SV) -> Option { if sv.constellation != Constellation::GPS { return None; // invalid API usage } @@ -57,10 +57,10 @@ impl Ephemeris { let omega_sc = self.get_orbit_f64("omega")?; let omegadot_sc_s = self.get_orbit_f64("omegaDot")?; let omega0_sc = self.get_orbit_f64("omega0")?; - let tgd_s = self.tgd()?.to_unit(Unit::Second) as f32; let sv_health_ind = self.get_orbit_f64("health")? as u8; let l2_p_data_flag = self.get_orbit_f64("l2p")? as u8; - let fit_interval_ind = self.get_orbit_f64("fitInt")? as u8; + let fit_interval_ind = self.get_orbit_f64("fitInt").unwrap_or_default() as u8; // TODO fit int issue + let tgd_s = self.tgd().unwrap_or(Duration::ZERO).to_unit(Unit::Second) as f32; let code_on_l2_ind = 0; // TODO @@ -105,7 +105,7 @@ impl Ephemeris { /// /// ## Output /// - [Msg1020T] Glonass ephemeris message. - pub fn to_rtcm_glo_msg1020(&self, toc: Epoch, sv: SV) -> Option { + pub fn to_rtcm_glo1020(&self, toc: Epoch, sv: SV) -> Option { if sv.constellation != Constellation::Glonass { return None; // invalid API usage } @@ -210,7 +210,7 @@ impl Ephemeris { /// /// ## Output /// - [Msg1045T] Galileo ephemeris message. - pub fn to_rtcm_gal_msg1045(&self, toc: Epoch, sv: SV) -> Option { + pub fn to_rtcm_gal1045(&self, toc: Epoch, sv: SV) -> Option { if sv.constellation != Constellation::Galileo { return None; // invalid API usage } @@ -235,13 +235,13 @@ impl Ephemeris { let omega_sc = self.get_orbit_f64("omega")?; let omegadot_sc_s = self.get_orbit_f64("omegaDot")?; let sqrt_a_sqrt_m = self.get_orbit_f64("sqrta")?; + let iodnav = self.get_orbit_f64("iodnav").unwrap_or_default() as u16; // TODO IODNAV issue? + let bgd_e1_e5a_s = self.get_orbit_f64("bgdE5aE1").unwrap_or_default() as f32; // TODO BGD_E1/E5A + let sisa_e1_e5a_index = self.get_orbit_f64("sisa").unwrap_or_default() as u8; // TODO SISA index - let iodnav = 0; // TODO - let bgd_e1_e5a_s = 0.0; // TODO let e5a_data_validity_flag = 0; // TODO let e5a_sig_health_ind = 0; // TODO let reserved_489_7 = 0; // TODO - let sisa_e1_e5a_index = 0; // TODO Some(Msg1045T { af0_s: self.clock_bias, @@ -282,7 +282,7 @@ impl Ephemeris { /// /// ## Output /// - [Msg1045T] Galileo ephemeris message. - pub fn to_rtcm_gal_msg1046(&self, toc: Epoch, sv: SV) -> Option { + pub fn to_rtcm_gal1046(&self, toc: Epoch, sv: SV) -> Option { if sv.constellation != Constellation::Galileo { return None; // invalid API usage } @@ -361,7 +361,7 @@ impl Ephemeris { /// /// ## Output /// - [Msg1042T] BDS ephemeris message. - pub fn to_rtcm_bds_msg1042(&self, toc: Epoch, sv: SV) -> Option { + pub fn to_rtcm_bds1042(&self, toc: Epoch, sv: SV) -> Option { if sv.constellation != Constellation::BeiDou { return None; // invalid API usage } @@ -391,10 +391,11 @@ impl Ephemeris { let sqrt_a_sqrt_m = self.get_orbit_f64("sqrta")?; let sv_health_flag = 0; // TODO - let tgd1_s = self.tgd()?.to_unit(Unit::Second) as f32; - let tgd2_s = tgd1_s; // TODO let ura_index = 0; // TODO + let tgd1_s = self.tgd().unwrap_or(Duration::ZERO).to_unit(Unit::Second) as f32; + let tgd2_s = tgd1_s; // TODO + Some(Msg1042T { a0_s: self.clock_bias, a1_s_s: self.clock_drift, @@ -434,14 +435,14 @@ impl Ephemeris { /// /// ## Output /// - [Msg1044T] QZSS ephemeris message. - pub fn to_rtcm_qzss_msg1044(&self, epoch: Epoch, sv: SV) -> Option { - if sv.constellation != Constellation::GPS { + pub fn to_rtcm_qzss1044(&self, epoch: Epoch, sv: SV) -> Option { + if sv.constellation != Constellation::QZSS { return None; // invalid API usage } let (toc_week, toc_week_nanos) = epoch.to_time_of_week(); - let toc_s = (toc_week_nanos as f32) * 1.0E-9; + let toc_s = (toc_week_nanos as f32) * 1.0E-9; let toe_s = self.toe(sv)?.duration.to_unit(Unit::Second) as f32; let idot_sc_s = self.get_orbit_f64("idot")?; @@ -464,7 +465,7 @@ impl Ephemeris { let tgd_s = self.tgd()?.to_unit(Unit::Second) as f32; let sv_health_ind = self.get_orbit_f64("health")? as u8; let l2_p_data_flag = self.get_orbit_f64("l2p")? as u8; - let fit_interval_ind = self.get_orbit_f64("fitInt")? as u8; + let fit_interval_ind = self.get_orbit_f64("fitInt").unwrap_or_default() as u8; // TODO fitInt issue let code_on_l2_ind = 0; // TODO let ura_index = 0; // TODO diff --git a/src/rtcm/nav.rs b/src/rtcm/nav.rs index 9133c63f..f806b0e4 100644 --- a/src/rtcm/nav.rs +++ b/src/rtcm/nav.rs @@ -29,24 +29,24 @@ impl<'a> Iterator for Streamer<'a> { match key.sv.constellation { Constellation::GPS => { - let msg1019 = eph.to_rtcm_gps_msg1019(key.epoch, key.sv)?; + let msg1019 = eph.to_rtcm_gps1019(key.epoch, key.sv)?; return Some(Message::Msg1019(msg1019)); }, Constellation::QZSS => { - let msg1044 = eph.to_rtcm_qzss_msg1044(key.epoch, key.sv)?; + let msg1044 = eph.to_rtcm_qzss1044(key.epoch, key.sv)?; return Some(Message::Msg1044(msg1044)); }, Constellation::Galileo => { // TODO may have 2 forms - let msg1045 = eph.to_rtcm_gal_msg1045(key.epoch, key.sv)?; + let msg1045 = eph.to_rtcm_gal1045(key.epoch, key.sv)?; return Some(Message::Msg1045(msg1045)); }, Constellation::Glonass => { - let msg1020 = eph.to_rtcm_glo_msg1020(key.epoch, key.sv)?; + let msg1020 = eph.to_rtcm_glo1020(key.epoch, key.sv)?; return Some(Message::Msg1020(msg1020)); }, Constellation::BeiDou => { - let msg1042 = eph.to_rtcm_bds_msg1042(key.epoch, key.sv)?; + let msg1042 = eph.to_rtcm_bds1042(key.epoch, key.sv)?; return Some(Message::Msg1042(msg1042)); }, _ => { diff --git a/src/tests/rtcm.rs b/src/tests/rtcm.rs index 5f0691dc..913e1173 100644 --- a/src/tests/rtcm.rs +++ b/src/tests/rtcm.rs @@ -1,5 +1,3 @@ -use std::io::Read; - use crate::{ navigation::Ephemeris, prelude::{Constellation, Rinex}, @@ -9,17 +7,56 @@ use crate::{ #[test] #[cfg(feature = "nav")] fn esbcdnk_ephv3_to_rtcm() { + let mut gps1019 = 0; + let mut glo1020 = 0; + let mut bds1042 = 0; + let mut qzss1044 = 0; + let mut gal1045 = 0; + let rinex = Rinex::from_gzip_file("data/NAV/V3/ESBC00DNK_R_20201770000_01D_MN.rnx.gz").unwrap(); for (k, ephemeris) in rinex.nav_ephemeris_frames_iter() { match k.sv.constellation { - Constellation::GPS => {}, - Constellation::QZSS => {}, - Constellation::BeiDou => {}, - Constellation::Galileo => {}, + Constellation::GPS => { + if let Some(msg) = ephemeris.to_rtcm_gps1019(k.epoch, k.sv) { + gps1019 += 1; + } + }, + Constellation::QZSS => { + if let Some(msg) = ephemeris.to_rtcm_qzss1044(k.epoch, k.sv) { + qzss1044 += 1; + } + }, + Constellation::BeiDou => { + if let Some(msg) = ephemeris.to_rtcm_bds1042(k.epoch, k.sv) { + bds1042 += 1; + } + }, + Constellation::Galileo => { + if let Some(msg) = ephemeris.to_rtcm_gal1045(k.epoch, k.sv) { + gal1045 += 1; + } + }, + Constellation::Glonass => { + if let Some(msg) = ephemeris.to_rtcm_glo1020(k.epoch, k.sv) { + glo1020 += 1; + } + }, _ => {}, // not supported yet } } + + assert!(gps1019 > 0); + // assert!(glo1020 > 0); // TODO + assert!(bds1042 > 0); + assert!(qzss1044 > 0); + assert!(gal1045 > 0); + + assert_eq!(gps1019, 253); + // assert_eq!(glo1020, 0); + assert_eq!(gal1045, 806); + assert_eq!(bds1042, 353); + assert_eq!(qzss1044, 15); } // GLO (V2) to RTCM @@ -53,95 +90,4 @@ fn esbcdnk_nav3_to_ubx() { let mut buffer = [0; 2048]; let rinex = Rinex::from_gzip_file("data/NAV/V3/ESBC00DNK_R_20201770000_01D_MN.rnx.gz").unwrap(); - - let mut streamer = rinex.rnx2ubx(); - - let mut parser = Parser::default(); // tester - - loop { - match streamer.read(&mut buffer) { - Ok(0) => { - break; - }, - Ok(size) => { - total_size += size; - let mut iter = parser.consume_ubx(&buffer); - - loop { - match iter.next() { - Some(message) => match message { - Ok(packet) => match packet { - PacketRef::MgaGpsEph(_) => { - total_mga_gps_eph += 1; - }, - PacketRef::MgaBdsEph(_) => { - total_mga_bds_eph += 1; - }, - PacketRef::MgaGalEph(_) => { - total_mga_gal_eph += 1; - }, - PacketRef::MgaGloEph(_) => { - total_mga_glo_eph += 1; - }, - msg => { - panic!("unexpected UBX message found: {:?}", msg); - }, - }, - Err(e) => { - panic!("invalid UBX content identified: {}", e); - }, - }, - None => break, - } - total_msg += 1; - } - }, - Err(_) => {}, - } - } - - assert!(total_size > 0); - assert!(total_msg > 0); - - // assert_eq!(total_mga_gps_eph, 253 + 15); // TODO: this fails, should be GPS+QZSS from test #1 - assert_eq!(total_mga_bds_eph, 360); - // assert_eq!(total_mga_gal_eph, 806); - - println!("ESCDNK-NAV (V3): {:8} bytes", total_size); - println!("ESCDNK-NAV (V3): {:8} messages", total_msg); - println!( - "ESCDNK-NAV (V3): {:8} MGA-GPS-EPH frames", - total_mga_gps_eph - ); - println!( - "ESCDNK-NAV (V3): {:8} MGA-GAL-EPH frames", - total_mga_glo_eph - ); - println!( - "ESCDNK-NAV (V3): {:8} MGA-BDS-EPH frames", - total_mga_bds_eph - ); - println!( - "ESCDNK-NAV (V3): {:8} MGA-GLO-EPH frames", - total_mga_glo_eph - ); -} - -// MGA-TIM-XXX -#[test] -fn esbcdnk_timv4_to_ubx_mga() { - let mut total_msg = 0; - let mut total_size = 0; - - let mut buffer = [0; 2048]; - - let rinex = Rinex::from_gzip_file("data/NAV/V4/BRD400DLR_S_20230710000_01D_MN.rnx.gz").unwrap(); - - for (k, time_offset) in rinex.nav_system_time_frames_iter() { - match k.sv.constellation { - Constellation::GPS => {}, - Constellation::Galileo => {}, - _ => {}, - } - } } From 3671775a174ff0b810fbf5b1dcace23895f26cd6 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 13 Sep 2025 13:15:26 +0200 Subject: [PATCH 10/14] Ephemeris to RTCM helpers Signed-off-by: Guillaume W. Bres --- src/navigation/ephemeris/rtcm.rs | 39 +++++++++++++++++--------------- src/tests/rtcm.rs | 4 ++-- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/navigation/ephemeris/rtcm.rs b/src/navigation/ephemeris/rtcm.rs index 3cdc1290..d4a1137b 100644 --- a/src/navigation/ephemeris/rtcm.rs +++ b/src/navigation/ephemeris/rtcm.rs @@ -99,6 +99,11 @@ impl Ephemeris { } /// Converts this [Ephemeris] to [Msg1020T] [Constellation::Glonass] ephemeris message. + /// + /// NB: we tolerate null secondary derivative terms (acceleration terms), which may + /// impact the accuracy of your navigation. Double check the output value and possibly + /// post-correct them. + /// /// ## Input /// - toc: Time of Clock as [Epoch] /// - sv: attached satellite as [SV] which must a [Constellation::Glonass] vehicle. @@ -110,13 +115,11 @@ impl Ephemeris { return None; // invalid API usage } - let toe = self.toe(sv)?; - - let tweek_seconds = toe.to_time_of_week().1 * 1_000_000_000; + let toc_nanos = Duration::from_nanoseconds(toc.to_time_of_week().1 as f64); - let tk_h = 0; // TODO - let tk_min = 0; // TODO - let tk_s = 0; // TODO + let tk_h = toc_nanos.to_unit(Unit::Hour).round() as u8; + let tk_min = (toc_nanos.to_unit(Unit::Hour) / 60.0).round() as u8; + let tk_s = toc_nanos.to_unit(Unit::Second).round() as u8; let glo_satellite_freq_chan_number = 0; // TODO let glo_alm_health_flag = 0; // TODO @@ -133,17 +136,17 @@ impl Ephemeris { let tau_c_s = 0.0; // TODO let tau_n_s = 0.0; // TODO - let xn_km = 0.0; // TODO - let yn_km = 0.0; // TODO - let zn_km = 0.0; // TODO + let xn_km = self.get_orbit_f64("satPosX")?; + let yn_km = self.get_orbit_f64("satPosY")?; + let zn_km = self.get_orbit_f64("satPosZ")?; - let xn_first_deriv_km_s = 0.0; // TODO - let yn_first_deriv_km_s = 0.0; // TODO - let zn_first_deriv_km_s = 0.0; // TODO + let xn_first_deriv_km_s = self.get_orbit_f64("velX")?; + let yn_first_deriv_km_s = self.get_orbit_f64("velY")?; + let zn_first_deriv_km_s = self.get_orbit_f64("velZ")?; - let xn_second_deriv_km_s2 = 0.0; // TODO - let yn_second_deriv_km_s2 = 0.0; // TODO - let zn_second_deriv_km_s2 = 0.0; // TODO + let xn_second_deriv_km_s2 = self.get_orbit_f64("accelX").unwrap_or_default() as f32; + let yn_second_deriv_km_s2 = self.get_orbit_f64("accelY").unwrap_or_default() as f32; + let zn_second_deriv_km_s2 = self.get_orbit_f64("accelZ").unwrap_or_default() as f32; let en_d = 0; // TODO let na_d = 0; // TODO @@ -167,9 +170,9 @@ impl Ephemeris { glo_alm_health_flag, glo_alm_health_avail_flag, p1_ind, - tk_h, - tk_min, - tk_s, + tk_h: tk_h as u8, + tk_min: tk_min as u8, + tk_s: tk_s as u8, glo_eph_health_flag, p2_flag, tb_min, diff --git a/src/tests/rtcm.rs b/src/tests/rtcm.rs index 913e1173..6b221773 100644 --- a/src/tests/rtcm.rs +++ b/src/tests/rtcm.rs @@ -47,13 +47,13 @@ fn esbcdnk_ephv3_to_rtcm() { } assert!(gps1019 > 0); - // assert!(glo1020 > 0); // TODO + assert!(glo1020 > 0); assert!(bds1042 > 0); assert!(qzss1044 > 0); assert!(gal1045 > 0); assert_eq!(gps1019, 253); - // assert_eq!(glo1020, 0); + assert_eq!(glo1020, 510); assert_eq!(gal1045, 806); assert_eq!(bds1042, 353); assert_eq!(qzss1044, 15); From e4672013419aec38afbb7d3fe5dfe6552781156d Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 13 Sep 2025 13:16:48 +0200 Subject: [PATCH 11/14] BIN2RINEX collection is not feasible in this crate, needs dedicated app Signed-off-by: Guillaume W. Bres --- src/binex/bin2rnx.rs | 288 --------------------------------- src/binex/mod.rs | 195 +++++++++++++++++++++- src/binex/{rnx2bin => }/nav.rs | 0 src/binex/rnx2bin/mod.rs | 192 ---------------------- src/lib.rs | 6 +- 5 files changed, 192 insertions(+), 489 deletions(-) delete mode 100644 src/binex/bin2rnx.rs rename src/binex/{rnx2bin => }/nav.rs (100%) delete mode 100644 src/binex/rnx2bin/mod.rs diff --git a/src/binex/bin2rnx.rs b/src/binex/bin2rnx.rs deleted file mode 100644 index 392aa183..00000000 --- a/src/binex/bin2rnx.rs +++ /dev/null @@ -1,288 +0,0 @@ -//! BINEX to RINEX deserialization -use std::io::Read; - -use crate::{ - prelude::{Duration, Epoch, Rinex}, - production::{Postponing, SnapshotMode}, -}; - -use binex::prelude::{Decoder, EphemerisFrame, Message, Record, SolutionsFrame, StreamElement}; - -#[cfg(feature = "log")] -use log::{error, info}; - -/// BIN2RNX is a RINEX producer from a BINEX stream. -/// It can serialize the streamed messages and collect them as RINEX. -/// The production behavior is defined by [SnapshotMode] -pub struct BIN2RNX<'a, R: Read> { - /// True when collecting is feasible - pub active: bool, - - /// Collected size, for postponing mechanism - size: usize, - - /// Snapshot mode - pub snapshot_mode: SnapshotMode, - - /// Postponing option - pub postponing: Postponing, - - /// Deploy time - deploy_t: Epoch, - - /// BINEX [Decoder] - decoder: Decoder<'a, R>, - - /// Pending NAV [Rinex] - nav_rinex: Rinex, - - /// Pending OBS [Rinex] - obs_rinex: Rinex, -} - -impl<'a, R: Read> Iterator for BIN2RNX<'a, R> { - type Item = Option; - fn next(&mut self) -> Option { - match self.decoder.next() { - Some(Ok(StreamElement::OpenSource(msg))) => { - if self.active { - match msg.record { - Record::EphemerisFrame(fr) => { - //let nav = self.nav_rinex.record.as_mut_nav().unwrap(); - match fr { - EphemerisFrame::GAL(_) => {}, - EphemerisFrame::GLO(_) => {}, - EphemerisFrame::GPS(_) => {}, - EphemerisFrame::SBAS(_) => {}, - EphemerisFrame::GPSRaw(_raw) => {}, - } - }, - Record::MonumentGeo(geo) => for _ in geo.frames.iter() {}, - Record::Solutions(pvt) => { - for fr in pvt.frames.iter() { - match fr { - SolutionsFrame::AntennaEcefPosition(_ecef) => {}, - SolutionsFrame::AntennaGeoPosition(_geo) => {}, - SolutionsFrame::Comment(_comment) => {}, - SolutionsFrame::TemporalSolution(_time) => {}, - SolutionsFrame::TimeSystem(_time) => {}, - SolutionsFrame::AntennaEcefVelocity(_ecef) => {}, - SolutionsFrame::AntennaGeoVelocity(_geo) => {}, - SolutionsFrame::Extra(_extra) => {}, - } - } - }, - } - } else { - self.postponed(&msg); - } - }, - #[cfg(feature = "log")] - Some(Ok(StreamElement::ClosedSource(msg))) => { - error!( - "received closed source message: cannot interprate {:?}", - msg.closed_meta - ) - }, - #[cfg(not(feature = "log"))] - Some(Ok(StreamElement::ClosedSource(_))) => {}, - #[cfg(feature = "log")] - Some(Err(e)) => { - error!("binex decoding error: {:?}", e); - }, - #[cfg(not(feature = "log"))] - Some(Err(_)) => {}, - _ => {}, - } - - None - } -} - -impl<'a, R: Read> BIN2RNX<'a, R> { - /// Creates a new [BIN2RNX] working from [Read]able interface. - /// It will stream Tokens as long as the interface is alive. - /// - /// NB: - /// - [BIN2RNX] needs the system time to be determined for the postponing - /// mechanism. If determination fails, this method will panic. - /// We propose [Self::new_system_time] if you want to manually - /// define "now". - /// - since RINEX is fully open source, only open source BINEX messages - /// may be picked up and collected: closed source elements are discarded. - /// - /// ## Inputs - /// - crinex: set to true if you want to use the CRINEX compression - /// algorithm when collecting Observation RINEX. - /// - production rate control as [SnapshotMode] - /// - [Postponing] option - /// - read: [Read]able interface - pub fn new(crinex: bool, snapshot_mode: SnapshotMode, postponing: Postponing, read: R) -> Self { - Self::new_system_time( - crinex, - Epoch::now().unwrap_or_else(|e| panic!("system time determination failed with {}", e)), - snapshot_mode, - postponing, - read, - ) - } - - /// Infaillible [BIN2RNX] creation, use this if you have no means to access system time. - /// Define it yourself with "now". Refer to [Self::new] for more information. - /// - /// ## Inputs - /// - crinex: set to true if you want to use the CRINEX compression - /// algorithm when collecting Observation RINEX. - pub fn new_system_time( - crinex: bool, - now: Epoch, - snapshot_mode: SnapshotMode, - postponing: Postponing, - read: R, - ) -> Self { - Self { - size: 0, - postponing, - snapshot_mode, - deploy_t: now, - nav_rinex: Rinex::basic_nav(), - obs_rinex: if crinex { - Rinex::basic_crinex() - } else { - Rinex::basic_obs() - }, - decoder: Decoder::new(read), - active: postponing == Postponing::None, - } - } - - /// Creates a new [BIN2RNX] that will collect a [Rinex] once a day at midnight, - /// with deployment possibly postponed. - /// - /// ## Inputs - /// - crinex: set to true if you want to use the CRINEX compression - /// algorithm when collecting Observation RINEX. - /// - [Postponing] option - /// - read: [Read]able interface - pub fn new_daily(crinex: bool, postponing: Postponing, read: R) -> Self { - Self::new(crinex, SnapshotMode::DailyMidnight, postponing, read) - } - - /// Creates a new [BIN2RNX] that will collect a [Rinex] twice a day at midnight and noon, - /// with deployment possibly postponed. - /// - /// ## Inputs - /// - crinex: set to true if you want to use the CRINEX compression - /// algorithm when collecting Observation RINEX. - /// - [Postponing] option - /// - read: [Read]able interface - pub fn new_midnight_noon(crinex: bool, postponing: Postponing, read: R) -> Self { - Self::new(crinex, SnapshotMode::DailyMidnightNoon, postponing, read) - } - - /// Creates a new [BIN2RNX] that will collect a [Rinex] hourly - /// with deployment possibly postponed. - /// - /// ## Inputs - /// - crinex: set to true if you want to use the CRINEX compression - /// algorithm when collecting Observation RINEX. - /// - [Postponing] option - /// - read: [Read]able interface - pub fn new_hourly(crinex: bool, postponing: Postponing, read: R) -> Self { - Self::new(crinex, SnapshotMode::Hourly, postponing, read) - } - - /// Creates a new [BIN2RNX] that will collect a [Rinex] periodically, - /// with deployment possibly postponed. - /// ## Inputs - /// - crinex: set to true if you want to use the CRINEX compression - /// algorithm when collecting Observation RINEX. - /// - period: production period, as [Duration] - /// - [Postponing] option - /// - read: [Read]able interface - pub fn new_periodic(crinex: bool, period: Duration, postponing: Postponing, read: R) -> Self { - Self::new(crinex, SnapshotMode::Periodic(period), postponing, read) - } - - fn postponed(&mut self, msg: &Message) { - match self.postponing { - Postponing::SystemTime(t) => self.system_time_postponing(t), - Postponing::Size(size) => self.bytewise_postponing(msg.encoding_size(), size), - Postponing::Messages(size) => self.protocol_postponing(size), - _ => unreachable!("no postponing!"), - } - } - - /// Holds production until system time as reached specific instant - fn system_time_postponing(&mut self, t: Epoch) { - let now = - Epoch::now().unwrap_or_else(|e| panic!("system time determination failure: {}", e)); - - if now > t { - // todo log message - self.active = true; - self.deploy_t = now; - } - } - - /// Collect "size" bytes until production is allowed - fn bytewise_postponing(&mut self, msg_size: usize, size: usize) { - self.size += msg_size; - if self.size >= size { - #[cfg(feature = "log")] - info!("bin2rnx now deployed: production is pending"); - let now = - Epoch::now().unwrap_or_else(|e| panic!("system time determination failure: {}", e)); - self.active = true; - self.deploy_t = now; - } else { - #[cfg(feature = "log")] - info!("binex postponing.."); - } - } - - /// Collect "size" messages until production is allowed - fn protocol_postponing(&mut self, size: usize) { - match self.decoder.next() { - Some(Ok(StreamElement::OpenSource(_))) => { - self.size += 1; - #[cfg(feature = "log")] - info!("binex postponing {}/{} messages", self.size, size); - }, - #[cfg(feature = "log")] - Some(Ok(StreamElement::ClosedSource(msg))) => { - error!( - "received closed source message: cannot interprate {:?}", - msg.closed_meta - ) - }, - #[cfg(not(feature = "log"))] - Some(Ok(StreamElement::ClosedSource(_))) => {}, - #[cfg(feature = "log")] - Some(Err(e)) => { - error!("binex decoding error: {:?}", e); - }, - #[cfg(not(feature = "log"))] - Some(Err(_)) => {}, - _ => {}, - } - if self.size >= size { - let now = - Epoch::now().unwrap_or_else(|e| panic!("system time determination failure: {}", e)); - self.active = true; - self.deploy_t = now; - #[cfg(feature = "log")] - info!("bin2rnx now deployed: production is pending"); - } - } - - /// Obtain reference to collected Observation RINEX - pub fn obs_rinex(&self) -> &Rinex { - &self.obs_rinex - } - - /// Obtain reference to collected Navigation RINEX - pub fn nav_rinex(&self) -> &Rinex { - &self.nav_rinex - } -} diff --git a/src/binex/mod.rs b/src/binex/mod.rs index 3e51e8b0..89baaacf 100644 --- a/src/binex/mod.rs +++ b/src/binex/mod.rs @@ -1,5 +1,192 @@ -mod bin2rnx; -pub use bin2rnx::BIN2RNX; +//! RINEX to BINEX serialization +use crate::prelude::{Epoch, Header, Rinex}; +use binex::prelude::{Message, Meta, MonumentGeoMetadata, MonumentGeoRecord}; -mod rnx2bin; -pub use rnx2bin::RNX2BIN; +mod nav; +use nav::Streamer as NavStreamer; + +/// RINEX Type dependant record streamer +enum TypeDependentStreamer<'a> { + /// NAV Record streamer + Nav(NavStreamer<'a>), +} + +impl<'a> TypeDependentStreamer<'a> { + pub fn new(meta: Meta, rinex: &'a Rinex) -> Self { + Self::Nav(NavStreamer::new(meta, rinex)) + } +} + +impl<'a> Iterator for TypeDependentStreamer<'a> { + type Item = Message; + fn next(&mut self) -> Option { + match self { + Self::Nav(streamer) => streamer.next(), + } + } +} + +/// RNX2BIN can serialize a [Rinex] into a stream of BINEX [Message]s +pub struct RNX2BIN<'a> { + /// First [Epoch] or [Epoch] of publication + t0: Epoch, + + /// BINEX [Message] encoding [Meta] + meta: Meta, + + /// Header consumption State machine + state: State, + + /// RINEX [Header] snapshot + header: &'a Header, + + /// RINEX [TypeDependentStreamer] + streamer: TypeDependentStreamer<'a>, + + /// Assert (before deployment) whether the Header should not be serialized (no default!) + pub skip_header: bool, + + /// Define (before deployment) a custom message to be included in the announcement. + pub custom_announce: Option, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq)] +enum State { + /// Describes the RINEX format, Constellation and revision + #[default] + HeaderPkgVersion, + MonumentGeo, + AnnounceHeaderComments, + HeaderComments, + AnnounceRecord, + RecordStream, +} + +impl<'a> Iterator for RNX2BIN<'a> { + type Item = Message; + fn next(&mut self) -> Option { + let content = match self.state { + State::HeaderPkgVersion => { + let mut geo = self.forge_monument_geo(); + if let Some(custom) = &self.custom_announce { + geo.comments.push(custom.clone()); + } + // announce stream beginning + geo.comments.push("Stream starting!".to_string()); + self.state = State::MonumentGeo; + Some(geo) + }, + State::MonumentGeo => { + let mut geo = self.forge_monument_geo(); + if let Some(agency) = &self.header.agency { + geo = geo.with_agency(agency); + } + if let Some(observer) = &self.header.observer { + geo = geo.with_observer(observer); + } + if let Some(_marker) = &self.header.geodetic_marker { + //geo = geo.with_marker_name(); + //geo = geo.with_marker_number(); + } + if let Some(rx) = &self.header.rcvr { + geo = geo.with_receiver_model(&rx.model); + geo = geo.with_receiver_serial_number(&rx.sn); + geo = geo.with_receiver_firmware_version(&rx.firmware); + } + if let Some(_cospar) = &self.header.cospar { + //geo = geo.with_reference_number(cospar); + } + if let Some(_position) = &self.header.rx_position { + //geo = geo.with_site_location(); + } + if !self.header.comments.is_empty() && !self.skip_header { + self.state = State::AnnounceHeaderComments; + } else { + self.state = State::AnnounceRecord; + } + Some(geo) + }, + State::AnnounceHeaderComments => { + let mut geo = self.forge_monument_geo(); + geo = geo.with_comment("RINEX Header comments following!"); + self.state = State::HeaderComments; + Some(geo) + }, + State::HeaderComments => { + let mut geo = self.forge_monument_geo(); + for comment in self.header.comments.iter() { + geo = geo.with_comment(comment); + } + self.state = State::AnnounceRecord; + Some(geo) + }, + State::AnnounceRecord => { + let mut geo = self.forge_monument_geo(); + geo = geo.with_comment("RINEX Record starting!"); + self.state = State::RecordStream; + Some(geo) + }, + State::RecordStream => { + let msg = self.streamer.next()?; + return Some(msg); + }, + }; + + if let Some(content) = content { + // forge new message + Some(Message { + meta: self.meta, + record: content.into(), + }) + } else { + None + } + } +} + +impl<'a> RNX2BIN<'a> { + fn forge_monument_geo(&self) -> MonumentGeoRecord { + let mut geo = MonumentGeoRecord::default(); + geo.epoch = self.t0; + geo.meta = MonumentGeoMetadata::RNX2BIN; + geo = geo.with_software_name(&format!( + "nav-solutions/rinex v{}", + env!("CARGO_PKG_VERSION") + )); + geo + } +} + +impl Rinex { + /// Create a [RNX2BIN] streamer to convert this [Rinex] + /// into a stream of BINEX [Message]s. You can then use the Iterator implementation + /// to forge the stream. + /// The stream will be made of + /// - One geo monument message describing this software package + /// - One geo monument message announcing the Header fields + /// - One geo monument message describing all [Header] fields + /// - One geo monument message wrapping all comments contained in [Header] + /// - One geo monument message announcing the start of Record stream + /// - One RINEX format depending by record entry. For example, + /// one Ephemeris frame per decoded Navigation message. + /// + /// ## Inputs: + /// - meta: BINEX encoding [Meta] + /// ## Output + /// - [RNX2BIN]: a BINEX [Message] Iterator + /// + /// This is work in progress. Currently, we support + /// the streaming of Navigation Ephemeris. + pub fn rnx2bin<'a>(&'a self, meta: Meta) -> Option> { + let t0 = self.first_epoch()?; + Some(RNX2BIN { + t0, + meta, + header: &self.header, + state: State::default(), + skip_header: false, + custom_announce: Default::default(), + streamer: TypeDependentStreamer::new(meta, self), + }) + } +} diff --git a/src/binex/rnx2bin/nav.rs b/src/binex/nav.rs similarity index 100% rename from src/binex/rnx2bin/nav.rs rename to src/binex/nav.rs diff --git a/src/binex/rnx2bin/mod.rs b/src/binex/rnx2bin/mod.rs deleted file mode 100644 index 89baaacf..00000000 --- a/src/binex/rnx2bin/mod.rs +++ /dev/null @@ -1,192 +0,0 @@ -//! RINEX to BINEX serialization -use crate::prelude::{Epoch, Header, Rinex}; -use binex::prelude::{Message, Meta, MonumentGeoMetadata, MonumentGeoRecord}; - -mod nav; -use nav::Streamer as NavStreamer; - -/// RINEX Type dependant record streamer -enum TypeDependentStreamer<'a> { - /// NAV Record streamer - Nav(NavStreamer<'a>), -} - -impl<'a> TypeDependentStreamer<'a> { - pub fn new(meta: Meta, rinex: &'a Rinex) -> Self { - Self::Nav(NavStreamer::new(meta, rinex)) - } -} - -impl<'a> Iterator for TypeDependentStreamer<'a> { - type Item = Message; - fn next(&mut self) -> Option { - match self { - Self::Nav(streamer) => streamer.next(), - } - } -} - -/// RNX2BIN can serialize a [Rinex] into a stream of BINEX [Message]s -pub struct RNX2BIN<'a> { - /// First [Epoch] or [Epoch] of publication - t0: Epoch, - - /// BINEX [Message] encoding [Meta] - meta: Meta, - - /// Header consumption State machine - state: State, - - /// RINEX [Header] snapshot - header: &'a Header, - - /// RINEX [TypeDependentStreamer] - streamer: TypeDependentStreamer<'a>, - - /// Assert (before deployment) whether the Header should not be serialized (no default!) - pub skip_header: bool, - - /// Define (before deployment) a custom message to be included in the announcement. - pub custom_announce: Option, -} - -#[derive(Debug, Default, Clone, Copy, PartialEq)] -enum State { - /// Describes the RINEX format, Constellation and revision - #[default] - HeaderPkgVersion, - MonumentGeo, - AnnounceHeaderComments, - HeaderComments, - AnnounceRecord, - RecordStream, -} - -impl<'a> Iterator for RNX2BIN<'a> { - type Item = Message; - fn next(&mut self) -> Option { - let content = match self.state { - State::HeaderPkgVersion => { - let mut geo = self.forge_monument_geo(); - if let Some(custom) = &self.custom_announce { - geo.comments.push(custom.clone()); - } - // announce stream beginning - geo.comments.push("Stream starting!".to_string()); - self.state = State::MonumentGeo; - Some(geo) - }, - State::MonumentGeo => { - let mut geo = self.forge_monument_geo(); - if let Some(agency) = &self.header.agency { - geo = geo.with_agency(agency); - } - if let Some(observer) = &self.header.observer { - geo = geo.with_observer(observer); - } - if let Some(_marker) = &self.header.geodetic_marker { - //geo = geo.with_marker_name(); - //geo = geo.with_marker_number(); - } - if let Some(rx) = &self.header.rcvr { - geo = geo.with_receiver_model(&rx.model); - geo = geo.with_receiver_serial_number(&rx.sn); - geo = geo.with_receiver_firmware_version(&rx.firmware); - } - if let Some(_cospar) = &self.header.cospar { - //geo = geo.with_reference_number(cospar); - } - if let Some(_position) = &self.header.rx_position { - //geo = geo.with_site_location(); - } - if !self.header.comments.is_empty() && !self.skip_header { - self.state = State::AnnounceHeaderComments; - } else { - self.state = State::AnnounceRecord; - } - Some(geo) - }, - State::AnnounceHeaderComments => { - let mut geo = self.forge_monument_geo(); - geo = geo.with_comment("RINEX Header comments following!"); - self.state = State::HeaderComments; - Some(geo) - }, - State::HeaderComments => { - let mut geo = self.forge_monument_geo(); - for comment in self.header.comments.iter() { - geo = geo.with_comment(comment); - } - self.state = State::AnnounceRecord; - Some(geo) - }, - State::AnnounceRecord => { - let mut geo = self.forge_monument_geo(); - geo = geo.with_comment("RINEX Record starting!"); - self.state = State::RecordStream; - Some(geo) - }, - State::RecordStream => { - let msg = self.streamer.next()?; - return Some(msg); - }, - }; - - if let Some(content) = content { - // forge new message - Some(Message { - meta: self.meta, - record: content.into(), - }) - } else { - None - } - } -} - -impl<'a> RNX2BIN<'a> { - fn forge_monument_geo(&self) -> MonumentGeoRecord { - let mut geo = MonumentGeoRecord::default(); - geo.epoch = self.t0; - geo.meta = MonumentGeoMetadata::RNX2BIN; - geo = geo.with_software_name(&format!( - "nav-solutions/rinex v{}", - env!("CARGO_PKG_VERSION") - )); - geo - } -} - -impl Rinex { - /// Create a [RNX2BIN] streamer to convert this [Rinex] - /// into a stream of BINEX [Message]s. You can then use the Iterator implementation - /// to forge the stream. - /// The stream will be made of - /// - One geo monument message describing this software package - /// - One geo monument message announcing the Header fields - /// - One geo monument message describing all [Header] fields - /// - One geo monument message wrapping all comments contained in [Header] - /// - One geo monument message announcing the start of Record stream - /// - One RINEX format depending by record entry. For example, - /// one Ephemeris frame per decoded Navigation message. - /// - /// ## Inputs: - /// - meta: BINEX encoding [Meta] - /// ## Output - /// - [RNX2BIN]: a BINEX [Message] Iterator - /// - /// This is work in progress. Currently, we support - /// the streaming of Navigation Ephemeris. - pub fn rnx2bin<'a>(&'a self, meta: Meta) -> Option> { - let t0 = self.first_epoch()?; - Some(RNX2BIN { - t0, - meta, - header: &self.header, - state: State::default(), - skip_header: false, - custom_announce: Default::default(), - streamer: TypeDependentStreamer::new(meta, self), - }) - } -} diff --git a/src/lib.rs b/src/lib.rs index 53a110f2..9681ad6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -153,7 +153,7 @@ pub mod prelude { #[cfg(feature = "binex")] #[cfg_attr(docsrs, doc(cfg(feature = "binex")))] pub mod binex { - pub use crate::binex::{BIN2RNX, RNX2BIN}; + pub use crate::binex::RNX2BIN; pub use binex::prelude::{Message, Meta}; } @@ -194,10 +194,6 @@ pub mod prelude { }; } - #[cfg(feature = "binex")] - #[cfg_attr(docsrs, doc(cfg(feature = "binex")))] - pub use crate::binex::BIN2RNX; - #[cfg(feature = "rtcm")] #[cfg_attr(docsrs, doc(cfg(feature = "rtcm")))] pub use crate::rtcm::RNX2RTCM; From 73eaf2c9fbfd45c05c71dcb9b18a6da0f44549c4 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 13 Sep 2025 13:25:54 +0200 Subject: [PATCH 12/14] improve binex implementation --- src/navigation/ephemeris/binex.rs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/navigation/ephemeris/binex.rs b/src/navigation/ephemeris/binex.rs index a3862a59..e5285da6 100644 --- a/src/navigation/ephemeris/binex.rs +++ b/src/navigation/ephemeris/binex.rs @@ -66,6 +66,9 @@ impl Ephemeris { /// - [EphemerisFrame]: all required fields must exist /// so we can forge a frame. pub fn to_binex(&self, toc: Epoch, sv: SV) -> Option { + let tow = toc.to_time_of_week().1 / 1_000_000_000; + let toc = toc.to_time_of_week().1 / 1_000_000_000; + match sv.constellation { Constellation::GPS | Constellation::QZSS => { let clock_offset = self.clock_bias as f32; @@ -103,8 +106,8 @@ impl Ephemeris { iode, iodc, toe, - tow: 0, // TODO - toc: 0, // TODO + tow: tow as i32, + toc: toc as i32, tgd, clock_offset, clock_drift, @@ -173,8 +176,6 @@ impl Ephemeris { })) }, Constellation::Galileo => { - let _sv_prn = sv.prn; - let clock_offset = self.clock_bias as f32; let clock_drift = self.clock_drift as f32; let clock_drift_rate = self.clock_drift_rate as f32; @@ -202,16 +203,21 @@ impl Ephemeris { let delta_n_rad_s = self.orbits.get("delta_n")?.as_f64() as f32; let delta_n_semi_circles_s = delta_n_rad_s; - let sv_health = self.orbits.get("health")?.as_f64() as u16; + let sv_health = self.get_orbit_f64("health")? as u16; + let sisa = self.get_orbit_f64("sisa").unwrap_or_default() as f32; // TODO SISA issue? + let iodnav = self.get_orbit_f64("iodnav").unwrap_or_default() as i32; // TODO IODNAV issue? + + let (toe_week, toe_nanos) = self.toe(sv)?.to_time_of_week(); + let toe_s = (toe_nanos / 1_000_000_000) as i32; Some(EphemerisFrame::GAL(GALEphemeris { sv_prn: sv.prn, - toe_week: 0, // TODO - tow: 0, // TODO - toe_s: 0, // TODO + tow: tow as i32, + toe_week: toe_week as u16, + toe_s, bgd_e5a_e1_s: 0.0, // TODO bgd_e5b_e1_s: 0.0, // TODO - iodnav: 0, // TODO + iodnav, clock_drift_rate, clock_drift, clock_offset, @@ -230,7 +236,7 @@ impl Ephemeris { i0_rad, omega_dot_semi_circles, idot_semi_circles_s, - sisa: 0.0, // TODO + sisa, sv_health, source: 0, // TODO })) From 99c0c516df58db01941f8e14aac0fd66e3f8b0a4 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 13 Sep 2025 14:01:05 +0200 Subject: [PATCH 13/14] binex testing Signed-off-by: Guillaume W. Bres --- src/navigation/ephemeris/binex.rs | 107 ++++++++++++++------------- src/tests/binex.rs | 117 +++++++++++++++++++++++------- 2 files changed, 147 insertions(+), 77 deletions(-) diff --git a/src/navigation/ephemeris/binex.rs b/src/navigation/ephemeris/binex.rs index e5285da6..c844a287 100644 --- a/src/navigation/ephemeris/binex.rs +++ b/src/navigation/ephemeris/binex.rs @@ -8,8 +8,9 @@ use std::collections::HashMap; use binex::prelude::{EphemerisFrame, GALEphemeris, GLOEphemeris, GPSEphemeris, SBASEphemeris}; impl Ephemeris { - /// Converts this BINEX [EphemerisFrame] to [Ephemeris], ready to format. - /// We support GPS, QZSS, Galileo, Glonass and SBAS frames. + /// Converts this BINEX [EphemerisFrame] to [Ephemeris], ready to format. + /// We support [Constellation::GPS], + /// [Constellation::SBAS], [Constellation::Galileo] and [Constellation::Glonass]. /// /// ## Inputs /// - now: usually the [Epoch] of message reception @@ -18,17 +19,17 @@ impl Ephemeris { EphemerisFrame::GPS(serialized) => Some(( SV::new(Constellation::GPS, serialized.sv_prn), Self { - clock_bias: 0.0, - clock_drift: 0.0, - clock_drift_rate: 0.0, + clock_bias: serialized.clock_offset as f64, + clock_drift: serialized.clock_drift as f64, + clock_drift_rate: serialized.clock_drift_rate as f64, orbits: HashMap::from_iter([("week".to_string(), OrbitItem::from(0.0f64))]), }, )), EphemerisFrame::SBAS(serialized) => Some(( - SV::new(Constellation::SBAS, serialized.sbas_prn), + SV::new(Constellation::SBAS, serialized.sbas_prn - 100), Self { - clock_bias: 0.0, - clock_drift: 0.0, + clock_bias: serialized.clock_offset as f64, + clock_drift: serialized.clock_drift as f64, clock_drift_rate: 0.0, orbits: HashMap::from_iter([("week".to_string(), OrbitItem::from(0.0f64))]), }, @@ -36,8 +37,8 @@ impl Ephemeris { EphemerisFrame::GLO(serialized) => Some(( SV::new(Constellation::Glonass, serialized.slot), Self { - clock_bias: 0.0, - clock_drift: 0.0, + clock_bias: serialized.clock_offset_s as f64, + clock_drift: serialized.clock_rel_freq_bias as f64, clock_drift_rate: 0.0, orbits: HashMap::from_iter([("week".to_string(), OrbitItem::from(0.0f64))]), }, @@ -45,9 +46,9 @@ impl Ephemeris { EphemerisFrame::GAL(serialized) => Some(( SV::new(Constellation::Galileo, serialized.sv_prn), Self { - clock_bias: 0.0, - clock_drift: 0.0, - clock_drift_rate: 0.0, + clock_bias: serialized.clock_offset as f64, + clock_drift: serialized.clock_drift as f64, + clock_drift_rate: serialized.clock_drift_rate as f64, orbits: HashMap::from_iter([("week".to_string(), OrbitItem::from(0.0f64))]), }, )), @@ -55,8 +56,14 @@ impl Ephemeris { } } - /// Encodes this [Ephemeris] to BINEX [EphemerisFrame], ready to encode. - /// We currently support GPS, QZSS, SBAS, Galileo and Glonass. + /// Encodes this [Ephemeris] to BINEX [EphemerisFrame], ready to encode. + /// We support [Constellation::GPS], + /// [Constellation::SBAS], [Constellation::Galileo] and [Constellation::Glonass]. + /// + /// NB:: we tolerate null acceleration value + /// for both [Constellation::Glonass] and [Constellation::SBAS] vehicles + /// which may impact your final accuracy. You should post-correct that otherwise, + /// or potentially drop null values in this case. /// /// ## Inputs /// - toc: time of clock as [Epoch] @@ -70,7 +77,7 @@ impl Ephemeris { let toc = toc.to_time_of_week().1 / 1_000_000_000; match sv.constellation { - Constellation::GPS | Constellation::QZSS => { + Constellation::GPS => { let clock_offset = self.clock_bias as f32; let clock_drift = self.clock_drift as f32; let clock_drift_rate = self.clock_drift_rate as f32; @@ -92,10 +99,10 @@ impl Ephemeris { let sqrt_a = self.orbits.get("sqrta")?.as_f64(); let omega_rad = self.orbits.get("omega")?.as_f64(); let omega_0_rad = self.orbits.get("omega0")?.as_f64(); - let omega_dot_rad_s = self.orbits.get("oemgaDot")?.as_f64() as f32; + let omega_dot_rad_s = self.orbits.get("omegaDot")?.as_f64() as f32; let i_dot_rad_s = self.orbits.get("idot")?.as_f64() as f32; - let delta_n_rad_s = self.orbits.get("delta_n")?.as_f64() as f32; + let delta_n_rad_s = self.orbits.get("deltaN")?.as_f64() as f32; let tgd = self.orbits.get("tgd")?.as_f64() as f32; let iode = self.orbits.get("iode")?.as_u32() as i32; @@ -139,17 +146,17 @@ impl Ephemeris { // let slot = self.orbits.get("channel")?.as_u8(); let sv_health = self.orbits.get("health")?.as_u8(); - let x_km = self.orbits.get("satPosX")?.as_f64(); - let vel_x_km = self.orbits.get("velX")?.as_f64(); - let acc_x_km = self.orbits.get("accelX")?.as_f64(); + let x_km = self.get_orbit_f64("satPosX")?; + let y_km = self.get_orbit_f64("satPosY")?; + let z_km = self.get_orbit_f64("satPosZ")?; - let y_km = self.orbits.get("satPosX")?.as_f64(); - let vel_y_km = self.orbits.get("velY")?.as_f64(); - let acc_y_km = self.orbits.get("accelY")?.as_f64(); + let vel_x_km = self.get_orbit_f64("velX")?; + let vel_y_km = self.get_orbit_f64("velY")?; + let vel_z_km = self.get_orbit_f64("velZ")?; - let z_km = self.orbits.get("satPosX")?.as_f64(); - let vel_z_km = self.orbits.get("velZ")?.as_f64(); - let acc_z_km = self.orbits.get("accelZ")?.as_f64(); + let acc_x_km = self.get_orbit_f64("accelX").unwrap_or_default(); + let acc_y_km = self.get_orbit_f64("accelY").unwrap_or_default(); + let acc_z_km = self.get_orbit_f64("accelZ").unwrap_or_default(); Some(EphemerisFrame::GLO(GLOEphemeris { slot: 0, // TODO @@ -180,12 +187,12 @@ impl Ephemeris { let clock_drift = self.clock_drift as f32; let clock_drift_rate = self.clock_drift_rate as f32; - let cic = self.orbits.get("cic")?.as_f64() as f32; - let crc = self.orbits.get("crc")?.as_f64() as f32; - let cis = self.orbits.get("cis")?.as_f64() as f32; - let crs = self.orbits.get("crs")?.as_f64() as f32; - let cuc = self.orbits.get("cuc")?.as_f64() as f32; - let cus = self.orbits.get("cus")?.as_f64() as f32; + let cic = self.get_orbit_f64("cic")? as f32; + let crc = self.get_orbit_f64("crc")? as f32; + let cis = self.get_orbit_f64("cis")? as f32; + let crs = self.get_orbit_f64("crs")? as f32; + let cuc = self.get_orbit_f64("cuc")? as f32; + let cus = self.get_orbit_f64("cus")? as f32; let e = self.orbits.get("e")?.as_f64(); let m0_rad = self.orbits.get("m0")?.as_f64(); @@ -194,14 +201,14 @@ impl Ephemeris { let omega_rad = self.orbits.get("omega")?.as_f64(); let omega_0_rad = self.orbits.get("omega0")?.as_f64(); - let omega_dot_rad_s = self.orbits.get("oemgaDot")?.as_f64() as f32; - let omega_dot_semi_circles = omega_dot_rad_s; + let omega_dot_rad_s = self.orbits.get("omegaDot")?.as_f64() as f32; + let omega_dot_semi_circles = omega_dot_rad_s; // TODO double check (=binex testbench) let i_dot_rad_s = self.orbits.get("idot")?.as_f64() as f32; - let idot_semi_circles_s = i_dot_rad_s; + let idot_semi_circles_s = i_dot_rad_s; // TODO double check (=binex testbench) - let delta_n_rad_s = self.orbits.get("delta_n")?.as_f64() as f32; - let delta_n_semi_circles_s = delta_n_rad_s; + let delta_n_rad_s = self.orbits.get("deltaN")?.as_f64() as f32; + let delta_n_semi_circles_s = delta_n_rad_s; // TODO double check (=binex testbench) let sv_health = self.get_orbit_f64("health")? as u16; let sisa = self.get_orbit_f64("sisa").unwrap_or_default() as f32; // TODO SISA issue? @@ -246,22 +253,20 @@ impl Ephemeris { let clock_offset = self.clock_bias; let clock_drift = self.clock_drift; - let x_km = self.orbits.get("satPosX")?.as_f64(); - let vel_x_km = self.orbits.get("velX")?.as_f64(); - let acc_x_km = self.orbits.get("accelX")?.as_f64(); - - let y_km = self.orbits.get("satPosX")?.as_f64(); - let vel_y_km = self.orbits.get("velY")?.as_f64(); - let acc_y_km = self.orbits.get("accelY")?.as_f64(); + let x_km = self.get_orbit_f64("satPosX")?; + let y_km = self.get_orbit_f64("satPosY")?; + let z_km = self.get_orbit_f64("satPosZ")?; - let z_km = self.orbits.get("satPosX")?.as_f64(); - let vel_z_km = self.orbits.get("velZ")?.as_f64(); - let acc_z_km = self.orbits.get("accelZ")?.as_f64(); + let vel_x_km = self.get_orbit_f64("velX")?; + let vel_y_km = self.get_orbit_f64("velY")?; + let vel_z_km = self.get_orbit_f64("velZ")?; - let iodn = self.orbits.get("iodn")?.as_u8(); + let acc_x_km = self.get_orbit_f64("accelX").unwrap_or_default(); + let acc_y_km = self.get_orbit_f64("accelY").unwrap_or_default(); + let acc_z_km = self.get_orbit_f64("accelZ").unwrap_or_default(); Some(EphemerisFrame::SBAS(SBASEphemeris { - sbas_prn: sv.prn, + sbas_prn: sv.prn + 100, toe: 0, tow: 0, clock_offset, @@ -277,7 +282,7 @@ impl Ephemeris { acc_z_km, uint1: 0, // TODO ura: 0, // TODO - iodn, + iodn: 0, // TODO })) } else { None diff --git a/src/tests/binex.rs b/src/tests/binex.rs index fea8e6aa..48229627 100644 --- a/src/tests/binex.rs +++ b/src/tests/binex.rs @@ -1,42 +1,107 @@ use crate::navigation::Ephemeris; -use crate::prelude::Rinex; +use crate::prelude::{Constellation, Rinex}; use binex::prelude::Meta; #[test] -#[ignore] -fn nav_v3_to_binex() { +fn esbcdnk_ephv3_binex() { + let mut gps_passed = 0; + let mut gal_passed = 0; + let mut glo_passed = 0; + // TODO let mut bds_passed = 0; + // TODO let mut qzss_passed = 0; + let mut sbas_passed = 0; + let rinex = Rinex::from_gzip_file("data/NAV/V3/ESBC00DNK_R_20201770000_01D_MN.rnx.gz").unwrap(); - let meta = Meta::default(); + for (k, ephemeris) in rinex.nav_ephemeris_frames_iter() { + match k.sv.constellation { + Constellation::GPS | Constellation::Galileo => { + if let Some(serialized) = ephemeris.to_binex(k.epoch, k.sv) { + // mirror + let (decoded_sv, decoded) = Ephemeris::from_binex(k.epoch, serialized) + .unwrap_or_else(|| { + panic!("Failed to decoded {}({}) BINEX frame", k.epoch, k.sv); + }); - let mut streamer = rinex.rnx2bin(meta); + // testbench + assert_eq!(k.sv, decoded_sv, "{}({}) invalid SV", k.epoch, k.sv); - for message in streamer.iter() {} + // TODO achieve full reciprocity + // assert_eq!( + // *ephemeris, decoded, + // "{}({}) invalid content decoded", + // k.epoch, k.sv + // ); + + match k.sv.constellation { + Constellation::GPS => gps_passed += 1, + Constellation::Galileo => gal_passed += 1, + Constellation::Glonass => glo_passed += 1, + Constellation::BeiDou => { + // TODO + }, + Constellation::Glonass => { + // TODO: issue with sv.PRN + }, + Constellation::QZSS => { + // TODO + }, + _ => {}, + } + } + }, + constellation => { + if constellation.is_sbas() { + if let Some(serialized) = ephemeris.to_binex(k.epoch, k.sv) { + // mirror + let (decoded_sv, decoded) = Ephemeris::from_binex(k.epoch, serialized) + .unwrap_or_else(|| { + panic!("Failed to decoded {}({}) BINEX frame", k.epoch, k.sv); + }); + + // testbench + assert!(decoded_sv.constellation.is_sbas()); + // TODO error in the SV::new API unable to identify correctly + // assert_eq!(decoded_sv.prn, k.sv.prn); + // assert_eq!(decoded_sv.constellation, k.sv.constellation); + + // TODO invalid PRN + // assert_eq!(k.sv, decoded_sv, "{}({}) invalid SV", k.epoch, k.sv); + + // TODO achieve full reciprocity + // assert_eq!( + // *ephemeris, decoded, + // "{}({}) invalid content decoded", + // k.epoch, k.sv + // ); + sbas_passed += 1; + } + } + }, + } + } + + assert!(gps_passed > 0); + assert!(gal_passed > 0); + assert!(sbas_passed > 0); + // TODO assert!(glo_passed > 0); + // TODO assert!(bds_passed > 0); + // TODO assert!(qzss_passed > 0); + + assert_eq!(gps_passed, 253); + assert_eq!(gal_passed, 806); + assert_eq!(sbas_passed, 320); } #[test] #[ignore] -fn nav_v3_ephemeris() { +fn nav_v3_to_binex() { let rinex = Rinex::from_gzip_file("data/NAV/V3/ESBC00DNK_R_20201770000_01D_MN.rnx.gz").unwrap(); - for (k, ephemeris) in rinex.nav_ephemeris_frames_iter() { - let serialized = ephemeris.to_binex(k.epoch, k.sv).unwrap_or_else(|| { - panic!("Failed to serialize {}({})", k.epoch, k.sv); - }); - - // mirror - let (decoded_sv, decoded) = - Ephemeris::from_binex(k.epoch, serialized).unwrap_or_else(|| { - panic!("Failed to decoded {}({}) BINEX frame", k.epoch, k.sv); - }); - - // testbench - assert_eq!(k.sv, decoded_sv, "{}({}) invalid SV", k.epoch, k.sv); - assert_eq!( - *ephemeris, decoded, - "{}({}) invalid content decoded", - k.epoch, k.sv - ); - } + let meta = Meta::default(); + + let mut streamer = rinex.rnx2bin(meta); + + for message in streamer.iter() {} } From 12d0aa5300bf9998b6bae13387aa39b811c6db84 Mon Sep 17 00:00:00 2001 From: "Guillaume W. Bres" Date: Sat, 13 Sep 2025 14:01:51 +0200 Subject: [PATCH 14/14] todo: docs Signed-off-by: Guillaume W. Bres --- src/rtcm/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/rtcm/mod.rs b/src/rtcm/mod.rs index 5a252dd6..c200d314 100644 --- a/src/rtcm/mod.rs +++ b/src/rtcm/mod.rs @@ -48,12 +48,14 @@ impl Rinex { /// loop { /// match streamer.next() { /// Some(message) => { + /// // TODO /// }, /// None => { /// // end of stream /// // RINEX file has been consumed entirely /// break; /// }, + /// } /// } pub fn rnx2rtcm<'a>(rinex: &'a Rinex) -> Option> { let type_dependent = match rinex.header.rinex_type {