diff --git a/Cargo.toml b/Cargo.toml index 6fa3e074d..aabcd90df 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/README.md b/README.md index c43c480f6..0564ad6d2 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: diff --git a/src/binex/bin2rnx.rs b/src/binex/bin2rnx.rs deleted file mode 100644 index 17faaa2b7..000000000 --- a/src/binex/bin2rnx.rs +++ /dev/null @@ -1,281 +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 3e51e8b08..89baaacf6 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/nav.rs b/src/binex/nav.rs new file mode 100644 index 000000000..c30f040ff --- /dev/null +++ b/src/binex/nav.rs @@ -0,0 +1,34 @@ +use crate::{ + navigation::{Ephemeris, NavKey}, + prelude::Rinex, +}; + +use binex::prelude::{Message, Meta, Record}; + +/// NAV Record Streamer +pub struct Streamer<'a> { + meta: Meta, + ephemeris_iter: Box + 'a>, +} + +impl<'a> Streamer<'a> { + pub fn new(meta: Meta, rinex: &'a Rinex) -> Self { + Self { + meta: meta, + ephemeris_iter: rinex.nav_ephemeris_frames_iter(), + } + } +} + +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)?; + + Some(Message { + meta: self.meta, + record: Record::new_ephemeris_frame(frame), + }) + } +} diff --git a/src/binex/rnx2bin/mod.rs b/src/binex/rnx2bin/mod.rs deleted file mode 100644 index acf460a1b..000000000 --- a/src/binex/rnx2bin/mod.rs +++ /dev/null @@ -1,186 +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/binex/rnx2bin/nav.rs b/src/binex/rnx2bin/nav.rs deleted file mode 100644 index 8ee035d25..000000000 --- a/src/binex/rnx2bin/nav.rs +++ /dev/null @@ -1,251 +0,0 @@ -use crate::{ - navigation::{Ephemeris, NavKey}, - prelude::{Constellation, Epoch, Rinex, SV}, -}; - -use binex::prelude::{ - EphemerisFrame, GALEphemeris, GLOEphemeris, GPSEphemeris, Message, Meta, Record, SBASEphemeris, -}; - -/// NAV Record Streamer -pub struct Streamer<'a> { - meta: Meta, - 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 { - meta: meta, - ephemeris_iter: rinex.nav_ephemeris_frames_iter(), - } - } -} - -impl<'a> Iterator for Streamer<'a> { - type Item = Message; - fn next(&mut self) -> Option { - let (key, eph) = self.ephemeris_iter.next()?; - - 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/lib.rs b/src/lib.rs index 84c26e1f2..9681ad6c5 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,13 +194,9 @@ 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::RTCM2RNX; + pub use crate::rtcm::RNX2RTCM; #[cfg(feature = "ublox")] #[cfg_attr(docsrs, doc(cfg(feature = "ublox")))] diff --git a/src/navigation/ephemeris/binex.rs b/src/navigation/ephemeris/binex.rs new file mode 100644 index 000000000..c844a2879 --- /dev/null +++ b/src/navigation/ephemeris/binex.rs @@ -0,0 +1,293 @@ +use crate::{ + 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 [Constellation::GPS], + /// [Constellation::SBAS], [Constellation::Galileo] and [Constellation::Glonass]. + /// + /// ## 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: 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 - 100), + Self { + 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))]), + }, + )), + EphemerisFrame::GLO(serialized) => Some(( + SV::new(Constellation::Glonass, serialized.slot), + Self { + 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))]), + }, + )), + EphemerisFrame::GAL(serialized) => Some(( + SV::new(Constellation::Galileo, serialized.sv_prn), + Self { + 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))]), + }, + )), + _ => None, + } + } + + /// 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] + /// - 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 { + 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 => { + 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("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("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; + let iodc = self.orbits.get("iodc")?.as_u32() as i32; + + Some(EphemerisFrame::GPS(GPSEphemeris { + sv_prn: sv.prn, + iode, + iodc, + toe, + tow: tow as i32, + toc: toc as i32, + 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.get_orbit_f64("satPosX")?; + let y_km = self.get_orbit_f64("satPosY")?; + let z_km = self.get_orbit_f64("satPosZ")?; + + 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 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 + 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 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.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(); + 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("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; // TODO double check (=binex testbench) + + 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? + 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, + 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, + 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, + 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.get_orbit_f64("satPosX")?; + let y_km = self.get_orbit_f64("satPosY")?; + let z_km = self.get_orbit_f64("satPosZ")?; + + 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 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 + 100, + 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: 0, // TODO + })) + } else { + None + } + }, + } + } +} diff --git a/src/navigation/ephemeris/mod.rs b/src/navigation/ephemeris/mod.rs index a1091aa67..c9f822b68 100644 --- a/src/navigation/ephemeris/mod.rs +++ b/src/navigation/ephemeris/mod.rs @@ -20,12 +20,21 @@ 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; #[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, @@ -403,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 new file mode 100644 index 000000000..d4a1137be --- /dev/null +++ b/src/navigation/ephemeris/rtcm.rs @@ -0,0 +1,525 @@ +use std::collections::HashMap; + +use hifitime::prelude::{Duration, Unit}; + +use crate::{ + navigation::{Ephemeris, OrbitItem}, + prelude::{Constellation, Epoch, SV}, +}; + +use rtcm_rs::msg::{ + Msg1019T, + Msg1020T, + Msg1042T, //Msg1043T, + Msg1044T, + Msg1045T, + Msg1046T, +}; + +#[cfg(doc)] +use crate::prelude::Rinex; + +impl Ephemeris { + /// Converts this [Ephemeris] to [Msg1019T] [Constellation::GPS] ephemeris message. + /// ## Input + /// - toc: Time of Clock as [Epoch] + /// - sv: attached satellite as [SV] which must a [Constellation::GPS] vehicle. + /// + /// ## Output + /// - [Msg1019T] GPS ephemeris message. + pub fn to_rtcm_gps1019(&self, toc: Epoch, sv: SV) -> Option { + if sv.constellation != Constellation::GPS { + return None; // invalid API usage + } + + let (toc_week, toc_week_nanos) = toc.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 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 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").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 + + 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 [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. + /// + /// ## Output + /// - [Msg1020T] Glonass ephemeris message. + pub fn to_rtcm_glo1020(&self, toc: Epoch, sv: SV) -> Option { + if sv.constellation != Constellation::Glonass { + return None; // invalid API usage + } + + let toc_nanos = Duration::from_nanoseconds(toc.to_time_of_week().1 as f64); + + 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 + 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 = 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 = 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 = 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 + + 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_h as u8, + tk_min: tk_min as u8, + tk_s: tk_s as u8, + 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, + }) + } + + /// 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_gal1045(&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 = 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 e5a_data_validity_flag = 0; // TODO + let e5a_sig_health_ind = 0; // TODO + let reserved_489_7 = 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_gal1046(&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_bds1042(&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 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, + 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_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 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").unwrap_or_default() as u8; // TODO fitInt issue + + 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 { + + // }) + // } +} diff --git a/src/rtcm/mod.rs b/src/rtcm/mod.rs index 1ad55b0e4..c200d314d 100644 --- a/src/rtcm/mod.rs +++ b/src/rtcm/mod.rs @@ -1,5 +1,70 @@ -//! 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) => { + /// // 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 { + 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 000000000..f806b0e42 --- /dev/null +++ b/src/rtcm/nav.rs @@ -0,0 +1,58 @@ +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 { + loop { + let (key, eph) = self.ephemeris_iter.next()?; + + match key.sv.constellation { + Constellation::GPS => { + let msg1019 = eph.to_rtcm_gps1019(key.epoch, key.sv)?; + return Some(Message::Msg1019(msg1019)); + }, + Constellation::QZSS => { + 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_gal1045(key.epoch, key.sv)?; + return Some(Message::Msg1045(msg1045)); + }, + Constellation::Glonass => { + let msg1020 = eph.to_rtcm_glo1020(key.epoch, key.sv)?; + return Some(Message::Msg1020(msg1020)); + }, + Constellation::BeiDou => { + let msg1042 = eph.to_rtcm_bds1042(key.epoch, key.sv)?; + return Some(Message::Msg1042(msg1042)); + }, + _ => { + // Not supported yet + }, + } + } + } +} diff --git a/src/rtcm/rnx2rtcm.rs b/src/rtcm/rnx2rtcm.rs deleted file mode 100644 index 4314d33a9..000000000 --- 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 297280710..000000000 --- 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/binex.rs b/src/tests/binex.rs new file mode 100644 index 000000000..48229627e --- /dev/null +++ b/src/tests/binex.rs @@ -0,0 +1,107 @@ +use crate::navigation::Ephemeris; +use crate::prelude::{Constellation, Rinex}; + +use binex::prelude::Meta; + +#[test] +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(); + + 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); + }); + + // testbench + 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 + // ); + + 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_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() {} +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index ba97f4e10..4fcb40be5 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; @@ -29,6 +32,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 000000000..6b2217732 --- /dev/null +++ b/src/tests/rtcm.rs @@ -0,0 +1,93 @@ +use crate::{ + navigation::Ephemeris, + prelude::{Constellation, Rinex}, +}; + +// NAV (V3) to RTCM +#[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 => { + 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); + assert!(bds1042 > 0); + assert!(qzss1044 > 0); + assert!(gal1045 > 0); + + assert_eq!(gps1019, 253); + assert_eq!(glo1020, 510); + assert_eq!(gal1045, 806); + assert_eq!(bds1042, 353); + assert_eq!(qzss1044, 15); +} + +// 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(); +} diff --git a/src/tests/ublox.rs b/src/tests/ublox.rs index adc9c65d8..fa0ce7a41 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 6b9136b59..f4fc1482e 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; @@ -23,8 +21,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: /// ``` diff --git a/src/ublox/nav.rs b/src/ublox/nav.rs index fd3ecb06b..7fa198675 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> {