diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 00000000..814bf31e --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,38 @@ +name: Benchmarks + +on: + push: + branches: + - main + tags: + - "*" + pull_request: + branches: + - main + +env: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + +jobs: + benchmark: + name: Benchmarking + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + include: + - name: BINEX Benchmark + folder: "binex" + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + - name: ${{ matrix.name }} + run: cargo bench -p ${{ matrix.folder }} > benchmark.txt + + - name: Parse and publish summary + run: python tools/parse_crit_benchmark.py < benchmark.txt >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index f33e1a0c..cfaba3ef 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -56,6 +56,9 @@ jobs: - name: RINEX All-features folder: rinex opts: --all-features + - name: BINEX + folder: binex + opts: --all-features - name: SP3 default folder: sp3 opts: -r diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d02405ea..971e7ebe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,7 @@ jobs: - crate: rinex - crate: sp3 - crate: sinex + - crate: binex - crate: rinex-qc - crate: ublox-rnx - crate: rnx2crx diff --git a/Cargo.toml b/Cargo.toml index e28fab03..c938b3d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ + "binex", "crx2rnx", "qc-traits", "rinex", diff --git a/README.md b/README.md index 1c66eed3..940a338a 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,10 @@ You can also open a [Discussion](https://github.com/georust/rinex/discussions) o ## Advantages :rocket: - Fast - - Parse and render reports in a few seconds - - Perform precise Geodetic surveys in a few seconds -- Open sources - - Read and access all the code - - All examples based on Open data +- Render High level Geodetic survey reports +- Resolve PPP solutions in a few seconds +- Open sources: read and access all the code! +- Self sustained examples and tutorials: data hosted within this repo - All modern GNSS constellations, codes and signals - Surveying with GPS, Galileo, BeiDou and QZSS - Time scales: GPST, QZSST, BDT, GST, UTC, TAI @@ -34,21 +33,18 @@ You can also open a [Discussion](https://github.com/georust/rinex/discussions) o - High Precision Clock RINEX products (for PPP) - High Precision Orbital [SP3 for PPP](https://docs.rs/sp3/1.0.7/sp3/) - DORIS (special RINEX) -- Several pre-processing algorithms: - - [File merging](https://github.com/georust/rinex/wiki/file-merging) - - [Time binning](https://github.com/georust/rinex/wiki/time-binning) - - [Filtering](https://github.com/georust/rinex/wiki/Preprocessing) -- Several post-processing operations - - [File Operations](https://github.com/georust/rinex/wiki/fops) +- Many pre-processing algorithms including Filter Designer +- Several file operations: merging, splitting, time binning (batch) +- Post processing: - [Position solver](https://github.com/georust/rinex/wiki/Positioning) - [CGGTTS solver](https://github.com/georust/rinex/wiki/CGGTTS) - - [Graphical QC](https://github.com/georust/rinex/wiki/Graph-Mode) ## Disadvantages :warning: +- BINEX support is currently work in progress - Navigation is currently not feasible with Glonass and IRNSS - Differential navigation (SBAS, DGNSS or RTK) is not support yet -- Our applications do not accept BINEX or other proprietary formats +- Our applications do not accept proprietary formats like Septentrio for example - File production might lack some features, mostly because we're currently focused on data processing ## Repository @@ -62,6 +58,7 @@ The application is auto-generated for a few architectures, you can directly * [`tutorials`](tutorials/) is a superset of scripts (Linux/MacOS compatible) to get started quickly. The examples span pretty much everything our applications allow. * [`sp3`](sp3/) High Precision Orbits (by IGS) +* [`binex`](binex/) BINEX Encoding and Decoding library * [`rnx2crx`](rnx2crx/) is a RINEX compressor (RINEX to Compact RINEX) * [`crx2rnx`](crx2rnx/) is a CRINEX decompresor (Compact RINEX to RINEX) * [`rinex-qc`](rinex-qc/) is a library dedicated to RINEX files analysis diff --git a/binex/Cargo.toml b/binex/Cargo.toml new file mode 100644 index 00000000..e055a180 --- /dev/null +++ b/binex/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "binex" +version = "0.2.0" +license = "MIT OR Apache-2.0" +authors = ["Guillaume W. Bres "] +description = "BINEX Binary RINEX encoder and decoder" +homepage = "https://github.com/georust/rinex" +repository = "https://github.com/georust/rinex" +keywords = ["rinex", "timing", "gps", "glonass", "galileo"] +categories = ["science", "science::geo", "parsing"] +edition = "2021" +rust-version = "1.64" + +[features] +default = ["flate2"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docrs", "--generate-link-to-definition"] + +[dependencies] +log = "0.4" +thiserror = "1" +flate2 = { version = "1.0.34", optional = true } +hifitime = { version = "4.0.0-alpha", features = ["serde", "std"] } + +[dev-dependencies] +criterion = "0.5.1" + +[[bench]] +name = "encoding" +harness = false + +[[bench]] +name = "decoding" +harness = false diff --git a/binex/README.md b/binex/README.md new file mode 100644 index 00000000..4c9cd73b --- /dev/null +++ b/binex/README.md @@ -0,0 +1,43 @@ +# BINEX + +[![Rust](https://github.com/georust/rinex/actions/workflows/rust.yml/badge.svg)](https://github.com/georust/rinex/actions/workflows/rust.yml) +[![Rust](https://github.com/georust/rinex/actions/workflows/daily.yml/badge.svg)](https://github.com/georust/rinex/actions/workflows/daily.yml) +[![crates.io](https://img.shields.io/crates/v/binex.svg)](https://crates.io/crates/binex) +[![crates.io](https://docs.rs/binex/badge.svg)](https://docs.rs/binex/badge.svg) + +BINEX is a simple library to decode and encode BINEX messages. +BINEX stands for BINary EXchange and is the "real time" stream oriented +version of the RINEX format. + +RINEX is a readable text format which is based on line termination and allows describing +from the minimum requirement for GNSS navigation up to very precise navigation and +other side GNSS applications. + +BINEX is a binary stream (non readable) conversion to that, dedicated to GNSS receivers and hardware interfacing. +Like RINEX, it is an open source format, the specifications are described by +[UNAVCO here](https://www.unavco.org/data/gps-gnss/data-formats/binex). + +This library allows easy message encoding and decoding, and aims at providing seamless +convertion from RINEX back and forth. + +## Message Decoding + +Use the BINEX `Decoder` to decode messages from a `Readable` interface: + +```rust +``` + +## Message forging + +The BINEX library allows easy message forging. Each message can be easily encoded and then +streamed into a `Writable` interface: + +```rust +``` + +## Licensing + +Licensed under either of: + +* Apache Version 2.0 ([LICENSE-APACHE](http://www.apache.org/licenses/LICENSE-2.0)) +* MIT ([LICENSE-MIT](http://opensource.org/licenses/MIT) diff --git a/binex/benches/decoding.rs b/binex/benches/decoding.rs new file mode 100644 index 00000000..6fb10d7e --- /dev/null +++ b/binex/benches/decoding.rs @@ -0,0 +1,55 @@ +use binex::prelude::{ + EphemerisFrame, Epoch, Message, MonumentGeoMetadata, MonumentGeoRecord, Record, TimeResolution, +}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +#[allow(unused_must_use)] +pub fn criterion_benchmark(c: &mut Criterion) { + let t0 = Epoch::from_gpst_seconds(10.0); + let meta = MonumentGeoMetadata::RNX2BIN; + + let record = MonumentGeoRecord::new(t0, meta) + .with_comment("This is a test") + .with_climatic_info("basic info") + .with_geophysical_info("another field") + .with_user_id("Test"); + + let record = Record::new_monument_geo(record); + let msg = Message::new(true, TimeResolution::QuarterSecond, false, false, record); + + let mut buf = [0; 256]; + msg.encode(&mut buf).unwrap(); + + c.bench_function("decoding-00", |b| { + b.iter(|| { + black_box(Message::decode(&buf).unwrap()); + }) + }); + + let record = Record::new_ephemeris_frame(EphemerisFrame::GPSRaw(Default::default())); + let msg = Message::new(true, TimeResolution::QuarterSecond, false, false, record); + + let mut buf = [0; 256]; + msg.encode(&mut buf).unwrap(); + + c.bench_function("decoding-01-00", |b| { + b.iter(|| { + black_box(Message::decode(&buf).unwrap()); + }) + }); + + let record = Record::new_ephemeris_frame(EphemerisFrame::GPS(Default::default())); + let msg = Message::new(true, TimeResolution::QuarterSecond, false, false, record); + + let mut buf = [0; 256]; + msg.encode(&mut buf).unwrap(); + + c.bench_function("decoding-01-01", |b| { + b.iter(|| { + black_box(Message::decode(&buf).unwrap()); + }) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/binex/benches/encoding.rs b/binex/benches/encoding.rs new file mode 100644 index 00000000..572a2aea --- /dev/null +++ b/binex/benches/encoding.rs @@ -0,0 +1,47 @@ +use binex::prelude::{ + EphemerisFrame, Epoch, Message, MonumentGeoMetadata, MonumentGeoRecord, Record, TimeResolution, +}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +#[allow(unused_must_use)] +pub fn criterion_benchmark(c: &mut Criterion) { + let mut buf = [0; 256]; + let t0 = Epoch::from_gpst_seconds(10.0); + let meta = MonumentGeoMetadata::RNX2BIN; + + let record = MonumentGeoRecord::new(t0, meta) + .with_comment("This is a test") + .with_climatic_info("basic info") + .with_geophysical_info("another field") + .with_user_id("Test"); + + let record = Record::new_monument_geo(record); + let msg = Message::new(true, TimeResolution::QuarterSecond, false, false, record); + + c.bench_function("encoding-00", |b| { + b.iter(|| { + black_box(msg.encode(&mut buf).unwrap()); + }) + }); + + let record = Record::new_ephemeris_frame(EphemerisFrame::GPSRaw(Default::default())); + let msg = Message::new(true, TimeResolution::QuarterSecond, false, false, record); + + c.bench_function("encoding-01-00", |b| { + b.iter(|| { + black_box(msg.encode(&mut buf).unwrap()); + }) + }); + + let record = Record::new_ephemeris_frame(EphemerisFrame::GPS(Default::default())); + let msg = Message::new(true, TimeResolution::QuarterSecond, false, false, record); + + c.bench_function("encoding-01-01", |b| { + b.iter(|| { + black_box(msg.encode(&mut buf).unwrap()); + }) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/binex/src/constants.rs b/binex/src/constants.rs new file mode 100644 index 00000000..00c7297b --- /dev/null +++ b/binex/src/constants.rs @@ -0,0 +1,35 @@ +pub struct Constants {} + +impl Constants { + /// Forward Little Endian stream with standard CRC + pub const FWDSYNC_LE_STANDARD_CRC: u8 = 0xC2; + + /// Forward Big Endian stream with standard CRC + pub const FWDSYNC_BE_STANDARD_CRC: u8 = 0xE2; + + /// Forward Little Endian stream with enhanced CRC + pub const FWDSYNC_LE_ENHANCED_CRC: u8 = 0xC8; + + /// Forward Big Endian stream with enhanced CRC + pub const FWDSYNC_BE_ENHANCED_CRC: u8 = 0xE8; + + /// Rerversed Little Endian stream with standard CRC + pub const REVSYNC_LE_STANDARD_CRC: u8 = 0xD2; + + /// Rerversed Big Endian stream with standard CRC + pub const REVSYNC_BE_STANDARD_CRC: u8 = 0xF2; + + /// Rerversed Little Endian stream with enhanced CRC + pub const REVSYNC_LE_ENHANCED_CRC: u8 = 0xD8; + + /// Rerversed Big Endian stream with enhanced CRC + pub const REVSYNC_BE_ENHANCED_CRC: u8 = 0xF8; + + /// Keep going byte mask in the BNXI algorithm, + /// as per [https://www.unavco.org/data/gps-gnss/data-formats/binex/conventions.html/#ubnxi_details] + pub const BNXI_KEEP_GOING_MASK: u8 = 0x80; + + /// Data byte mask in the BNXI algorithm, + /// as per [https://www.unavco.org/data/gps-gnss/data-formats/binex/conventions.html/#ubnxi_details] + pub const BNXI_BYTE_MASK: u8 = 0x7f; +} diff --git a/binex/src/decoder.rs b/binex/src/decoder.rs new file mode 100644 index 00000000..f4c89fb1 --- /dev/null +++ b/binex/src/decoder.rs @@ -0,0 +1,281 @@ +// use log::{debug, error}; +use std::io::{Error as IoError, Read}; + +#[cfg(feature = "flate2")] +use flate2::read::GzDecoder; + +use crate::{message::Message, Error}; +use log::warn; + +/// Abstraction for Plain or Compressed [R] +enum Reader { + Plain(R), + #[cfg(feature = "flate2")] + Compressed(GzDecoder), +} + +impl From for Reader { + fn from(r: R) -> Reader { + Self::Plain(r) + } +} + +#[cfg(feature = "flate2")] +#[cfg_attr(docsrs, doc(cfg(feature = "flate2")))] +impl From> for Reader { + fn from(r: GzDecoder) -> Reader { + Self::Compressed(r) + } +} + +impl Read for Reader { + fn read(&mut self, buf: &mut [u8]) -> Result { + match self { + Self::Plain(r) => r.read(buf), + #[cfg(feature = "flate2")] + Self::Compressed(r) => r.read(buf), + } + } +} + +/// Decoder FSM +#[derive(Debug, Copy, Clone, Default, PartialEq)] +enum State { + /// Everything is OK we're consuming data + #[default] + Parsing, + /// Partial frame is found in internal Buffer. + /// We need a secondary read to complete this message + IncompleteMessage, + /// Partial frame was found in internal Buffer. + /// But the total expected payload exceeds our internal buffer capacity. + /// [Decoder] is currently limited to parsing [Message] that fits + /// in the buffer entirely. This may not apply to very length (> 1 MB) messages + /// which is the case of signal observations for example - that we do not support at the moment. + /// In this case, we proceed to trash (consume the Input interface), complete the message + /// we do not know how to interprate & move on to next message. + IncompleteTrashing, +} + +/// [BINEX] Stream Decoder. Use this structure to decode all messages streamed +/// on a readable interface. +pub struct Decoder { + /// Internal state + state: State, + /// Read pointer + rd_ptr: usize, + /// Internal buffer + buffer: Vec, + /// [R] + reader: Reader, + /// Used when partial frame is saved within Buffer + size_to_complete: usize, +} + +impl Decoder { + /// Creates a new BINEX [Decoder] from [R] readable interface, + /// ready to parse incoming bytes. + /// ``` + /// use std::fs::File; + /// use binex::prelude::{Decoder, Error}; + /// + /// // Create the Decoder: + /// // * works from our local source + /// // * needs to be mutable due to iterating process + /// let mut fd = File::open("../test_resources/BIN/mfle20190130.bnx") + /// .unwrap(); + /// + /// let mut decoder = Decoder::new(fd); + /// + /// // Consume data stream + /// loop { + /// match decoder.next() { + /// Some(Ok(msg)) => { + /// // do something + /// }, + /// Some(Err(e)) => match e { + /// Error::IoError(e) => { + /// // any I/O error should be handled + /// // and user should react accordingly, + /// break; + /// }, + /// Error::ReversedStream | Error::LittleEndianStream => { + /// // this library is currently limited: + /// // - reversed streams are not supported yet + /// // - little endian streams are not supported yet + /// }, + /// Error::InvalidStartofStream => { + /// // other errors give meaningful information + /// }, + /// _ => {}, + /// }, + /// None => { + /// // End of stream! + /// break; + /// }, + /// } + /// } + /// ``` + pub fn new(reader: R) -> Self { + Self { + rd_ptr: 1024, + size_to_complete: 0, + reader: reader.into(), + state: State::default(), + buffer: [0; 1024].to_vec(), + } + } + + #[cfg(feature = "flate2")] + #[cfg_attr(docsrs, doc(cfg(feature = "flate2")))] + /// Creates a new Compressed BINEX stream [Decoder] from [R] readable + /// interface, that must stream Gzip encoded bytes. + /// ``` + /// use std::fs::File; + /// use binex::prelude::{Decoder, Error}; + /// + /// // Create the Decoder: + /// // * works from our local source + /// // * needs to be mutable due to iterating process + /// let mut fd = File::open("../test_resources/BIN/mfle20200105.bnx.gz") + /// .unwrap(); + /// + /// let mut decoder = Decoder::new(fd); + /// + /// // Consume data stream + /// loop { + /// match decoder.next() { + /// Some(Ok(msg)) => { + /// // do something + /// }, + /// Some(Err(e)) => match e { + /// Error::IoError(e) => { + /// // any I/O error should be handled + /// // and user should react accordingly, + /// break; + /// }, + /// Error::ReversedStream | Error::LittleEndianStream => { + /// // this library is currently limited: + /// // - reversed streams are not supported yet + /// // - little endian streams are not supported yet + /// }, + /// Error::InvalidStartofStream => { + /// // other errors give meaningful information + /// }, + /// _ => {}, + /// }, + /// None => { + /// // End of stream! + /// break; + /// }, + /// } + /// } + /// ``` + pub fn new_gzip(reader: R) -> Self { + Self { + rd_ptr: 1024, + size_to_complete: 0, + state: State::default(), + buffer: [0; 1024].to_vec(), + reader: GzDecoder::new(reader).into(), + } + } +} + +impl Iterator for Decoder { + type Item = Result; + /// Parse next message contained in stream + fn next(&mut self) -> Option { + // parse internal buffer + while self.rd_ptr < 1024 && self.state == State::Parsing { + //println!("parsing: rd={}/wr={}", self.rd_ptr, 1024); + //println!("workbuf: {:?}", &self.buffer[self.rd_ptr..]); + + match Message::decode(&self.buffer[self.rd_ptr..]) { + Ok(msg) => { + // one message fully decoded + // - increment pointer so we can move on to the next + // - and expose to User. + self.rd_ptr += msg.encoding_size(); + return Some(Ok(msg)); + }, + Err(Error::IncompleteMessage(mlen)) => { + //print!("INCOMPLETE: rd_ptr={}/mlen={}", self.rd_ptr, mlen); + // buffer contains partial message + + // [IF] mlen (size to complete) fits in self.buffer + self.size_to_complete = mlen - self.rd_ptr; + if self.size_to_complete < 1024 { + // Then next .read() will complete this message + // and we will then be able to complete the parsing. + // Shift current content (rd_ptr=>0) and preserve then move on to Reading. + self.buffer.copy_within(self.rd_ptr..1024, 0); + self.state = State::IncompleteMessage; + } else { + // OR + // NB: some messages can be very lengthy (some MB) + // especially the signal sampling that we do not support yet. + // In this case, we simply trash the remaning amount of bytes, + // message is lost and we move on to the next SYNC + warn!("library limitation: unprocessed message"); + self.state = State::IncompleteTrashing; + //println!("need to trash {} bytes", self.size_to_complete); + } + }, + Err(Error::NoSyncByte) => { + // no SYNC in entire buffer + //println!(".decode no-sync"); + // prepare for next read + self.rd_ptr = 1024; + //self.buffer.clear(); + self.buffer = [0; 1024].to_vec(); + }, + Err(_) => { + // decoding error: unsupported message + // Keep iterating the buffer + self.rd_ptr += 1; + }, + } + } + + // read data: fill in buffer + match self.reader.read_exact(&mut self.buffer) { + Ok(_) => { + match self.state { + State::Parsing => {}, + State::IncompleteMessage => { + // complete frame, move on to parsing + self.state = State::Parsing; + }, + State::IncompleteTrashing => { + if self.size_to_complete == 0 { + // trashed completely. + self.state = State::Parsing; + //println!("back to parsing"); + } else { + if self.size_to_complete < 1024 { + //println!("shiting {} bytes", self.size_to_complete); + + // discard remaning bytes from buffer + // and move on to parsing to analyze new content + self.buffer.copy_within(self.size_to_complete.., 0); + self.state = State::Parsing; + //println!("back to parsing"); + } else { + self.size_to_complete = + self.size_to_complete.saturating_add_signed(-1024); + //println!("size to trash: {}", self.size_to_complete); + } + } + }, + } + // read success + self.rd_ptr = 0; // reset pointer & prepare for next Iter + Some(Err(Error::NotEnoughBytes)) + }, + Err(_) => { + None // EOS + }, + } + } +} diff --git a/binex/src/encoder.rs b/binex/src/encoder.rs new file mode 100644 index 00000000..e3cc2c8d --- /dev/null +++ b/binex/src/encoder.rs @@ -0,0 +1,66 @@ +// use log::{debug, error}; +use std::io::{ + Result as IoResult, + //Error as IoError, + Write, +}; + +#[cfg(feature = "flate2")] +use flate2::{write::GzEncoder, Compression as GzCompression}; + +/// Abstraction for Plain or Compressed [R] +enum Writer { + Plain(W), + #[cfg(feature = "flate2")] + Compressed(GzEncoder), +} + +impl From for Writer { + fn from(w: W) -> Writer { + Self::Plain(w) + } +} + +#[cfg(feature = "flate2")] +impl From> for Writer { + fn from(w: GzEncoder) -> Writer { + Self::Compressed(w) + } +} + +impl Write for Writer { + fn write(&mut self, buf: &[u8]) -> IoResult { + match self { + Self::Plain(w) => w.write(buf), + #[cfg(feature = "flate2")] + Self::Compressed(w) => w.write(buf), + } + } + fn flush(&mut self) -> IoResult<()> { + match self { + Self::Plain(w) => w.flush(), + #[cfg(feature = "flate2")] + Self::Compressed(w) => w.flush(), + } + } +} + +/// [BINEX] Stream Encoder. +pub struct Encoder { + /// [W] + writer: Writer, +} + +impl Encoder { + pub fn new(writer: W) -> Self { + Self { + writer: writer.into(), + } + } + #[cfg(feature = "flate2")] + pub fn new_gzip(writer: W, compression_level: u32) -> Self { + Self { + writer: GzEncoder::new(writer, GzCompression::new(compression_level)).into(), + } + } +} diff --git a/binex/src/lib.rs b/binex/src/lib.rs new file mode 100644 index 00000000..04f519e6 --- /dev/null +++ b/binex/src/lib.rs @@ -0,0 +1,54 @@ +#![doc(html_logo_url = "https://raw.githubusercontent.com/georust/meta/master/logo/logo.png")] +#![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_cfg))] + +use thiserror::Error; + +mod decoder; +mod encoder; +mod message; + +pub(crate) mod constants; +pub(crate) mod utils; + +pub mod prelude { + pub use crate::{ + decoder::Decoder, + encoder::Encoder, + message::{ + EphemerisFrame, GPSEphemeris, GPSRaw, Message, MonumentGeoMetadata, MonumentGeoRecord, + Record, TimeResolution, + }, + Error, + }; + // re-export + pub use hifitime::Epoch; +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("not enough bytes available")] + NotEnoughBytes, + #[error("i/o error")] + IoError(#[from] std::io::Error), + #[error("invalid start of stream")] + InvalidStartofStream, + #[error("no SYNC byte found")] + NoSyncByte, + #[error("reversed streams are not supported yet")] + ReversedStream, + #[error("little endian encoded streams not supported yet")] + LittleEndianStream, + #[error("enhanced crc is not supported yet")] + EnhancedCrc, + #[error("non supported timescale")] + NonSupportedTimescale, + #[error("unknown message")] + UnknownMessage, + #[error("unknown record field id")] + UnknownRecordFieldId, + #[error("utf8 error")] + Utf8Error, + #[error("Incomplete message")] + IncompleteMessage(usize), +} diff --git a/binex/src/message/checksum.rs b/binex/src/message/checksum.rs new file mode 100644 index 00000000..bbe20474 --- /dev/null +++ b/binex/src/message/checksum.rs @@ -0,0 +1,68 @@ +use crate::Message; + +/// BINEX Checksum Calculator +pub enum Checksum { + /// For [1 - 2^8-1] message + /// CRC is 1 byte XOR + XOR8, + /// For [2^8, 2^12-1] message + POLY12, + /// For [2^12, 2^20-1] message, + POLY20, +} + +impl Checksum { + const fn binary_mask(&self) -> u32 { + match self { + Self::XOR8 => 0xff, + Self::POLY12 => 0x7ff, + Self::POLY20 => 0xfff, + } + } + const fn look_up_table(&self) -> &[u8] { + match self { + Self::XOR8 => &[0, 0, 0, 0], + Self::POLY12 => { + // CRC12 table + // x^16 + x^12 + x^5 +1 + &[0, 1, 2, 3] + }, + Self::POLY20 => { + // CRC16 table + // x^32 + x^26 + x^23 + x^22 + x^16 + x^12 + x^11 + x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x^1 +1 + &[0, 1, 2, 3] + }, + } + } + pub fn new(msg: &Message) -> Self { + if msg.len() < 127 { + Self::XOR8 + } else if msg.len() < 4096 { + Self::POLY12 + } else { + Self::POLY20 + } + } + /// Calculates CRC from [Message] + pub fn calc_from_msg(msg: &Message) -> u32 { + let bytes = msg.to_bytes(); + Self::calc_from_bytes(bytes) + } + /// Macro that verifies this [Message] contains correct CRC + pub fn crc_ok(msg: &Message) -> bool { + let crc = msg.crc(); + Self::calc_from_msg(msg) == crc + } + /// Calculates CRC from buffer content. + /// Correct content must be correctly extracted beforehand. + pub fn calc_from_bytes(raw: &[u8]) -> u32 { + let size = raw.len(); + if size < 128 { + // 0-127 bytes: 1 byte checksum XOR all bytes + } else if size < 4096 { + // 128-4095 x^16 + x^12 + x^5 + x^0 polynomial + //let lut = self.look_up_table(); + } + 0 + } +} diff --git a/binex/src/message/mid.rs b/binex/src/message/mid.rs new file mode 100644 index 00000000..8cf9779a --- /dev/null +++ b/binex/src/message/mid.rs @@ -0,0 +1,63 @@ +//! Message ID from and to binary + +/// MessageID stands for Record ID byte +/// and follows the Sync Byte +#[derive(Debug, Copy, Clone, Default, PartialEq)] +pub enum MessageID { + /// Geodetic Marker, Site and Refenrece point info: + /// Geodetic metadata + SiteMonumentMarker = 0, + /// Decode Ephemeris frame + Ephemeris = 1, + /// Observation time tag and receiver info + ObservationTimeTagRxInfo = 2, + /// Local Meteorological and Geophysical information + Meteo = 3, + /// Receiver info: BINEX specific + ReceiverInfo = 4, + /// Processed Solutions like PVT + ProcessedSolutions = 5, + // Receiver info prototyping: BINEX specific + ReceiverInfoPrototyping = 125, + /// Meteo prototyping: BINEX specific + MeteoPrototyping = 126, + /// Observation time tag prototyping: BINEX specific + ObservationTimeTagRxPrototyping = 127, + // Unknown / unsupported message + #[default] + Unknown = 0xffffffff, +} + +impl From for MessageID { + fn from(val: u32) -> Self { + match val { + 0 => Self::SiteMonumentMarker, + 1 => Self::Ephemeris, + 2 => Self::ObservationTimeTagRxInfo, + 3 => Self::Meteo, + 4 => Self::ReceiverInfo, + 5 => Self::ProcessedSolutions, + 125 => Self::ReceiverInfoPrototyping, + 126 => Self::MeteoPrototyping, + 127 => Self::ObservationTimeTagRxPrototyping, + _ => Self::Unknown, + } + } +} + +impl From for u32 { + fn from(val: MessageID) -> u32 { + match val { + MessageID::SiteMonumentMarker => 0, + MessageID::Ephemeris => 1, + // MessageID::ObservationTimeTagRxInfo => 0x02, + // MessageID::Meteo => 0x03, + // MessageID::ReceiverInfo => 0x04, + // MessageID::ProcessedSolutions => 0x05, + // MessageID::ReceiverInfoPrototyping => 0x7d, + // MessageID::MeteoPrototyping => 0x7e, + // MessageID::ObservationTimeTagRxPrototyping => 0x7f, + _ => 0xffffffff, + } + } +} diff --git a/binex/src/message/mod.rs b/binex/src/message/mod.rs new file mode 100644 index 00000000..5af4ef92 --- /dev/null +++ b/binex/src/message/mod.rs @@ -0,0 +1,583 @@ +mod mid; // message ID +mod record; // Record: message content +mod time; // Epoch encoding/decoding + +pub use record::{ + EphemerisFrame, GPSEphemeris, GPSRaw, MonumentGeoMetadata, MonumentGeoRecord, Record, +}; + +pub use time::TimeResolution; + +pub(crate) use mid::MessageID; + +use crate::{constants::Constants, utils::Utils, Error}; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Message { + /// Endianness used when encoding current message, + /// defined by SYNC byte + pub big_endian: bool, + /// MID byte stored as [MessageID] + mid: MessageID, + /// True when using enhanced CRC + pub enhanced_crc: bool, + /// True when reversible stream + pub reversed: bool, + /// [Record] + pub record: Record, + /// Time Resolution in use + time_res: TimeResolution, +} + +impl Message { + /// Creates new [Message] from given specs, ready to encode. + pub fn new( + big_endian: bool, + time_res: TimeResolution, + enhanced_crc: bool, + reversed: bool, + record: Record, + ) -> Self { + let mid = record.to_message_id(); + Self { + mid, + record, + time_res, + reversed, + big_endian, + enhanced_crc, + } + } + + /// Returns total size required to encode this [Message]. + /// Use this to fulfill [Self::encode] requirements. + pub fn encoding_size(&self) -> usize { + let mut total = 1; // SYNC + + let mid = self.record.to_message_id() as u32; + total += Self::bnxi_encoding_size(mid); + + let mlen = self.record.encoding_size() as u32; + total += Self::bnxi_encoding_size(mlen); + + total += self.record.encoding_size(); + total += 1; // CRC: TODO! + total + } + + /// Decoding attempt from buffered content. + pub fn decode(buf: &[u8]) -> Result { + let sync_off; + let mut big_endian = true; + let mut reversed = false; + let mut enhanced_crc = false; + let time_res = TimeResolution::QuarterSecond; + + // 1. locate SYNC byte + if let Some(offset) = Self::locate(Constants::FWDSYNC_BE_STANDARD_CRC, buf) { + big_endian = true; + sync_off = offset; + } else if let Some(offset) = Self::locate(Constants::FWDSYNC_LE_STANDARD_CRC, buf) { + sync_off = offset; + big_endian = false; + } else if let Some(offset) = Self::locate(Constants::FWDSYNC_BE_ENHANCED_CRC, buf) { + big_endian = true; + enhanced_crc = true; + sync_off = offset; + } else if let Some(offset) = Self::locate(Constants::FWDSYNC_LE_ENHANCED_CRC, buf) { + enhanced_crc = true; + sync_off = offset; + } else if let Some(offset) = Self::locate(Constants::REVSYNC_LE_STANDARD_CRC, buf) { + reversed = true; + sync_off = offset; + } else if let Some(offset) = Self::locate(Constants::REVSYNC_BE_STANDARD_CRC, buf) { + reversed = true; + big_endian = true; + sync_off = offset; + } else if let Some(offset) = Self::locate(Constants::REVSYNC_BE_ENHANCED_CRC, buf) { + reversed = true; + big_endian = true; + enhanced_crc = true; + sync_off = offset; + } else if let Some(offset) = Self::locate(Constants::REVSYNC_LE_ENHANCED_CRC, buf) { + reversed = true; + enhanced_crc = true; + sync_off = offset; + } else { + // no SYNC byte found + return Err(Error::NoSyncByte); + } + + // TODO: non supported cases + // * Rev Streams are not supported + // * Only basic CRC is managed + // * Little Endianness not tested yet! + if reversed { + return Err(Error::ReversedStream); + } + if enhanced_crc { + return Err(Error::EnhancedCrc); + } + if !big_endian { + return Err(Error::LittleEndianStream); + } + + // make sure we can parse up to 4 byte MID + if buf.len() - sync_off < 4 { + return Err(Error::NotEnoughBytes); + } + + let mut ptr = sync_off + 1; + + // 2. parse MID + let (bnxi, size) = Self::decode_bnxi(&buf[ptr..], big_endian); + let mid = MessageID::from(bnxi); + //println!("mid={:?}", mid); + ptr += size; + + // make sure we can parse up to 4 byte MLEN + if buf.len() - ptr < 4 { + return Err(Error::NotEnoughBytes); + } + + // 3. parse MLEN + let (mlen, size) = Self::decode_bnxi(&buf[ptr..], big_endian); + let mlen = mlen as usize; + + if buf.len() - ptr < mlen { + // buffer does not contain complete message! + return Err(Error::IncompleteMessage(mlen)); + } + + //println!("mlen={:?}", mlen); + ptr += size; + + // 4. parse RECORD + let record = match mid { + MessageID::SiteMonumentMarker => { + let rec = + MonumentGeoRecord::decode(mlen as usize, time_res, big_endian, &buf[ptr..])?; + Record::new_monument_geo(rec) + }, + MessageID::Ephemeris => { + let fr = EphemerisFrame::decode(big_endian, &buf[ptr..])?; + Record::new_ephemeris_frame(fr) + }, + MessageID::Unknown => { + //println!("id=0xffffffff"); + return Err(Error::UnknownMessage); + }, + id => { + //println!("found unsupported msg id={:?}", id); + return Err(Error::UnknownMessage); + }, + }; + + // 5. CRC verification + + Ok(Self { + mid, + record, + reversed, + time_res, + big_endian, + enhanced_crc, + }) + } + + /// [Message] encoding attempt into buffer. + /// [Self] must fit in preallocated size. + /// Returns total encoded size, which is equal to the message size (in bytes). + pub fn encode(&self, buf: &mut [u8]) -> Result { + let total = self.encoding_size(); + if buf.len() < total { + return Err(Error::NotEnoughBytes); + } + + // Encode SYNC byte + buf[0] = self.sync_byte(); + let mut ptr = 1; + + // Encode MID + let mid = self.record.to_message_id() as u32; + ptr += Self::encode_bnxi(mid, self.big_endian, &mut buf[ptr..])?; + + // Encode MLEN + let mlen = self.record.encoding_size() as u32; + ptr += Self::encode_bnxi(mlen, self.big_endian, &mut buf[ptr..])?; + + // Encode message + match &self.record { + Record::EphemerisFrame(fr) => { + fr.encode(self.big_endian, &mut buf[ptr..])?; + }, + Record::MonumentGeo(geo) => { + geo.encode(self.big_endian, &mut buf[ptr..])?; + }, + } + + // TODO: encode CRC + + Ok(ptr) + } + + /// Returns the SYNC byte we expect for [Self] + pub(crate) fn sync_byte(&self) -> u8 { + if self.reversed { + if self.big_endian { + if self.enhanced_crc { + Constants::REVSYNC_BE_ENHANCED_CRC + } else { + Constants::REVSYNC_BE_STANDARD_CRC + } + } else { + if self.enhanced_crc { + Constants::REVSYNC_LE_ENHANCED_CRC + } else { + Constants::REVSYNC_LE_STANDARD_CRC + } + } + } else { + if self.big_endian { + if self.enhanced_crc { + Constants::FWDSYNC_BE_ENHANCED_CRC + } else { + Constants::FWDSYNC_BE_STANDARD_CRC + } + } else { + if self.enhanced_crc { + Constants::FWDSYNC_LE_ENHANCED_CRC + } else { + Constants::FWDSYNC_LE_STANDARD_CRC + } + } + } + } + + /// Tries to locate desired byte within buffer + fn locate(to_find: u8, buf: &[u8]) -> Option { + buf.iter().position(|b| *b == to_find) + } + + // /// Evaluates CRC for [Self] + // pub(crate) fn eval_crc(&self) -> u32 { + // 0 + // } + + /// Decodes BNXI encoded unsigned U32 integer with selected endianness, + /// according to [https://www.unavco.org/data/gps-gnss/data-formats/binex/conventions.html/#ubnxi_details]. + /// ## Outputs + /// * u32: decoded U32 integer + /// * usize: number of bytes consumed in this process + /// ie., last byte contributing to the BNXI encoding. + /// The next byte is the following content. + pub(crate) fn decode_bnxi(buf: &[u8], big_endian: bool) -> (u32, usize) { + let mut last_preserved = 0; + + // handles invalid case + if buf.len() == 1 { + if buf[0] & Constants::BNXI_KEEP_GOING_MASK > 0 { + return (0, 0); + } + } + + for i in 0..Utils::min_usize(buf.len(), 4) { + if i < 3 { + if buf[i] & Constants::BNXI_KEEP_GOING_MASK == 0 { + last_preserved = i; + break; + } + } else { + last_preserved = i; + } + } + + // apply mask + let masked = buf + .iter() + .enumerate() + .map(|(j, b)| { + if j == 3 { + *b + } else { + *b & Constants::BNXI_BYTE_MASK + } + }) + .collect::>(); + + let mut ret = 0_u32; + + // interprate as desired + if big_endian { + for i in 0..=last_preserved { + ret += (masked[i] as u32) << (8 * i); + } + } else { + for i in 0..=last_preserved { + ret += (masked[i] as u32) << ((4 - i) * 8); + } + } + + (ret, last_preserved + 1) + } + + /// Number of bytes to encode U32 unsigned integer + /// following the 1-4 BNXI encoding algorithm + pub(crate) fn bnxi_encoding_size(val: u32) -> usize { + let bytes = (val as f64).log2().ceil() as usize / 8 + 1; + Utils::min_usize(bytes, 4) + } + + /// U32 to BNXI encoder according to [https://www.unavco.org/data/gps-gnss/data-formats/binex/conventions.html/#ubnxi_details]. + /// Encodes into given buffer, returns encoding size. + /// Will fail if buffer is too small. + pub(crate) fn encode_bnxi(val: u32, big_endian: bool, buf: &mut [u8]) -> Result { + let bytes = Self::bnxi_encoding_size(val); + if buf.len() < bytes { + return Err(Error::NotEnoughBytes); + } + + for i in 0..bytes { + if big_endian { + buf[i] = (val >> (8 * i)) as u8; + if i < 3 { + buf[i] &= Constants::BNXI_BYTE_MASK; + } + } else { + buf[bytes - 1 - i] = (val >> (8 * i)) as u8; + if i < 3 { + buf[bytes - 1 - i] &= Constants::BNXI_BYTE_MASK; + } + } + + if i > 0 { + if big_endian { + buf[i - 1] |= Constants::BNXI_KEEP_GOING_MASK; + } else { + buf[bytes - 1 - i - 1] |= Constants::BNXI_KEEP_GOING_MASK; + } + } + } + + return Ok(bytes); + } +} + +#[cfg(test)] +mod test { + use super::Message; + use crate::message::TimeResolution; + use crate::message::{EphemerisFrame, GPSRaw, Record}; + use crate::{constants::Constants, Error}; + #[test] + fn big_endian_bnxi_1() { + let bytes = [0x7a]; + let (val, size) = Message::decode_bnxi(&bytes, true); + assert_eq!(size, 1); + assert_eq!(val, 0x7a); + + // test mirror op + let mut buf = [0]; + let size = Message::encode_bnxi(val, true, &mut buf).unwrap(); + assert_eq!(size, 1); + assert_eq!(buf, [0x7a]); + + let mut buf = [0, 0, 0, 0]; + let size = Message::encode_bnxi(val, true, &mut buf).unwrap(); + assert_eq!(size, 1); + assert_eq!(buf, [0x7a, 0, 0, 0]); + + // invalid case + let bytes = [0x81]; + let (_, size) = Message::decode_bnxi(&bytes, true); + assert_eq!(size, 0); + } + + #[test] + fn big_endian_bnxi_2() { + let bytes = [0x7a, 0x81]; + let (val, size) = Message::decode_bnxi(&bytes, true); + assert_eq!(size, 1); + assert_eq!(val, 0x7a); + + // test mirror op + let mut buf = [0, 0]; + let size = Message::encode_bnxi(val, true, &mut buf).unwrap(); + assert_eq!(size, 1); + assert_eq!(buf, [0x7a, 0]); + + let bytes = [0x83, 0x7a]; + let (val, size) = Message::decode_bnxi(&bytes, true); + assert_eq!(size, 2); + assert_eq!(val, 0x7a03); + + // test mirror op + let mut buf = [0, 0]; + let size = Message::encode_bnxi(val, true, &mut buf).unwrap(); + assert_eq!(size, 2); + assert_eq!(buf, [0x83, 0x7a]); + } + + #[test] + fn big_endian_bnxi_3() { + let bytes = [0x83, 0x84, 0x7a]; + let (val, size) = Message::decode_bnxi(&bytes, true); + assert_eq!(size, 3); + assert_eq!(val, 0x7a0403); + + let bytes = [0x83, 0x84, 0x7a, 0]; + let (val, size) = Message::decode_bnxi(&bytes, true); + assert_eq!(size, 3); + assert_eq!(val, 0x7a0403); + + let bytes = [0x83, 0x84, 0x7a, 0, 0]; + let (val, size) = Message::decode_bnxi(&bytes, true); + assert_eq!(size, 3); + assert_eq!(val, 0x7a0403); + + // test mirror op + let mut buf = [0, 0, 0, 0, 0, 0]; + let size = Message::encode_bnxi(val, true, &mut buf).unwrap(); + assert_eq!(size, 3); + assert_eq!(buf, [0x83, 0x84, 0x7a, 0, 0, 0]); + } + + #[test] + fn big_endian_bnxi_4() { + let bytes = [0x7f, 0x81, 0x7f, 0xab]; + let (val, size) = Message::decode_bnxi(&bytes, true); + assert_eq!(size, 1); + assert_eq!(val, 0x7f); + + // test mirror + let mut buf = [0, 0, 0, 0]; + let size = Message::encode_bnxi(val, true, &mut buf).unwrap(); + assert_eq!(size, 1); + assert_eq!(buf, [0x7f, 0, 0, 0]); + + let bytes = [0x81, 0xaf, 0x7f, 0xab]; + let (val, size) = Message::decode_bnxi(&bytes, true); + assert_eq!(size, 3); + assert_eq!(val, 0x7f2f01); + + // test mirror + let mut buf = [0, 0, 0]; + let size = Message::encode_bnxi(val, true, &mut buf).unwrap(); + assert_eq!(size, 3); + assert_eq!(buf, [0x81, 0xaf, 0x7f]); + + // test mirror + let mut buf = [0, 0, 0, 0]; + let size = Message::encode_bnxi(val, true, &mut buf).unwrap(); + assert_eq!(size, 3); + assert_eq!(buf, [0x81, 0xaf, 0x7f, 0]); + + let bytes = [0x81, 0xaf, 0x8f, 1]; + let (val, size) = Message::decode_bnxi(&bytes, true); + assert_eq!(size, 4); + assert_eq!(val, 0x10f2f01); + + // test mirror + let mut buf = [0, 0, 0, 0]; + let size = Message::encode_bnxi(val, true, &mut buf).unwrap(); + assert_eq!(size, 4); + assert_eq!(buf, [0x81, 0xaf, 0x8f, 1]); + + // test mirror + let mut buf = [0, 0, 0, 0, 0]; + let size = Message::encode_bnxi(val, true, &mut buf).unwrap(); + assert_eq!(size, 4); + assert_eq!(buf, [0x81, 0xaf, 0x8f, 1, 0]); + + let bytes = [0x81, 0xaf, 0x8f, 0x7f]; + let (val, size) = Message::decode_bnxi(&bytes, true); + assert_eq!(size, 4); + assert_eq!(val, 0x7f0f2f01); + + // test mirror + let mut buf = [0, 0, 0, 0]; + let size = Message::encode_bnxi(val, true, &mut buf).unwrap(); + assert_eq!(size, 4); + assert_eq!(buf, [0x81, 0xaf, 0x8f, 0x7f]); + + let bytes = [0x81, 0xaf, 0x8f, 0x80]; + let (val, size) = Message::decode_bnxi(&bytes, true); + assert_eq!(size, 4); + assert_eq!(val, 0x800f2f01); + + // test mirror + let mut buf = [0, 0, 0, 0]; + let size = Message::encode_bnxi(val, true, &mut buf).unwrap(); + assert_eq!(size, 4); + assert_eq!(buf, [0x81, 0xaf, 0x8f, 0x80]); + } + + #[test] + fn decode_no_sync_byte() { + let buf = [0, 0, 0, 0, 0]; + match Message::decode(&buf) { + Err(Error::NoSyncByte) => {}, + Err(e) => panic!("returned unexpected error: {}", e), + _ => panic!("should have paniced"), + } + let buf = [0, 0, 0, 0, 0]; + match Message::decode(&buf) { + Err(Error::NoSyncByte) => {}, + Err(e) => panic!("returned unexpected error: {}", e), + _ => panic!("should have paniced"), + } + } + #[test] + fn decode_fwd_enhancedcrc_stream() { + let buf = [Constants::FWDSYNC_BE_ENHANCED_CRC, 0, 0, 0]; + match Message::decode(&buf) { + Err(Error::EnhancedCrc) => {}, + Err(e) => panic!("returned unexpected error: {}", e), + _ => panic!("should have paniced"), + } + } + #[test] + fn decode_fwd_le_stream() { + let buf = [Constants::FWDSYNC_LE_STANDARD_CRC, 0, 0, 0]; + match Message::decode(&buf) { + Err(Error::LittleEndianStream) => {}, + Err(e) => panic!("returned unexpected error: {}", e), + _ => panic!("should have paniced"), + } + } + #[test] + fn decode_reversed_stream() { + let buf = [Constants::REVSYNC_BE_STANDARD_CRC, 0, 0, 0]; + match Message::decode(&buf) { + Err(Error::ReversedStream) => {}, + Err(e) => panic!("returned unexpected error: {}", e), + _ => panic!("should have paniced"), + } + let buf = [Constants::REVSYNC_BE_ENHANCED_CRC, 0, 0, 0]; + match Message::decode(&buf) { + Err(Error::ReversedStream) => {}, + Err(e) => panic!("returned unexpected error: {}", e), + _ => panic!("should have paniced"), + } + let buf = [Constants::REVSYNC_LE_STANDARD_CRC, 0, 0, 0]; + match Message::decode(&buf) { + Err(Error::ReversedStream) => {}, + Err(e) => panic!("returned unexpected error: {}", e), + _ => panic!("should have paniced"), + } + let buf = [Constants::REVSYNC_LE_ENHANCED_CRC, 0, 0, 0]; + match Message::decode(&buf) { + Err(Error::ReversedStream) => {}, + Err(e) => panic!("returned unexpected error: {}", e), + _ => panic!("should have paniced"), + } + } + #[test] + fn test_gps_raw() { + let record = Record::new_ephemeris_frame(EphemerisFrame::GPSRaw(GPSRaw::default())); + let msg = Message::new(true, TimeResolution::QuarterSecond, false, false, record); + + let mut encoded = [0; 256]; + msg.encode(&mut encoded).unwrap(); + } +} diff --git a/binex/src/message/record/ephemeris/fid.rs b/binex/src/message/record/ephemeris/fid.rs new file mode 100644 index 00000000..e633cf6f --- /dev/null +++ b/binex/src/message/record/ephemeris/fid.rs @@ -0,0 +1,48 @@ +//! Ephemeris Field ID + +/// [FieldID] describes the content to follow in Ephemeris frames +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum FieldID { + /// Raw (non decoded) GPS Ephemeris message. + /// Streamed as is: it did not go through the decoding process. + /// * uint1 + /// * sint4 ToW in seconds + /// * 72 bytes: GPS ephemeris subframe + GPSRaw = 0, + /// Decoded GPS Ephemeris + GPS = 1, + /// Decoded GLO Ephemeris + GLO = 2, + /// Decoded SBAS Ephemeris + SBAS = 3, + /// Decoded GAL Ephemeris + GAL = 4, + /// Unknown / Invalid + Unknown = 0xffffffff, +} + +impl From for FieldID { + fn from(val: u32) -> Self { + match val { + 0 => Self::GPSRaw, + 1 => Self::GPS, + 2 => Self::GLO, + 3 => Self::SBAS, + 4 => Self::GAL, + _ => Self::Unknown, + } + } +} + +impl From for u32 { + fn from(val: FieldID) -> u32 { + match val { + FieldID::GPSRaw => 0, + FieldID::GPS => 1, + FieldID::GLO => 2, + FieldID::SBAS => 3, + FieldID::GAL => 4, + FieldID::Unknown => 0xffffffff, + } + } +} diff --git a/binex/src/message/record/ephemeris/galileo.rs b/binex/src/message/record/ephemeris/galileo.rs new file mode 100644 index 00000000..eca9f90c --- /dev/null +++ b/binex/src/message/record/ephemeris/galileo.rs @@ -0,0 +1,419 @@ +//! Galileo ephemeris +use crate::{utils::Utils, Error}; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct GALEphemeris { + pub sv_prn: u8, + pub toe_week: u16, + pub tow: i32, + pub toe_s: i32, + pub bgd_e5a_e1_s: f32, + pub bgd_e5b_e1_s: f32, + pub iodnav: i32, + pub clock_drift_rate: f32, + pub clock_drift: f32, + pub clock_offset: f32, + pub delta_n_semi_circles_s: f32, + pub m0_rad: f64, + pub e: f64, + pub sqrt_a: f64, + pub cic: f32, + pub crc: f32, + pub cis: f32, + pub crs: f32, + pub cuc: f32, + pub cus: f32, + pub omega_0_rad: f64, + pub omega_rad: f64, + pub i0_rad: f64, + pub omega_dot_semi_circles: f32, + pub idot_semi_circles_s: f32, + pub sisa: f32, + pub sv_health: u16, + pub source: u16, +} + +impl GALEphemeris { + pub(crate) const fn encoding_size() -> usize { + 154 + } + pub fn encode(&self, big_endian: bool, buf: &mut [u8]) -> Result { + let size = Self::encoding_size(); + if buf.len() < size { + return Err(Error::NotEnoughBytes); + } + + buf[0] = self.sv_prn; + + let toe_week = if big_endian { + self.toe_week.to_be_bytes() + } else { + self.toe_week.to_le_bytes() + }; + + buf[1..3].copy_from_slice(&toe_week); + + let tow = if big_endian { + self.tow.to_be_bytes() + } else { + self.tow.to_le_bytes() + }; + + buf[4..8].copy_from_slice(&tow); + + let toe_s = if big_endian { + self.toe_s.to_be_bytes() + } else { + self.toe_s.to_le_bytes() + }; + + buf[9..13].copy_from_slice(&toe_s); + + let bgd_e5a_e1_s = if big_endian { + self.bgd_e5a_e1_s.to_be_bytes() + } else { + self.bgd_e5a_e1_s.to_le_bytes() + }; + + buf[14..18].copy_from_slice(&bgd_e5a_e1_s); + + let bgd_e5b_e1_s = if big_endian { + self.bgd_e5b_e1_s.to_be_bytes() + } else { + self.bgd_e5b_e1_s.to_le_bytes() + }; + + buf[19..23].copy_from_slice(&bgd_e5b_e1_s); + + let iodnav = if big_endian { + self.iodnav.to_be_bytes() + } else { + self.iodnav.to_le_bytes() + }; + + buf[24..28].copy_from_slice(&iodnav); + + let clock_drift_rate = if big_endian { + self.clock_drift_rate.to_be_bytes() + } else { + self.clock_drift_rate.to_le_bytes() + }; + + buf[29..33].copy_from_slice(&clock_drift_rate); + + let clock_drift = if big_endian { + self.clock_drift.to_be_bytes() + } else { + self.clock_drift.to_le_bytes() + }; + + buf[34..38].copy_from_slice(&clock_drift); + + let clock_offset = if big_endian { + self.clock_offset.to_be_bytes() + } else { + self.clock_offset.to_le_bytes() + }; + + buf[39..43].copy_from_slice(&clock_offset); + + let delta_n_semi_circles_s = if big_endian { + self.delta_n_semi_circles_s.to_be_bytes() + } else { + self.delta_n_semi_circles_s.to_le_bytes() + }; + + buf[44..48].copy_from_slice(&delta_n_semi_circles_s); + + let m0_rad = if big_endian { + self.m0_rad.to_be_bytes() + } else { + self.m0_rad.to_le_bytes() + }; + + buf[49..57].copy_from_slice(&m0_rad); + + let e = if big_endian { + self.e.to_be_bytes() + } else { + self.e.to_le_bytes() + }; + + buf[58..66].copy_from_slice(&e); + + let sqrt_a = if big_endian { + self.sqrt_a.to_be_bytes() + } else { + self.sqrt_a.to_le_bytes() + }; + + buf[67..75].copy_from_slice(&sqrt_a); + + let cic = if big_endian { + self.cic.to_be_bytes() + } else { + self.cic.to_le_bytes() + }; + + buf[76..80].copy_from_slice(&cic); + + let crc = if big_endian { + self.crc.to_be_bytes() + } else { + self.crc.to_le_bytes() + }; + + buf[81..85].copy_from_slice(&crc); + + let cis = if big_endian { + self.cis.to_be_bytes() + } else { + self.cis.to_le_bytes() + }; + + buf[86..90].copy_from_slice(&cis); + + let crs = if big_endian { + self.crs.to_be_bytes() + } else { + self.crs.to_le_bytes() + }; + + buf[91..95].copy_from_slice(&crs); + + let cuc = if big_endian { + self.cuc.to_be_bytes() + } else { + self.cuc.to_le_bytes() + }; + + buf[96..100].copy_from_slice(&cuc); + + let cus = if big_endian { + self.cus.to_be_bytes() + } else { + self.cus.to_le_bytes() + }; + + buf[101..105].copy_from_slice(&cus); + + let omega_0_rad = if big_endian { + self.omega_0_rad.to_be_bytes() + } else { + self.omega_0_rad.to_le_bytes() + }; + + buf[106..114].copy_from_slice(&omega_0_rad); + + let omega_rad = if big_endian { + self.omega_rad.to_be_bytes() + } else { + self.omega_rad.to_le_bytes() + }; + + buf[115..123].copy_from_slice(&omega_rad); + + let i0_rad = if big_endian { + self.i0_rad.to_be_bytes() + } else { + self.i0_rad.to_le_bytes() + }; + + buf[124..132].copy_from_slice(&i0_rad); + + let omega_dot_semi_circles = if big_endian { + self.omega_dot_semi_circles.to_be_bytes() + } else { + self.omega_dot_semi_circles.to_le_bytes() + }; + + buf[133..137].copy_from_slice(&omega_dot_semi_circles); + + let idot_semi_circles_s = if big_endian { + self.idot_semi_circles_s.to_be_bytes() + } else { + self.idot_semi_circles_s.to_le_bytes() + }; + + buf[138..142].copy_from_slice(&idot_semi_circles_s); + + let sisa = if big_endian { + self.sisa.to_be_bytes() + } else { + self.sisa.to_le_bytes() + }; + + buf[143..147].copy_from_slice(&sisa); + + let sv_health = if big_endian { + self.sv_health.to_be_bytes() + } else { + self.sv_health.to_le_bytes() + }; + + buf[148..150].copy_from_slice(&sv_health); + + let source = if big_endian { + self.source.to_be_bytes() + } else { + self.source.to_le_bytes() + }; + + buf[151..153].copy_from_slice(&source); + Ok(154) + } + pub fn decode(big_endian: bool, buf: &[u8]) -> Result { + if buf.len() < Self::encoding_size() { + return Err(Error::NotEnoughBytes); + } + // 1. PRN + let sv_prn = buf[0]; + // 2. TOE + let toe_week = Utils::decode_u16(big_endian, &buf[1..3])?; + // 3. TOW + let tow = Utils::decode_i32(big_endian, &buf[4..8])?; + // 4. TOE(s) + let toe_s = Utils::decode_i32(big_endian, &buf[9..13])?; + // 4. TGD + let bgd_e5a_e1_s: f32 = Utils::decode_f32(big_endian, &buf[14..18])?; + let bgd_e5b_e1_s: f32 = Utils::decode_f32(big_endian, &buf[19..23])?; + // 5. IODNAV + let iodnav = Utils::decode_i32(big_endian, &buf[24..28])?; + // 6. Clock + let clock_drift_rate = Utils::decode_f32(big_endian, &buf[29..33])?; + let clock_drift = Utils::decode_f32(big_endian, &buf[34..38])?; + let clock_offset = Utils::decode_f32(big_endian, &buf[39..43])?; + // 7: delta_n + let delta_n_semi_circles_s = Utils::decode_f32(big_endian, &buf[44..48])?; + // 11: m0 + let m0_rad = Utils::decode_f64(big_endian, &buf[49..57])?; + // 12: e + let e = Utils::decode_f64(big_endian, &buf[58..66])?; + // 13: sqrt_a + let sqrt_a = Utils::decode_f64(big_endian, &buf[67..75])?; + // 14: cic + let cic = Utils::decode_f32(big_endian, &buf[76..80])?; + // 15: crc + let crc = Utils::decode_f32(big_endian, &buf[81..85])?; + // 16: cis + let cis = Utils::decode_f32(big_endian, &buf[86..90])?; + // 17: crs + let crs = Utils::decode_f32(big_endian, &buf[91..95])?; + // 18: cuc + let cuc = Utils::decode_f32(big_endian, &buf[96..100])?; + // 19: cus + let cus = Utils::decode_f32(big_endian, &buf[101..105])?; + // 20: omega0 + let omega_0_rad = Utils::decode_f64(big_endian, &buf[106..114])?; + // 21: omega + let omega_rad = Utils::decode_f64(big_endian, &buf[115..123])?; + // 22: i0 + let i0_rad = Utils::decode_f64(big_endian, &buf[124..132])?; + // 23: omega_dot + let omega_dot_semi_circles = Utils::decode_f32(big_endian, &buf[133..137])?; + // 24: idot + let idot_semi_circles_s = Utils::decode_f32(big_endian, &buf[138..142])?; + // 25: sisa + let sisa = Utils::decode_f32(big_endian, &buf[143..147])?; + // 26: sv_health + let sv_health = Utils::decode_u16(big_endian, &buf[148..150])?; + // 27: uint2 + let source = Utils::decode_u16(big_endian, &buf[151..153])?; + + Ok(Self { + sv_prn, + toe_week, + tow, + toe_s, + bgd_e5a_e1_s, + bgd_e5b_e1_s, + iodnav, + clock_drift_rate, + clock_drift, + clock_offset, + delta_n_semi_circles_s, + m0_rad, + e, + sqrt_a, + cic, + crc, + cis, + crs, + cuc, + cus, + omega_0_rad, + omega_rad, + i0_rad, + omega_dot_semi_circles, + idot_semi_circles_s, + sisa, + sv_health, + source, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn eph_x00_x04_error() { + let buf = [0; 100]; + assert!(GALEphemeris::decode(true, &buf).is_err()); + } + + #[test] + fn gal_ephemeris() { + let buf = [0; 154]; + + let eph = GALEphemeris::decode(true, &buf).unwrap(); + + // test mirror + let mut target = [0; 100]; + assert!(eph.encode(true, &mut target).is_err()); + + let mut target = [0; 154]; + let size = eph.encode(true, &mut target).unwrap(); + assert_eq!(size, 154); + assert_eq!(buf, target); + + let eph = GALEphemeris { + sv_prn: 10, + clock_offset: 123.0, + clock_drift_rate: 130.0, + clock_drift: 150.0, + sqrt_a: 56.0, + m0_rad: 0.1, + e: 0.2, + cic: 0.3, + crc: 0.4, + cis: 0.5, + crs: 0.6, + cuc: 0.7, + cus: 0.8, + omega_0_rad: 0.9, + omega_rad: 59.0, + i0_rad: 61.0, + toe_week: 112, + tow: -10, + toe_s: -32, + bgd_e5a_e1_s: -3.14, + bgd_e5b_e1_s: -6.18, + iodnav: -25, + delta_n_semi_circles_s: 150.0, + omega_dot_semi_circles: 160.0, + idot_semi_circles_s: 5000.0, + sisa: 1000.0, + sv_health: 155, + source: 156, + }; + + let mut target = [0; 154]; + eph.encode(true, &mut target).unwrap(); + + let decoded = GALEphemeris::decode(true, &target).unwrap(); + + assert_eq!(eph, decoded); + } +} diff --git a/binex/src/message/record/ephemeris/glonass.rs b/binex/src/message/record/ephemeris/glonass.rs new file mode 100644 index 00000000..7a75c90d --- /dev/null +++ b/binex/src/message/record/ephemeris/glonass.rs @@ -0,0 +1,297 @@ +//! Glonass ephemeris +use crate::{utils::Utils, Error}; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct GLOEphemeris { + pub slot: u8, + pub day: u16, + pub tod_s: u32, + pub clock_offset_s: f64, + pub clock_rel_freq_bias: f64, + pub t_k_sec: u32, + pub x_km: f64, + pub vel_x_km: f64, + pub acc_x_km: f64, + pub y_km: f64, + pub vel_y_km: f64, + pub acc_y_km: f64, + pub z_km: f64, + pub vel_z_km: f64, + pub acc_z_km: f64, + pub sv_health: u8, + pub freq_channel: i8, + pub age_op_days: u8, + pub leap_s: u8, + pub tau_gps_s: f64, + pub l1_l2_gd: f64, +} + +impl GLOEphemeris { + pub(crate) const fn encoding_size() -> usize { + 135 + } + pub fn encode(&self, big_endian: bool, buf: &mut [u8]) -> Result { + let size = Self::encoding_size(); + if buf.len() < size { + return Err(Error::NotEnoughBytes); + } + + buf[0] = self.slot; + + let day = if big_endian { + self.day.to_be_bytes() + } else { + self.day.to_le_bytes() + }; + + buf[1..3].copy_from_slice(&day); + + let tod_s = if big_endian { + self.tod_s.to_be_bytes() + } else { + self.tod_s.to_le_bytes() + }; + + buf[4..8].copy_from_slice(&tod_s); + + let clock_offset_s = if big_endian { + self.clock_offset_s.to_be_bytes() + } else { + self.clock_offset_s.to_le_bytes() + }; + + buf[9..17].copy_from_slice(&clock_offset_s); + + let clock_rel_freq_bias = if big_endian { + self.clock_rel_freq_bias.to_be_bytes() + } else { + self.clock_rel_freq_bias.to_le_bytes() + }; + + buf[18..26].copy_from_slice(&clock_rel_freq_bias); + + let t_k_sec = if big_endian { + self.t_k_sec.to_be_bytes() + } else { + self.t_k_sec.to_le_bytes() + }; + + buf[27..31].copy_from_slice(&t_k_sec); + + let x_km = if big_endian { + self.x_km.to_be_bytes() + } else { + self.x_km.to_le_bytes() + }; + + buf[32..40].copy_from_slice(&x_km); + + let vel_x_km = if big_endian { + self.vel_x_km.to_be_bytes() + } else { + self.vel_x_km.to_le_bytes() + }; + + buf[41..49].copy_from_slice(&vel_x_km); + + let acc_x_km = if big_endian { + self.acc_x_km.to_be_bytes() + } else { + self.acc_x_km.to_le_bytes() + }; + + buf[50..58].copy_from_slice(&acc_x_km); + + let y_km = if big_endian { + self.y_km.to_be_bytes() + } else { + self.y_km.to_le_bytes() + }; + + buf[59..67].copy_from_slice(&y_km); + + let vel_y_km = if big_endian { + self.vel_y_km.to_be_bytes() + } else { + self.vel_y_km.to_le_bytes() + }; + + buf[68..76].copy_from_slice(&vel_y_km); + + let acc_y_km = if big_endian { + self.acc_y_km.to_be_bytes() + } else { + self.acc_y_km.to_le_bytes() + }; + + buf[77..85].copy_from_slice(&acc_y_km); + + let z_km = if big_endian { + self.z_km.to_be_bytes() + } else { + self.z_km.to_le_bytes() + }; + + buf[86..94].copy_from_slice(&z_km); + + let vel_z_km = if big_endian { + self.vel_z_km.to_be_bytes() + } else { + self.vel_z_km.to_le_bytes() + }; + + buf[95..103].copy_from_slice(&vel_z_km); + + let acc_z_km = if big_endian { + self.acc_z_km.to_be_bytes() + } else { + self.acc_z_km.to_le_bytes() + }; + + buf[104..112].copy_from_slice(&acc_z_km); + + buf[113] = self.sv_health; + buf[114] = self.freq_channel as u8; + buf[115] = self.age_op_days; + buf[116] = self.leap_s; + + let tau_gps_s = if big_endian { + self.tau_gps_s.to_be_bytes() + } else { + self.tau_gps_s.to_le_bytes() + }; + + buf[117..125].copy_from_slice(&tau_gps_s); + + let l1_l2_gd = if big_endian { + self.l1_l2_gd.to_be_bytes() + } else { + self.l1_l2_gd.to_le_bytes() + }; + + buf[126..134].copy_from_slice(&l1_l2_gd); + + Ok(135) + } + + pub fn decode(big_endian: bool, buf: &[u8]) -> Result { + if buf.len() < Self::encoding_size() { + return Err(Error::NotEnoughBytes); + } + // 1. PRN + let slot = buf[0]; + // 2. DAY + let day = Utils::decode_u16(big_endian, &buf[1..3])?; + // 3. TOD + let tod_s = Utils::decode_u32(big_endian, &buf[4..8])?; + // 4. Clock + let clock_offset_s = Utils::decode_f64(big_endian, &buf[9..17])?; + // 4. Clock + let clock_rel_freq_bias = Utils::decode_f64(big_endian, &buf[18..26])?; + // 5. t_k + let t_k_sec = Utils::decode_u32(big_endian, &buf[27..31])?; + // 6. x + let x_km = Utils::decode_f64(big_endian, &buf[32..40])?; + let vel_x_km = Utils::decode_f64(big_endian, &buf[41..49])?; + let acc_x_km = Utils::decode_f64(big_endian, &buf[50..58])?; + // 7. y + let y_km = Utils::decode_f64(big_endian, &buf[59..67])?; + let vel_y_km = Utils::decode_f64(big_endian, &buf[68..76])?; + let acc_y_km = Utils::decode_f64(big_endian, &buf[77..85])?; + // 8. z + let z_km = Utils::decode_f64(big_endian, &buf[86..94])?; + let vel_z_km = Utils::decode_f64(big_endian, &buf[95..103])?; + let acc_z_km = Utils::decode_f64(big_endian, &buf[104..112])?; + // 9. bits + let sv_health = buf[113]; + let freq_channel = buf[114] as i8; + let age_op_days = buf[115]; + let leap_s = buf[116]; + + // 10 tau_gps_s + let tau_gps_s = Utils::decode_f64(big_endian, &buf[117..125])?; + // 11: l1/l2 gd + let l1_l2_gd = Utils::decode_f64(big_endian, &buf[126..134])?; + + Ok(Self { + slot, + day, + tod_s, + clock_offset_s, + clock_rel_freq_bias, + t_k_sec, + 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, + age_op_days, + leap_s, + tau_gps_s, + l1_l2_gd, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn eph_x00_x02_error() { + let buf = [0; 100]; + assert!(GLOEphemeris::decode(true, &buf).is_err()); + } + + #[test] + fn glo_ephemeris() { + let buf = [0; 135]; + let eph = GLOEphemeris::decode(true, &buf).unwrap(); + + // test mirror + let mut encoded = [0; 100]; + assert!(eph.encode(true, &mut encoded).is_err()); + + let mut encoded = [0; 135]; + let size = eph.encode(true, &mut encoded).unwrap(); + assert_eq!(size, 135); + assert_eq!(buf, encoded); + + let eph = GLOEphemeris { + t_k_sec: 0, + slot: 1, + day: 2, + tod_s: 3, + clock_offset_s: 1.0, + clock_rel_freq_bias: 2.0, + x_km: 3.0, + vel_x_km: 4.0, + acc_x_km: 4.0, + y_km: 5.0, + vel_y_km: 6.0, + acc_y_km: 7.0, + z_km: 8.0, + vel_z_km: 9.0, + acc_z_km: 10.0, + sv_health: 100, + freq_channel: -20, + age_op_days: 123, + leap_s: 124, + tau_gps_s: 3.14, + l1_l2_gd: 6.28, + }; + + let mut encoded = [0; 135]; + eph.encode(true, &mut encoded).unwrap(); + + let decoded = GLOEphemeris::decode(true, &encoded).unwrap(); + + assert_eq!(eph, decoded); + } +} diff --git a/binex/src/message/record/ephemeris/gps/eph.rs b/binex/src/message/record/ephemeris/gps/eph.rs new file mode 100644 index 00000000..eb57a1f7 --- /dev/null +++ b/binex/src/message/record/ephemeris/gps/eph.rs @@ -0,0 +1,445 @@ +use std::f32::consts::PI as Pi32; + +use crate::{utils::Utils, Error}; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct GPSEphemeris { + pub sv_prn: u8, + pub toe: u16, + pub tow: i32, + pub toc: i32, + pub tgd: f32, + pub iodc: i32, + /// Clock offset /bias [s] + pub clock_offset: f32, + /// Clock drift [s/s] + pub clock_drift: f32, + /// Clock drift rate [s/s²] + pub clock_drift_rate: f32, + pub iode: i32, + /// Delta n in [rad/s]. + pub delta_n_rad_s: f32, + /// Mean anomaly at reference time [rad] + pub m0_rad: f64, + /// Eccentricity + pub e: f64, + /// Square root of semi-major axis [m^1/2] + pub sqrt_a: f64, + /// cic perturbation + pub cic: f32, + /// crc perturbation + pub crc: f32, + /// cis perturbation + pub cis: f32, + /// crs perturbation + pub crs: f32, + /// cuc perturbation + pub cuc: f32, + /// cus perturbation + pub cus: f32, + /// longitude of ascending node [rad] + pub omega_0_rad: f64, + /// argument of perigee [rad] + pub omega_rad: f64, + /// inclination at reference time [rad] + pub i0_rad: f64, + /// rate of right ascention [rad/s] + pub omega_dot_rad_s: f32, + /// rate of inclination [rad/s] + pub i_dot_rad_s: f32, + /// nominal User Range Accuracy (URA) in [m] + pub ura_m: f32, + // SV health code + pub sv_health: u16, + // uint2 + pub uint2: u16, +} + +impl GPSEphemeris { + pub(crate) const fn encoding_size() -> usize { + 153 + } + pub fn encode(&self, big_endian: bool, buf: &mut [u8]) -> Result { + let size = Self::encoding_size(); + if buf.len() < size { + return Err(Error::NotEnoughBytes); + } + + buf[0] = self.sv_prn; + + let toe = if big_endian { + self.toe.to_be_bytes() + } else { + self.toe.to_le_bytes() + }; + + buf[1..3].copy_from_slice(&toe); + + let tow = if big_endian { + self.tow.to_be_bytes() + } else { + self.tow.to_le_bytes() + }; + + buf[4..8].copy_from_slice(&tow); + + let toc = if big_endian { + self.toc.to_be_bytes() + } else { + self.toc.to_le_bytes() + }; + + buf[9..13].copy_from_slice(&toc); + + let tgd = if big_endian { + self.tgd.to_be_bytes() + } else { + self.tgd.to_le_bytes() + }; + + buf[14..18].copy_from_slice(&tgd); + + let iodc = if big_endian { + self.iodc.to_be_bytes() + } else { + self.iodc.to_le_bytes() + }; + + buf[19..23].copy_from_slice(&iodc); + + let af2 = if big_endian { + self.clock_drift_rate.to_be_bytes() + } else { + self.clock_drift_rate.to_le_bytes() + }; + + buf[24..28].copy_from_slice(&af2); + + let af1 = if big_endian { + self.clock_drift.to_be_bytes() + } else { + self.clock_drift.to_le_bytes() + }; + + buf[29..33].copy_from_slice(&af1); + + let af0 = if big_endian { + self.clock_offset.to_be_bytes() + } else { + self.clock_offset.to_le_bytes() + }; + + buf[34..38].copy_from_slice(&af0); + + let iode = if big_endian { + self.iode.to_be_bytes() + } else { + self.iode.to_le_bytes() + }; + + buf[39..43].copy_from_slice(&iode); + + let delta_n = if big_endian { + (self.delta_n_rad_s / Pi32).to_be_bytes() + } else { + (self.delta_n_rad_s / Pi32).to_le_bytes() + }; + + buf[44..48].copy_from_slice(&delta_n); + + let m0 = if big_endian { + self.m0_rad.to_be_bytes() + } else { + self.m0_rad.to_le_bytes() + }; + + buf[49..57].copy_from_slice(&m0); + + let e = if big_endian { + self.e.to_be_bytes() + } else { + self.e.to_le_bytes() + }; + + buf[58..66].copy_from_slice(&e); + + let sqrt_a = if big_endian { + self.sqrt_a.to_be_bytes() + } else { + self.sqrt_a.to_le_bytes() + }; + + buf[67..75].copy_from_slice(&sqrt_a); + + let cic = if big_endian { + self.cic.to_be_bytes() + } else { + self.cic.to_le_bytes() + }; + + buf[76..80].copy_from_slice(&cic); + + let crc = if big_endian { + self.crc.to_be_bytes() + } else { + self.crc.to_le_bytes() + }; + + buf[81..85].copy_from_slice(&crc); + + let cis = if big_endian { + self.cis.to_be_bytes() + } else { + self.cis.to_le_bytes() + }; + + buf[86..90].copy_from_slice(&cis); + + let crs = if big_endian { + self.crs.to_be_bytes() + } else { + self.crs.to_le_bytes() + }; + + buf[91..95].copy_from_slice(&crs); + + let cuc = if big_endian { + self.cuc.to_be_bytes() + } else { + self.cuc.to_le_bytes() + }; + + buf[96..100].copy_from_slice(&cuc); + + let cus = if big_endian { + self.cus.to_be_bytes() + } else { + self.cus.to_le_bytes() + }; + + buf[101..105].copy_from_slice(&cus); + + let omega_0_rad = if big_endian { + self.omega_0_rad.to_be_bytes() + } else { + self.omega_0_rad.to_le_bytes() + }; + + buf[106..114].copy_from_slice(&omega_0_rad); + + let omega_rad = if big_endian { + self.omega_rad.to_be_bytes() + } else { + self.omega_rad.to_le_bytes() + }; + + buf[115..123].copy_from_slice(&omega_rad); + + let i0_rad = if big_endian { + self.i0_rad.to_be_bytes() + } else { + self.i0_rad.to_le_bytes() + }; + + buf[124..132].copy_from_slice(&i0_rad); + + let omega_dot_rad_s = if big_endian { + (self.omega_dot_rad_s / Pi32).to_be_bytes() + } else { + (self.omega_dot_rad_s / Pi32).to_le_bytes() + }; + + buf[133..137].copy_from_slice(&omega_dot_rad_s); + + let i_dot_rad_s = if big_endian { + (self.i_dot_rad_s / Pi32).to_be_bytes() + } else { + (self.i_dot_rad_s / Pi32).to_le_bytes() + }; + + buf[138..142].copy_from_slice(&i_dot_rad_s); + + let ura_m = if big_endian { + (self.ura_m / 0.1).to_be_bytes() + } else { + (self.ura_m / 0.1).to_le_bytes() + }; + + buf[143..147].copy_from_slice(&ura_m); + + let sv_health = if big_endian { + self.sv_health.to_be_bytes() + } else { + self.sv_health.to_le_bytes() + }; + + buf[148..150].copy_from_slice(&sv_health); + + let uint2 = if big_endian { + self.uint2.to_be_bytes() + } else { + self.uint2.to_le_bytes() + }; + + buf[151..153].copy_from_slice(&uint2); + + Ok(size) + } + pub fn decode(big_endian: bool, buf: &[u8]) -> Result { + if buf.len() < Self::encoding_size() { + return Err(Error::NotEnoughBytes); + } + // 1. PRN + let sv_prn = buf[0]; + // 2. TOE + let toe = Utils::decode_u16(big_endian, &buf[1..3])?; + // 3. TOW + let tow = Utils::decode_i32(big_endian, &buf[4..8])?; + // 4. TOC + let toc = Utils::decode_i32(big_endian, &buf[9..13])?; + // 4. TGD + let tgd = Utils::decode_f32(big_endian, &buf[14..18])?; + // 5. IODC + let iodc = Utils::decode_i32(big_endian, &buf[19..23])?; + // 6. Af2 + let af2 = Utils::decode_f32(big_endian, &buf[24..28])?; + // 7. Af1 + let af1 = Utils::decode_f32(big_endian, &buf[29..33])?; + // 8. Af0 + let af0 = Utils::decode_f32(big_endian, &buf[34..38])?; + // 9: IODE + let iode = Utils::decode_i32(big_endian, &buf[39..43])?; + // 10: delta_n + let delta_n_rad_s = Utils::decode_f32(big_endian, &buf[44..48])? * Pi32; + // 11: m0 + let m0_rad = Utils::decode_f64(big_endian, &buf[49..57])?; + // 12: e + let e = Utils::decode_f64(big_endian, &buf[58..66])?; + // 13: sqrt_a + let sqrt_a = Utils::decode_f64(big_endian, &buf[67..75])?; + // 14: cic + let cic = Utils::decode_f32(big_endian, &buf[76..80])?; + // 15: crc + let crc = Utils::decode_f32(big_endian, &buf[81..85])?; + // 16: cis + let cis = Utils::decode_f32(big_endian, &buf[86..90])?; + // 17: crs + let crs = Utils::decode_f32(big_endian, &buf[91..95])?; + // 18: cuc + let cuc = Utils::decode_f32(big_endian, &buf[96..100])?; + // 19: cus + let cus = Utils::decode_f32(big_endian, &buf[101..105])?; + // 20: omega0 + let omega_0_rad = Utils::decode_f64(big_endian, &buf[106..114])?; + // 21: omega + let omega_rad = Utils::decode_f64(big_endian, &buf[115..123])?; + // 22: i0 + let i0_rad = Utils::decode_f64(big_endian, &buf[124..132])?; + // 23: omega_dot + let omega_dot_rad_s = Utils::decode_f32(big_endian, &buf[133..137])? * Pi32; + // 24: idot + let i_dot_rad_s = Utils::decode_f32(big_endian, &buf[138..142])? * Pi32; + // 25: ura + let ura_m = Utils::decode_f32(big_endian, &buf[143..147])? * 0.1; + // 26: sv_health + let sv_health = Utils::decode_u16(big_endian, &buf[148..150])?; + // 27: uint2 + let uint2 = Utils::decode_u16(big_endian, &buf[151..153])?; + + Ok(Self { + sv_prn, + toe, + tow, + toc, + tgd, + iodc, + iode, + clock_offset: af0, + clock_drift: af1, + clock_drift_rate: af2, + delta_n_rad_s, + m0_rad, + e, + sqrt_a, + cic, + crc, + cis, + crs, + cuc, + cus, + omega_rad, + omega_0_rad, + i0_rad, + i_dot_rad_s, + omega_dot_rad_s, + ura_m, + sv_health, + uint2, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn eph_x00_x01_error() { + let buf = [0; 100]; + assert!(GPSEphemeris::decode(true, &buf).is_err()); + } + + #[test] + fn gps_ephemeris() { + let buf = [0; 153]; + + let eph = GPSEphemeris::decode(true, &buf).unwrap(); + + // test mirror + let mut target = [0; 100]; + assert!(eph.encode(true, &mut target).is_err()); + + let mut target = [0; 153]; + let size = eph.encode(true, &mut target).unwrap(); + assert_eq!(size, 153); + assert_eq!(buf, target); + + let eph = GPSEphemeris { + sv_prn: 10, + toe: 1000, + tow: 120, + toc: 130, + tgd: 10.0, + iodc: 24, + clock_offset: 123.0, + clock_drift_rate: 130.0, + clock_drift: 150.0, + sqrt_a: 56.0, + iode: -2000, + delta_n_rad_s: 12.0, + m0_rad: 0.1, + e: 0.2, + cic: 0.3, + crc: 0.4, + cis: 0.5, + crs: 0.6, + cuc: 0.7, + cus: 0.8, + omega_0_rad: 0.9, + omega_rad: 59.0, + i0_rad: 61.0, + omega_dot_rad_s: 62.0, + i_dot_rad_s: 74.0, + ura_m: 75.0, + sv_health: 16, + uint2: 17, + }; + + let mut target = [0; 153]; + eph.encode(true, &mut target).unwrap(); + + let decoded = GPSEphemeris::decode(true, &target).unwrap(); + + assert_eq!(eph, decoded); + } +} diff --git a/binex/src/message/record/ephemeris/gps/mod.rs b/binex/src/message/record/ephemeris/gps/mod.rs new file mode 100644 index 00000000..2e3aee7d --- /dev/null +++ b/binex/src/message/record/ephemeris/gps/mod.rs @@ -0,0 +1,5 @@ +mod eph; +mod raw; + +pub use eph::GPSEphemeris; +pub use raw::GPSRaw; diff --git a/binex/src/message/record/ephemeris/gps/raw.rs b/binex/src/message/record/ephemeris/gps/raw.rs new file mode 100644 index 00000000..afbd5d87 --- /dev/null +++ b/binex/src/message/record/ephemeris/gps/raw.rs @@ -0,0 +1,101 @@ +//! Raw GPS Ephemeris +use crate::{ + //utils::Utils, + Error, +}; + +#[derive(Debug, Clone, PartialEq)] +pub struct GPSRaw { + uint1: u8, + sint4: i32, + bytes: Vec, +} + +impl Default for GPSRaw { + fn default() -> Self { + Self { + uint1: 0, + sint4: 0, + bytes: [0; 72].to_vec(), + } + } +} + +impl GPSRaw { + /// Builds new Raw GPS Ephemeris message + pub fn new() -> Self { + Self::default() + } + pub const fn encoding_size() -> usize { + 77 + } + pub fn decode(big_endian: bool, buf: &[u8]) -> Result { + if buf.len() < Self::encoding_size() { + return Err(Error::NotEnoughBytes); + } + + let uint1 = buf[0]; + let sint4 = if big_endian { + i32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]) + } else { + i32::from_le_bytes([buf[1], buf[2], buf[3], buf[4]]) + }; + + Ok(Self { + uint1, + sint4, + bytes: buf[5..77].to_vec(), + }) + } + pub fn encode(&self, big_endian: bool, buf: &mut [u8]) -> Result { + let size = Self::encoding_size(); + if buf.len() < size { + Err(Error::NotEnoughBytes) + } else { + buf[0] = self.uint1; + + let bytes = if big_endian { + self.sint4.to_be_bytes() + } else { + self.sint4.to_le_bytes() + }; + + buf[1..5].copy_from_slice(&bytes); + buf[5..72 + 5].copy_from_slice(&self.bytes); + Ok(size) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn gps_raw() { + let big_endian = true; + + let buf = [0; 64]; + let decode = GPSRaw::decode(big_endian, &buf); + assert!(decode.is_err()); + + let mut buf = [0; 77]; + buf[0] = 10; + buf[4] = 1; + buf[5] = 123; + buf[6] = 124; + + let decoded = GPSRaw::decode(big_endian, &buf).unwrap(); + + assert_eq!(decoded.uint1, 10); + assert_eq!(decoded.sint4, 1); + assert_eq!(decoded.bytes.len(), 72); + assert_eq!(decoded.bytes[0], 123); + assert_eq!(decoded.bytes[1], 124); + + let mut encoded = [0; 77]; + let size = decoded.encode(big_endian, &mut encoded).unwrap(); + + assert_eq!(size, 77); + assert_eq!(buf, encoded) + } +} diff --git a/binex/src/message/record/ephemeris/mod.rs b/binex/src/message/record/ephemeris/mod.rs new file mode 100644 index 00000000..6d788759 --- /dev/null +++ b/binex/src/message/record/ephemeris/mod.rs @@ -0,0 +1,170 @@ +//! Raw, Decoded, Modern Ephemeris and ionosphere models +use crate::{message::Message, Error}; + +mod fid; +use fid::FieldID; + +mod gps; +pub use gps::{GPSEphemeris, GPSRaw}; + +mod glonass; +pub use glonass::GLOEphemeris; + +mod sbas; +pub use sbas::SBASEphemeris; + +mod galileo; +pub use galileo::GALEphemeris; + +/// [EphemerisFrame] may describe raw, decoded GNSS +/// Ephemeris or Ionosphere model parameters. +#[derive(Debug, Clone, PartialEq)] +pub enum EphemerisFrame { + /// Raw (encoded) GPS frame as is. + /// It did not go through a decoding & interpretation process. + GPSRaw(GPSRaw), + /// Decoded GPS Ephemeris + GPS(GPSEphemeris), + /// Decoded Glonass Ephemeris + GLO(GLOEphemeris), + /// Decoded SBAS Ephemeris + SBAS(SBASEphemeris), + /// Decoded Galileo Ephemeris + GAL(GALEphemeris), +} + +impl EphemerisFrame { + /// Returns total length (bytewise) required to fully encode [Self]. + /// Use this to fulfill [Self::encode] requirements. + pub fn encoding_size(&self) -> usize { + match self { + Self::GPSRaw(_) => GPSRaw::encoding_size(), + Self::GPS(_) => GPSEphemeris::encoding_size(), + Self::GLO(_) => GLOEphemeris::encoding_size(), + Self::SBAS(_) => SBASEphemeris::encoding_size(), + Self::GAL(_) => GALEphemeris::encoding_size(), + } + } + + /// Returns expected [FieldID] for [Self] + pub(crate) fn to_field_id(&self) -> FieldID { + match self { + Self::GPS(_) => FieldID::GPS, + Self::GLO(_) => FieldID::GLO, + Self::SBAS(_) => FieldID::SBAS, + Self::GAL(_) => FieldID::GAL, + Self::GPSRaw(_) => FieldID::GPSRaw, + } + } + + /// [EphemerisFrame] decoding attempt from given [FieldID] + pub(crate) fn decode(big_endian: bool, buf: &[u8]) -> Result { + // cant decode 1-4b + if buf.len() < 1 { + return Err(Error::NotEnoughBytes); + } + + // decode FID + let (bnxi, size) = Message::decode_bnxi(&buf, big_endian); + let fid = FieldID::from(bnxi); + println!("bnx01-eph fid={:?}", fid); + + match fid { + FieldID::GPSRaw => { + let fr = GPSRaw::decode(big_endian, &buf[size..])?; + Ok(Self::GPSRaw(fr)) + }, + FieldID::GPS => { + let fr = GPSEphemeris::decode(big_endian, &buf[size..])?; + Ok(Self::GPS(fr)) + }, + _ => Err(Error::UnknownRecordFieldId), + } + } + + /// Encodes [Self] into buffer, returns encoded size (total bytes). + /// [Self] must fit in preallocated buffer. + pub fn encode(&self, big_endian: bool, buf: &mut [u8]) -> Result { + if buf.len() < self.encoding_size() { + return Err(Error::NotEnoughBytes); + } + + // encode FID + let fid = self.to_field_id() as u32; + let offset = Message::encode_bnxi(fid, big_endian, buf)?; + + match self { + Self::GPSRaw(r) => r.encode(big_endian, &mut buf[offset..]), + Self::GPS(r) => r.encode(big_endian, &mut buf[offset..]), + Self::GLO(r) => r.encode(big_endian, &mut buf[offset..]), + Self::GAL(r) => r.encode(big_endian, &mut buf[offset..]), + Self::SBAS(r) => r.encode(big_endian, &mut buf[offset..]), + } + } + + /// Creates new [GPSRaw] frame + pub fn new_gps_raw(&self, raw: GPSRaw) -> Self { + Self::GPSRaw(raw) + } + + /// Creates new [GPSEphemeris] frame + pub fn new_gps(&self, gps: GPSEphemeris) -> Self { + Self::GPS(gps) + } + + /// Creates new [GLOEphemeris] frame + pub fn new_glonass(&self, glo: GLOEphemeris) -> Self { + Self::GLO(glo) + } + + /// Creates new [SBASEphemeris] frame + pub fn new_sbas(&self, sbas: SBASEphemeris) -> Self { + Self::SBAS(sbas) + } + + /// Creates new [GALEphemeris] frame + pub fn new_galileo(&self, gal: GALEphemeris) -> Self { + Self::GAL(gal) + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn gps_raw() { + let mut eph = GPSEphemeris::default(); + eph.sv_prn = 10; + eph.cic = 10.0; + eph.cus = 12.0; + eph.m0_rad = 100.0; + eph.clock_offset = 123.0; + + assert_eq!(GPSEphemeris::encoding_size(), 153); + + let big_endian = true; + + let mut encoded = [0; 77]; + assert!(eph.encode(big_endian, &mut encoded).is_err()); + + let mut encoded = [0; 153]; + let size = eph.encode(big_endian, &mut encoded).unwrap(); + assert_eq!(size, 153); + + let decoded = GPSEphemeris::decode(big_endian, &encoded).unwrap(); + + assert_eq!(decoded, eph); + } + #[test] + fn gps_eph() { + let raw: GPSRaw = GPSRaw::new(); + assert_eq!(GPSRaw::encoding_size(), 1 + 4 + 72); + + let big_endian = true; + let mut buf = [0; 77]; + let size = raw.encode(big_endian, &mut buf).unwrap(); + + assert_eq!(size, GPSRaw::encoding_size()); + assert_eq!(buf, [0; 77]); + } +} diff --git a/binex/src/message/record/ephemeris/sbas.rs b/binex/src/message/record/ephemeris/sbas.rs new file mode 100644 index 00000000..eb805454 --- /dev/null +++ b/binex/src/message/record/ephemeris/sbas.rs @@ -0,0 +1,253 @@ +//! SBAS ephemeris +use crate::{utils::Utils, Error}; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct SBASEphemeris { + pub sbas_prn: u8, + pub toe: u16, + pub tow: i32, + /// Clock offset /bias [s] + pub clock_offset: f64, + /// Clock drift [s/s] + pub clock_drift: f64, + pub x_km: f64, + pub vel_x_km: f64, + pub acc_x_km: f64, + pub y_km: f64, + pub vel_y_km: f64, + pub acc_y_km: f64, + pub z_km: f64, + pub vel_z_km: f64, + pub acc_z_km: f64, + pub uint1: u8, + pub ura: u8, + pub iodn: u8, +} + +impl SBASEphemeris { + pub(crate) const fn encoding_size() -> usize { + 111 + } + pub fn encode(&self, big_endian: bool, buf: &mut [u8]) -> Result { + let size = Self::encoding_size(); + if buf.len() < size { + return Err(Error::NotEnoughBytes); + } + + buf[0] = self.sbas_prn; + + let toe = if big_endian { + self.toe.to_be_bytes() + } else { + self.toe.to_le_bytes() + }; + + buf[1..3].copy_from_slice(&toe); + + let tow = if big_endian { + self.tow.to_be_bytes() + } else { + self.tow.to_le_bytes() + }; + + buf[4..8].copy_from_slice(&tow); + + let clock_offset = if big_endian { + self.clock_offset.to_be_bytes() + } else { + self.clock_offset.to_le_bytes() + }; + + buf[9..17].copy_from_slice(&clock_offset); + + let clock_drift = if big_endian { + self.clock_drift.to_be_bytes() + } else { + self.clock_drift.to_le_bytes() + }; + + buf[18..26].copy_from_slice(&clock_drift); + + let x_km = if big_endian { + self.x_km.to_be_bytes() + } else { + self.x_km.to_le_bytes() + }; + + buf[27..35].copy_from_slice(&x_km); + + let vel_x_km = if big_endian { + self.vel_x_km.to_be_bytes() + } else { + self.vel_x_km.to_le_bytes() + }; + + buf[36..44].copy_from_slice(&vel_x_km); + + let acc_x_km = if big_endian { + self.acc_x_km.to_be_bytes() + } else { + self.acc_x_km.to_le_bytes() + }; + + buf[45..53].copy_from_slice(&acc_x_km); + + let y_km = if big_endian { + self.y_km.to_be_bytes() + } else { + self.y_km.to_le_bytes() + }; + + buf[54..62].copy_from_slice(&y_km); + + let vel_y_km = if big_endian { + self.vel_y_km.to_be_bytes() + } else { + self.vel_y_km.to_le_bytes() + }; + + buf[63..71].copy_from_slice(&vel_y_km); + + let acc_y_km = if big_endian { + self.acc_y_km.to_be_bytes() + } else { + self.acc_y_km.to_le_bytes() + }; + + buf[72..80].copy_from_slice(&acc_y_km); + + let z_km = if big_endian { + self.z_km.to_be_bytes() + } else { + self.z_km.to_le_bytes() + }; + + buf[81..89].copy_from_slice(&z_km); + + let vel_z_km = if big_endian { + self.vel_z_km.to_be_bytes() + } else { + self.vel_z_km.to_le_bytes() + }; + + buf[90..98].copy_from_slice(&vel_z_km); + + let acc_z_km = if big_endian { + self.acc_z_km.to_be_bytes() + } else { + self.acc_z_km.to_le_bytes() + }; + + buf[99..107].copy_from_slice(&acc_z_km); + + buf[108] = self.uint1; + buf[109] = self.ura; + buf[110] = self.iodn; + + Ok(111) + } + pub fn decode(big_endian: bool, buf: &[u8]) -> Result { + if buf.len() < Self::encoding_size() { + return Err(Error::NotEnoughBytes); + } + // 1. PRN + let sbas_prn = buf[0]; + // 2. TOE + let toe = Utils::decode_u16(big_endian, &buf[1..3])?; + // 3. TOW + let tow = Utils::decode_i32(big_endian, &buf[4..8])?; + // 4. Clock + let clock_offset = Utils::decode_f64(big_endian, &buf[9..17])?; + let clock_drift = Utils::decode_f64(big_endian, &buf[18..26])?; + // 5. x + let x_km = Utils::decode_f64(big_endian, &buf[27..35])?; + let vel_x_km = Utils::decode_f64(big_endian, &buf[36..44])?; + let acc_x_km = Utils::decode_f64(big_endian, &buf[45..53])?; + // 6: y + let y_km = Utils::decode_f64(big_endian, &buf[54..62])?; + let vel_y_km = Utils::decode_f64(big_endian, &buf[63..71])?; + let acc_y_km = Utils::decode_f64(big_endian, &buf[72..80])?; + // 6: z + let z_km = Utils::decode_f64(big_endian, &buf[81..89])?; + let vel_z_km = Utils::decode_f64(big_endian, &buf[90..98])?; + let acc_z_km = Utils::decode_f64(big_endian, &buf[99..107])?; + // 7: bits + let uint1 = buf[108]; + let ura = buf[109]; + let iodn = buf[110]; + + Ok(Self { + sbas_prn, + toe, + tow, + 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, + ura, + iodn, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn eph_x00_x03_error() { + let buf = [0; 100]; + assert!(SBASEphemeris::decode(true, &buf).is_err()); + } + + #[test] + fn sbas_ephemeris() { + let buf = [0; 111]; + + let eph = SBASEphemeris::decode(true, &buf).unwrap(); + + // test mirror + let mut target = [0; 100]; + assert!(eph.encode(true, &mut target).is_err()); + + let mut target = [0; 111]; + let size = eph.encode(true, &mut target).unwrap(); + assert_eq!(size, 111); + assert_eq!(buf, target); + + let eph = SBASEphemeris { + sbas_prn: 10, + toe: 11, + tow: 12, + clock_drift: 0.1, + clock_offset: 0.2, + x_km: 1.4, + vel_x_km: 1.5, + acc_x_km: 1.6, + y_km: 2.4, + vel_y_km: 2.5, + acc_y_km: 2.6, + z_km: 3.1, + vel_z_km: 3.2, + acc_z_km: 3.3, + uint1: 4, + ura: 5, + iodn: 6, + }; + + let mut target = [0; 111]; + eph.encode(true, &mut target).unwrap(); + + let decoded = SBASEphemeris::decode(true, &target).unwrap(); + + assert_eq!(eph, decoded); + } +} diff --git a/binex/src/message/record/mod.rs b/binex/src/message/record/mod.rs new file mode 100644 index 00000000..0f886089 --- /dev/null +++ b/binex/src/message/record/mod.rs @@ -0,0 +1,64 @@ +//! Record: Message content + +use crate::message::MessageID; + +mod ephemeris; // ephemeris frames +mod monument; // geodetic marker // ephemeris frames + +pub use ephemeris::{EphemerisFrame, GPSEphemeris, GPSRaw}; +pub use monument::{MonumentGeoMetadata, MonumentGeoRecord}; + +#[derive(Debug, Clone, PartialEq)] +pub enum Record { + /// Geodetic Marker, Site and Reference point information. + /// Includes Geodetic metadata. + MonumentGeo(MonumentGeoRecord), + /// Ephemeris Frame + EphemerisFrame(EphemerisFrame), +} + +impl Default for Record { + fn default() -> Self { + Self::MonumentGeo(Default::default()) + } +} + +impl Record { + /// Builds new [MonumentGeoRecord] + pub fn new_monument_geo(r: MonumentGeoRecord) -> Self { + Self::MonumentGeo(r) + } + /// Builds new [EphemerisFrame] + pub fn new_ephemeris_frame(fr: EphemerisFrame) -> Self { + Self::EphemerisFrame(fr) + } + /// [MonumentGeoRecord] unwrapping attempt + pub fn as_monument_geo(&self) -> Option<&MonumentGeoRecord> { + match self { + Self::MonumentGeo(r) => Some(r), + _ => None, + } + } + /// [EphemerisFrame] unwrapping attempt + pub fn as_ephemeris(&self) -> Option<&EphemerisFrame> { + match self { + Self::EphemerisFrame(fr) => Some(fr), + _ => None, + } + } + /// Returns [MessageID] to associate to [Self] in stream header. + pub(crate) fn to_message_id(&self) -> MessageID { + match self { + Self::EphemerisFrame(_) => MessageID::Ephemeris, + Self::MonumentGeo(_) => MessageID::SiteMonumentMarker, + } + } + + /// Returns internal encoding size + pub(crate) fn encoding_size(&self) -> usize { + match self { + Self::EphemerisFrame(fr) => fr.encoding_size(), + Self::MonumentGeo(geo) => geo.encoding_size(), + } + } +} diff --git a/binex/src/message/record/monument/fid.rs b/binex/src/message/record/monument/fid.rs new file mode 100644 index 00000000..1af0c99a --- /dev/null +++ b/binex/src/message/record/monument/fid.rs @@ -0,0 +1,208 @@ +//! Monument / Geodetic Field ID + +/// [FieldID] describes the content to follow +/// in Geodetic marker frames +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum FieldID { + /// Comment: simple comment (readable string) + /// about the Geodetic marker. Several RINEX comments + /// are described by several BINEX Geodetic comments (repeated frames). + Comment = 0, + /// Software (=Program) name used in the creation of this BINEX Geodetic Record. + /// Must be unique in any BINEX Geodetic Record. Field length (bytewise) must follow + SoftwareName = 1, + /// Operator (=RunBy) name who created this BINEX Geodetic Record. + /// Must be unique, requires field length (bytewise) + OperatorName = 2, + /// Country / State / Province / City location of the data producer. + /// Must be unique, requires field length (bytewise). + SiteLocation = 3, + /// Site Agency name (=MARKER NAME). + /// Must be unique, requires field length (bytewise). + SiteName = 4, + /// Site Agency number. + /// Must be unique, requires field length (bytewise). + SiteNumber = 5, + /// Monument name and description. + /// Must be unique, requires field length (bytewise). + MonumentName = 6, + /// Monument number (=MARKER NUMBER). + /// Must be unique, requires field length (bytewise). + MonumentNumber = 7, + /// Marker name and description. + /// Must be unique, requires field length (bytewise). + MarkerName = 8, + /// Marker number (=MARKER NUMBER). + /// Must be unique, requires field length (bytewise). + MarkerNumber = 9, + /// Name for the Reference Coordinates. + /// Must be unique, requires field length (bytewise). + ReferenceName = 10, + /// Official Number (=DOMES) (=MARKER NUMBER) for the Reference Coordinates. + /// Must be unique, requires field length (bytewise). + ReferenceNumber = 11, + /// Date of the coordinates determination and marker installation. + /// Must be unique. Follows: + /// * number of ascii bytes + /// * ascii date description + /// * year (sint2) + /// * minutes into year (uint4) + /// Must be unique. + ReferenceDate = 12, + /// Site geologic / geophyiscal characteristics + /// (for example: tectonic plate of this site). + Geophysical = 13, + /// Climatic (=gross meteorological) local profile. + Climatic = 14, + /// Custom User defined 4 character ID associated to this + /// data & metadata. Must always be 4 byte long (fill with space). + /// Must be unique. + UserID = 15, + /// Project Name / description. Must be unique. + ProjectName = 16, + /// Observer (=OBSERVER), sometimes refered to as "Investigator". + /// Several entities or people can be described: repeat as need be. + ObserverName = 17, + /// Agency Name (entity/employer) (=OBSERVER AGENCY). + /// Must be unique. + AgencyName = 18, + /// Observer Contact. Repeat as need be. + ObserverContact = 19, + /// Site Operator (=OBSERVER). Must be unique. + SiteOperator = 20, + /// Site Operator Agency (=OBSERVER AGENCY). Must be unique. + SiteOperatorAgency = 21, + /// Site Operator Contact. Must be unique. + SiteOperatorContact = 22, + /// Antenna Type (=ANTENNA TYPE). Must be unique. + AntennaType = 23, + /// Antenna Number (=ANTENNA #). Must be unique + AntennaNumber = 24, + /// Receiver Type (=RECEIVER TYPE). Must be unique. + ReceiverType = 25, + /// Receiver Number (=RECEIVER #). Must be unique. + ReceiverNumber = 26, + /// Receiver Firmware Version (=RECEIVER VERS). Must be unique. + ReceiverFirmwareVersion = 27, + /// Antenna mount description. Must be unique. + AntennaMount = 28, + /// Antenna ECEF X/Y/Z coordinates (=APPROX POSITION XYZ), follows: + /// * ubnxi number of bytes in ECEF/ellipsoid model (may be 0) + /// * ECEF/ellipsoid model description. (When 0: WGS84 is assumed). + /// * ECEF(x) [m] (real8) + /// * ECEF(y) [m] (real8) + /// * ECEF(z) [m] (real8) + /// Must be unique + AntennaEcef3D = 29, + /// Antenna Geographic Position (Geo. Coordinates). Follows: + /// * ubnxi number of bytes in ECEF/ellipsoid model (may be 0) + /// * ECEF/ellipsoid model description. (When 0: WGS84 assumed) + /// * East/West longitude [ddeg] (real8) + /// * North/South latitude [ddeg] (real8) + /// * Elevation [m] (real8) + /// Must be unique + AntennaGeo3D = 30, + /// Antenna offset from reference point (= ANTENNA DELTA H/E/N). Follows: + /// * Height offset [m] (real8) + /// * East/West offset [m] (real8) + /// * North/South offset [m] (real8) + AntennaOffset3D = 31, + /// Antenna Radome Type (=TYPE). Must be unique. + AntennaRadomeType = 32, + /// Antenna Radom Number. Must be unique. + AntennaRadomeNumber = 33, + /// Geocode. Must be unique. + Geocode = 34, + /// Extra / Additional information, very similar to [Self::Comment] + Extra = 127, + /// Unknown / Invalid + Unknown = 0xffffffff, +} + +impl From for FieldID { + fn from(val: u32) -> Self { + match val { + 0 => Self::Comment, + 1 => Self::SoftwareName, + 2 => Self::OperatorName, + 3 => Self::SiteLocation, + 4 => Self::SiteName, + 5 => Self::SiteNumber, + 6 => Self::MonumentName, + 7 => Self::MonumentNumber, + 8 => Self::MarkerName, + 9 => Self::MarkerNumber, + 10 => Self::ReferenceName, + 11 => Self::ReferenceNumber, + 12 => Self::ReferenceDate, + 13 => Self::Geophysical, + 14 => Self::Climatic, + 15 => Self::UserID, + 16 => Self::ProjectName, + 17 => Self::ObserverName, + 18 => Self::AgencyName, + 19 => Self::ObserverContact, + 20 => Self::SiteOperator, + 21 => Self::SiteOperatorAgency, + 22 => Self::SiteOperatorContact, + 23 => Self::AntennaType, + 24 => Self::AntennaNumber, + 25 => Self::ReceiverType, + 26 => Self::ReceiverNumber, + 27 => Self::ReceiverFirmwareVersion, + 28 => Self::AntennaMount, + 29 => Self::AntennaEcef3D, + 30 => Self::AntennaGeo3D, + 31 => Self::AntennaOffset3D, + 32 => Self::AntennaRadomeType, + 33 => Self::AntennaRadomeNumber, + 34 => Self::Geocode, + 127 => Self::Extra, + _ => Self::Unknown, + } + } +} + +impl From for u32 { + fn from(val: FieldID) -> u32 { + match val { + FieldID::Comment => 0, + FieldID::SoftwareName => 1, + FieldID::OperatorName => 2, + FieldID::SiteLocation => 3, + FieldID::SiteName => 4, + FieldID::SiteNumber => 5, + FieldID::MonumentName => 6, + FieldID::MonumentNumber => 7, + FieldID::MarkerName => 8, + FieldID::MarkerNumber => 9, + FieldID::ReferenceName => 10, + FieldID::ReferenceNumber => 11, + FieldID::ReferenceDate => 12, + FieldID::Geophysical => 13, + FieldID::Climatic => 14, + FieldID::UserID => 15, + FieldID::ProjectName => 16, + FieldID::ObserverName => 17, + FieldID::AgencyName => 18, + FieldID::ObserverContact => 19, + FieldID::SiteOperator => 20, + FieldID::SiteOperatorAgency => 21, + FieldID::SiteOperatorContact => 22, + FieldID::AntennaType => 23, + FieldID::AntennaNumber => 24, + FieldID::ReceiverType => 25, + FieldID::ReceiverNumber => 26, + FieldID::ReceiverFirmwareVersion => 27, + FieldID::AntennaMount => 28, + FieldID::AntennaEcef3D => 29, + FieldID::AntennaGeo3D => 30, + FieldID::AntennaOffset3D => 31, + FieldID::AntennaRadomeType => 32, + FieldID::AntennaRadomeNumber => 33, + FieldID::Geocode => 34, + FieldID::Extra => 127, + FieldID::Unknown => 0xffffffff, + } + } +} diff --git a/binex/src/message/record/monument/frame.rs b/binex/src/message/record/monument/frame.rs new file mode 100644 index 00000000..204842ce --- /dev/null +++ b/binex/src/message/record/monument/frame.rs @@ -0,0 +1,363 @@ +//! Monument Geodetic marker specific frames + +use crate::{ + message::{record::monument::FieldID, Message}, + Error, +}; + +// use log::error; + +#[derive(Debug, Clone, PartialEq)] +pub enum MonumentGeoFrame { + /// Comment + Comment(String), + /// Software (Program) name + SoftwareName(String), + /// Agency Name + AgencyName(String), + /// Name of person or entity operating [MonumentGeoFrame::SoftwareName] + /// employed by [MonumentGeoFrame::AgencyName]. + OperatorName(String), + /// Site Location + SiteLocation(String), + /// Site Number + SiteNumber(String), + /// Site name + SiteName(String), + /// Site Operator + SiteOperator(String), + /// Site Operator Contact + SiteOperatorContact(String), + /// Site Operator Agency + SiteOperatorAgency(String), + /// Observer Name + ObserverName(String), + /// Observer Contact + ObserverContact(String), + /// Geodetic Marker Name + MarkerName(String), + /// Geodetic Monument Name + MonumentName(String), + /// Geodetic Monument Number + MonumentNumber(String), + /// Geodetic Marker Number (DOMES) + MarkerNumber(String), + /// Project Name + ProjectName(String), + /// Reference Name + ReferenceName(String), + /// Reference Date + ReferenceDate(String), + /// Reference Number + ReferenceNumber(String), + /// Local meteorological model/information at site location + Climatic(String), + /// Geophysical information at site location (like tectonic plate) + Geophysical(String), + /// Antenna Type + AntennaType(String), + /// Antenna Radome Type + AntennaRadomeType(String), + /// Antenna Mount information + AntennaMount(String), + /// Antenna Number + AntennaNumber(String), + /// Antenna Radome Number + AntennaRadomeNumber(String), + /// Receiver Firmware Version + ReceiverFirmwareVersion(String), + /// Receiver Type + ReceiverType(String), + /// Receiver (Serial) Number + ReceiverNumber(String), + /// User defined ID + UserID(String), + /// Extra information about production site + Extra(String), +} + +impl MonumentGeoFrame { + /// Returns total length (bytewise) required to fully encode [Self]. + /// Use this to fulfill [Self::encode] requirements. + pub(crate) fn encoding_size(&self) -> usize { + match self { + Self::Comment(s) + | Self::ReferenceDate(s) + | Self::ReferenceName(s) + | Self::ReferenceNumber(s) + | Self::SiteNumber(s) + | Self::SiteOperator(s) + | Self::SiteOperatorAgency(s) + | Self::SiteOperatorContact(s) + | Self::Extra(s) + | Self::SiteLocation(s) + | Self::SiteName(s) + | Self::ReceiverFirmwareVersion(s) + | Self::ReceiverNumber(s) + | Self::ReceiverType(s) + | Self::ObserverContact(s) + | Self::ObserverName(s) + | Self::MonumentName(s) + | Self::MonumentNumber(s) + | Self::ProjectName(s) + | Self::MarkerName(s) + | Self::MarkerNumber(s) + | Self::SoftwareName(s) + | Self::Geophysical(s) + | Self::Climatic(s) + | Self::AntennaType(s) + | Self::AntennaMount(s) + | Self::AntennaRadomeType(s) + | Self::AntennaRadomeNumber(s) + | Self::AntennaNumber(s) + | Self::OperatorName(s) + | Self::UserID(s) + | Self::AgencyName(s) => { + let s_len = s.len(); + s_len + 1 + Message::bnxi_encoding_size(s_len as u32) // FID + }, + } + } + + /// Returns expected [FieldID] for [Self] + pub(crate) fn to_field_id(&self) -> FieldID { + match self { + Self::Comment(_) => FieldID::Comment, + Self::OperatorName(_) => FieldID::OperatorName, + Self::SiteLocation(_) => FieldID::SiteLocation, + Self::SiteOperator(_) => FieldID::SiteOperator, + Self::SiteOperatorAgency(_) => FieldID::SiteOperatorAgency, + Self::SiteOperatorContact(_) => FieldID::SiteOperatorContact, + Self::SiteName(_) => FieldID::SiteName, + Self::MonumentName(_) => FieldID::MonumentName, + Self::MonumentNumber(_) => FieldID::MonumentNumber, + Self::ProjectName(_) => FieldID::ProjectName, + Self::MarkerName(_) => FieldID::MarkerName, + Self::MarkerNumber(_) => FieldID::MarkerNumber, + Self::ObserverContact(_) => FieldID::ObserverContact, + Self::ObserverName(_) => FieldID::ObserverName, + Self::Extra(_) => FieldID::Extra, + Self::UserID(_) => FieldID::UserID, + Self::Climatic(_) => FieldID::Climatic, + Self::Geophysical(_) => FieldID::Geophysical, + Self::SoftwareName(_) => FieldID::SoftwareName, + Self::AgencyName(_) => FieldID::AgencyName, + Self::AntennaType(_) => FieldID::AntennaType, + Self::AntennaMount(_) => FieldID::AntennaMount, + Self::AntennaNumber(_) => FieldID::AntennaNumber, + Self::AntennaRadomeType(_) => FieldID::AntennaRadomeType, + Self::AntennaRadomeNumber(_) => FieldID::AntennaRadomeNumber, + Self::ReceiverFirmwareVersion(_) => FieldID::ReceiverFirmwareVersion, + Self::ReceiverNumber(_) => FieldID::ReceiverNumber, + Self::ReceiverType(_) => FieldID::ReceiverType, + Self::SiteNumber(_) => FieldID::SiteNumber, + Self::ReferenceDate(_) => FieldID::ReferenceDate, + Self::ReferenceName(_) => FieldID::ReferenceName, + Self::ReferenceNumber(_) => FieldID::ReferenceNumber, + } + } + + /// [MonumentGeoFrame] decoding attempt from given [FieldID] + pub(crate) fn decode(big_endian: bool, buf: &[u8]) -> Result { + if buf.len() < 2 { + // smallest size + return Err(Error::NotEnoughBytes); + } + + // decode FID + let (fid, mut ptr) = Message::decode_bnxi(&buf, big_endian); + let fid = FieldID::from(fid); + + match fid { + FieldID::Comment + | FieldID::AntennaNumber + | FieldID::AntennaType + | FieldID::AntennaMount + | FieldID::AntennaRadomeNumber + | FieldID::AntennaRadomeType + | FieldID::AgencyName + | FieldID::Climatic + | FieldID::Geophysical + | FieldID::MonumentName + | FieldID::MonumentNumber + | FieldID::MarkerName + | FieldID::MarkerNumber + | FieldID::ObserverContact + | FieldID::ObserverName + | FieldID::ProjectName + | FieldID::SiteLocation + | FieldID::ReceiverFirmwareVersion + | FieldID::ReceiverType + | FieldID::ReceiverNumber + | FieldID::Extra => { + // can't decode 1-4b + if buf.len() < 1 + ptr { + return Err(Error::NotEnoughBytes); + } + + // decode slen + let (s_len, size) = Message::decode_bnxi(&buf[ptr..], big_endian); + let s_len = s_len as usize; + ptr += size; + + if buf.len() - ptr < s_len { + return Err(Error::NotEnoughBytes); // can't parse entire string + } + + match std::str::from_utf8(&buf[ptr..ptr + s_len]) { + Ok(s) => match fid { + FieldID::Comment => Ok(Self::Comment(s.to_string())), + FieldID::MonumentName => Ok(Self::MonumentName(s.to_string())), + FieldID::MonumentNumber => Ok(Self::MonumentNumber(s.to_string())), + FieldID::ProjectName => Ok(Self::ProjectName(s.to_string())), + FieldID::ObserverName => Ok(Self::ObserverName(s.to_string())), + FieldID::ObserverContact => Ok(Self::ObserverContact(s.to_string())), + FieldID::SoftwareName => Ok(Self::SoftwareName(s.to_string())), + FieldID::MarkerName => Ok(Self::MarkerName(s.to_string())), + FieldID::MarkerNumber => Ok(Self::MarkerNumber(s.to_string())), + FieldID::Extra => Ok(Self::Extra(s.to_string())), + FieldID::Climatic => Ok(Self::Climatic(s.to_string())), + FieldID::Geophysical => Ok(Self::Geophysical(s.to_string())), + FieldID::AgencyName => Ok(Self::AgencyName(s.to_string())), + FieldID::AntennaType => Ok(Self::AntennaType(s.to_string())), + FieldID::AntennaMount => Ok(Self::AntennaMount(s.to_string())), + FieldID::AntennaNumber => Ok(Self::AntennaNumber(s.to_string())), + FieldID::AntennaRadomeType => Ok(Self::AntennaRadomeType(s.to_string())), + FieldID::AntennaRadomeNumber => { + Ok(Self::AntennaRadomeNumber(s.to_string())) + }, + FieldID::ReceiverFirmwareVersion => { + Ok(Self::ReceiverFirmwareVersion(s.to_string())) + }, + FieldID::ReceiverNumber => Ok(Self::ReceiverNumber(s.to_string())), + FieldID::ReceiverType => Ok(Self::ReceiverType(s.to_string())), + FieldID::OperatorName => Ok(Self::OperatorName(s.to_string())), + FieldID::SiteLocation => Ok(Self::SiteLocation(s.to_string())), + FieldID::SiteName => Ok(Self::SiteName(s.to_string())), + FieldID::SiteNumber => Ok(Self::SiteNumber(s.to_string())), + FieldID::ReferenceDate => Ok(Self::ReferenceDate(s.to_string())), + FieldID::ReferenceName => Ok(Self::ReferenceName(s.to_string())), + FieldID::ReferenceNumber => Ok(Self::ReferenceNumber(s.to_string())), + FieldID::UserID => Ok(Self::UserID(s.to_string())), + FieldID::SiteOperator => Ok(Self::SiteOperator(s.to_string())), + FieldID::SiteOperatorAgency => Ok(Self::SiteOperatorAgency(s.to_string())), + FieldID::SiteOperatorContact => { + Ok(Self::SiteOperatorContact(s.to_string())) + }, + // TODO + FieldID::AntennaEcef3D + | FieldID::Geocode + | FieldID::AntennaOffset3D + | FieldID::AntennaGeo3D + | FieldID::Unknown => Err(Error::UnknownMessage), + }, + Err(e) => { + println!("bnx00-str: utf8 error {}", e); + Err(Error::Utf8Error) + }, + } + }, + _ => Err(Error::UnknownRecordFieldId), + } + } + + /// Encodes [Self] into buffer, returns encoded size (total bytes). + /// [Self] must fit in preallocated buffer. + pub fn encode(&self, big_endian: bool, buf: &mut [u8]) -> Result { + let size = self.encoding_size(); + if buf.len() < size { + return Err(Error::NotEnoughBytes); + } + + // encode FID + let fid = self.to_field_id() as u32; + let mut ptr = Message::encode_bnxi(fid, big_endian, buf)?; + + match self { + Self::Comment(s) + | Self::UserID(s) + | Self::SiteOperator(s) + | Self::SiteOperatorAgency(s) + | Self::OperatorName(s) + | Self::SiteLocation(s) + | Self::SiteOperatorContact(s) + | Self::SiteNumber(s) + | Self::ObserverName(s) + | Self::ProjectName(s) + | Self::ReferenceName(s) + | Self::MonumentNumber(s) + | Self::ReferenceDate(s) + | Self::ReferenceNumber(s) + | Self::ObserverContact(s) + | Self::MonumentName(s) + | Self::SiteName(s) + | Self::Extra(s) + | Self::SoftwareName(s) + | Self::Climatic(s) + | Self::Geophysical(s) + | Self::AgencyName(s) + | Self::MarkerName(s) + | Self::MarkerNumber(s) + | Self::ReceiverFirmwareVersion(s) + | Self::ReceiverNumber(s) + | Self::ReceiverType(s) + | Self::AntennaType(s) + | Self::AntennaNumber(s) + | Self::AntennaMount(s) + | Self::AntennaRadomeType(s) + | Self::AntennaRadomeNumber(s) => { + // encode strlen + let s_len = s.len(); + let size = Message::encode_bnxi(s_len as u32, big_endian, &mut buf[ptr..])?; + ptr += size; + + buf[ptr..ptr + s_len].clone_from_slice(s.as_bytes()); // utf8 encoding + }, + } + + Ok(size) + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn geo_comments() { + let frame = MonumentGeoFrame::Comment("Hello".to_string()); + assert_eq!(frame.encoding_size(), 5 + 2); + + let big_endian = true; + let mut buf = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let size = frame.encode(big_endian, &mut buf).unwrap(); + + assert_eq!(size, frame.encoding_size()); + assert_eq!( + buf, + [0, 5, 'H' as u8, 'e' as u8, 'l' as u8, 'l' as u8, 'o' as u8, 0, 0, 0, 0, 0, 0] + ); + + let decoded = MonumentGeoFrame::decode(big_endian, &buf).unwrap(); + + assert_eq!(decoded, frame); + } + #[test] + fn geo_climatic() { + let frame = MonumentGeoFrame::Climatic("ABC".to_string()); + assert_eq!(frame.encoding_size(), 3 + 2); + + let big_endian = true; + let mut buf = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let size = frame.encode(big_endian, &mut buf).unwrap(); + + assert_eq!(size, frame.encoding_size()); + assert_eq!( + buf, + [14, 3, 'A' as u8, 'B' as u8, 'C' as u8, 0, 0, 0, 0, 0, 0] + ); + + let decoded = MonumentGeoFrame::decode(big_endian, &buf).unwrap(); + + assert_eq!(decoded, frame); + } +} diff --git a/binex/src/message/record/monument/mod.rs b/binex/src/message/record/monument/mod.rs new file mode 100644 index 00000000..0ce1fad9 --- /dev/null +++ b/binex/src/message/record/monument/mod.rs @@ -0,0 +1,328 @@ +//! Monument / Geodetic marker frames + +use crate::{ + message::time::{decode_gpst_epoch, encode_epoch, TimeResolution}, + Error, +}; + +use hifitime::{Epoch, TimeScale}; + +mod fid; +mod frame; +mod src; + +// private +use fid::FieldID; + +// public +pub use frame::MonumentGeoFrame; +pub use src::MonumentGeoMetadata; + +#[derive(Debug, Clone, Default, PartialEq)] +pub struct MonumentGeoRecord { + /// [Epoch] + pub epoch: Epoch, + /// Source of this information + pub source_meta: MonumentGeoMetadata, + /// Frames also refered to as Subrecords + pub frames: Vec, +} + +impl Iterator for MonumentGeoRecord { + type Item = MonumentGeoFrame; + fn next(&mut self) -> Option { + self.frames.iter().next().cloned() + } +} + +impl MonumentGeoRecord { + /// 4 byte date uint4 } + /// 1 byte qsec } epoch + /// 1 byte MonumentGeoMetadata + /// 1 byte FID + /// if FID corresponds to a character string + /// the next 1-4 BNXI byte represent the number of bytes in the caracter string + /// follows: FID dependent sequence. See [FieldID]. + const MIN_SIZE: usize = 5 + 1 + 1; + + /// Creates new empty [MonumentGeoRecord], which is not suitable for encoding yet. + /// Use other method to customize it! + /// ``` + /// use binex::prelude::{ + /// Epoch, + /// Error, + /// MonumentGeoRecord, + /// MonumentGeoMetadata, + /// }; + /// + /// let t = Epoch::from_gpst_seconds(60.0 + 0.75); + /// + /// let record = MonumentGeoRecord::new(t, MonumentGeoMetadata::RNX2BIN) + /// .with_comment("A B C") + /// // read comments carefuly. For example, unlike `comments` + /// // we're not allowed to define more than one geophysical_info. + /// // Otherwise, to frame to be forged will not respect the standards. + /// .with_geophysical_info("Eurasian plate") + /// .with_climatic_info("Rain!"); + /// + /// let mut buf = [0; 8]; + /// match record.encode(true, &mut buf) { + /// Ok(_) => { + /// panic!("encoding should have failed!"); + /// }, + /// Err(Error::NotEnoughBytes) => { + /// // This frame does not fit in this pre allocated buffer. + /// // You should always tie your allocations to .encoding_size() ! + /// }, + /// Err(e) => { + /// panic!("{} error should not have happened!", e); + /// }, + /// } + /// + /// let mut buf = [0; 64]; + /// let _ = record.encode(true, &mut buf) + /// .unwrap(); + /// ``` + pub fn new(epoch: Epoch, meta: MonumentGeoMetadata) -> Self { + Self { + epoch, + source_meta: meta, + frames: Vec::with_capacity(8), + } + } + + /// [Self] decoding attempt from buffered content. + /// ## Inputs + /// - mlen: message length in bytes + /// - time_res: [TimeResolution] + /// - big_endian: endianness + /// - buf: buffered content + /// ## Outputs + /// - Ok: [Self] + /// - Err: [Error] + pub fn decode( + mlen: usize, + time_res: TimeResolution, + big_endian: bool, + buf: &[u8], + ) -> Result { + if mlen < Self::MIN_SIZE { + // does not look good + return Err(Error::NotEnoughBytes); + } + + // decode timestamp + let epoch = decode_gpst_epoch(big_endian, time_res, &buf)?; + + // decode source meta + let source_meta = MonumentGeoMetadata::from(buf[5]); + + // parse inner frames (= subrecords) + let mut ptr = 6; + let mut frames = Vec::::with_capacity(8); + + // this method tolerates badly duplicated subrecords + while ptr < mlen { + // decode field id + match MonumentGeoFrame::decode(big_endian, &buf[ptr..]) { + Ok(fr) => { + ptr += fr.encoding_size(); + frames.push(fr); + }, + Err(_) => { + if ptr == 6 { + // did not parse a single record: incorrect message + return Err(Error::NotEnoughBytes); + } else { + break; // parsed all records + } + }, + } + } + + Ok(Self { + epoch, + frames, + source_meta, + }) + } + + /// Encodes [Self] into buffer, returns encoded size (total bytes). + /// [Self] must fit in preallocated buffer. + pub fn encode(&self, big_endian: bool, buf: &mut [u8]) -> Result { + let size = self.encoding_size(); + if buf.len() < size { + return Err(Error::NotEnoughBytes); + } + + // encode tstamp + let mut ptr = encode_epoch(self.epoch.to_time_scale(TimeScale::GPST), big_endian, buf)?; + + // encode source meta + buf[ptr] = self.source_meta.into(); + ptr += 1; + + // encode subrecords + for fr in self.frames.iter() { + let offs = fr.encode(big_endian, &mut buf[ptr..])?; + ptr += offs; + } + + Ok(self.encoding_size()) + } + + /// Returns total length (bytewise) required to fully encode [Self]. + /// Use this to fulfill [Self::encode] requirements. + pub(crate) fn encoding_size(&self) -> usize { + let mut size = 6; // tstamp + source_meta + for fr in self.frames.iter() { + size += fr.encoding_size(); // content + } + size + } + + /// Add one [MonumentGeoFrame::Comment] to [MonumentGeoRecord]. + /// You can add as many as needed. + pub fn with_comment(&self, comment: &str) -> Self { + let mut s = self.clone(); + s.frames + .push(MonumentGeoFrame::Comment(comment.to_string())); + s + } + + /// Attach readable Geophysical information (like local tectonic plate) + /// to this [MonumentGeoRecord]. You can only add one per dataset + /// otherwise, the message will not respect the standard definitions. + pub fn with_geophysical_info(&self, info: &str) -> Self { + let mut s = self.clone(); + s.frames + .push(MonumentGeoFrame::Geophysical(info.to_string())); + s + } + + /// Provide Climatic or Meteorological information (local to reference site). + /// You can only add one per dataset otherwise, + /// the message will not respect the standard definitions. + pub fn with_climatic_info(&self, info: &str) -> Self { + let mut s = self.clone(); + s.frames.push(MonumentGeoFrame::Climatic(info.to_string())); + s + } + + /// Define a readable UserID to attach to this [MonumentGeoRecord] dataset. + /// You can only add one per dataset otherwise, + /// the message will not respect the standard definitions. + pub fn with_user_id(&self, userid: &str) -> Self { + let mut s = self.clone(); + s.frames.push(MonumentGeoFrame::Comment(userid.to_string())); + s + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn monument_marker_bnx00_error() { + let buf = [0, 0, 0, 0]; + let time_res = TimeResolution::QuarterSecond; + let monument = MonumentGeoRecord::decode(4, time_res, true, &buf); + assert!(monument.is_err()); + } + + #[test] + fn monument_geo_comments_decoding() { + let mlen = 17; + let big_endian = true; + let time_res = TimeResolution::QuarterSecond; + + let buf = [ + 0, 0, 1, 1, 41, 2, 0, 9, 'H' as u8, 'e' as u8, 'l' as u8, 'l' as u8, 'o' as u8, + ' ' as u8, 'G' as u8, 'E' as u8, 'O' as u8, + ]; + + match MonumentGeoRecord::decode(mlen, time_res, big_endian, &buf) { + Ok(monument) => { + assert_eq!( + monument.epoch, + Epoch::from_gpst_seconds(256.0 * 60.0 + 60.0 + 10.25) + ); + assert_eq!(monument.source_meta, MonumentGeoMetadata::IGS); + assert_eq!(monument.frames.len(), 1); + assert_eq!( + monument.frames[0], + MonumentGeoFrame::Comment("Hello GEO".to_string()) + ); + + // test mirror op + let mut target = [0, 0, 0, 0, 0, 0, 0, 0]; + assert!(monument.encode(big_endian, &mut target).is_err()); + + // test mirror op + let mut target = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + match monument.encode(big_endian, &mut target) { + Err(e) => panic!("{} should have passed", e), + Ok(_) => { + assert_eq!(target, buf,); + }, + } + }, + Err(e) => panic!("decoding error: {}", e), + } + } + + #[test] + fn monument_geo_double_comments_decoding() { + let t = Epoch::from_gpst_seconds(60.0 + 0.75); + + let record = MonumentGeoRecord::new(t, MonumentGeoMetadata::RNX2BIN) + .with_comment("A B C") + .with_comment("D E F"); + + let mut buf = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + match record.encode(true, &mut buf) { + Ok(_) => panic!("should have panic'ed!"), + Err(Error::NotEnoughBytes) => {}, + Err(e) => panic!("invalid error: {}", e), + } + + let mut buf = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + + match record.encode(true, &mut buf) { + Err(e) => panic!("{} should have passed!", e), + Ok(size) => { + assert_eq!(size, 20); + assert_eq!( + buf, + [0, 0, 0, 1, 3, 1, 0, 5, 65, 32, 66, 32, 67, 0, 5, 68, 32, 69, 32, 70, 0] + ); + }, + } + + let geo = MonumentGeoRecord::new(t, MonumentGeoMetadata::IGS) + .with_comment("Hello") + .with_climatic_info("Clim"); + + let mut buf = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + + match geo.encode(true, &mut buf) { + Err(e) => panic!("{} should have passed!", e), + Ok(size) => { + assert_eq!(size, 19); + assert_eq!( + buf, + [ + 0, 0, 0, 1, 3, 2, 0, 5, 'H' as u8, 'e' as u8, 'l' as u8, 'l' as u8, + 'o' as u8, 14, 4, 'C' as u8, 'l' as u8, 'i' as u8, 'm' as u8, 0, 0 + ] + ); + }, + } + } +} diff --git a/binex/src/message/record/monument/src.rs b/binex/src/message/record/monument/src.rs new file mode 100644 index 00000000..2e86bb71 --- /dev/null +++ b/binex/src/message/record/monument/src.rs @@ -0,0 +1,43 @@ +//! Monument / Geodetic marker source description + +/// [MonumentGeoMetadata] describes the source of information +/// of the Monument marker record +#[derive(Debug, Clone, Copy, Default, PartialEq)] +pub enum MonumentGeoMetadata { + /// Monument marker was generated by the firmware + /// of a GNSS receiver + ReceiverFirmware = 0, + #[default] + /// Monument marker was generated by RINEX to BINEX conversion + RNX2BIN = 1, + /// Created from IGS data (by software) + IGS = 2, + /// Information supplied by external user (like configuration file) + External = 3, + /// Created from other formats or streams (not RINEX nor BINEX) + Other = 4, +} + +impl From for MonumentGeoMetadata { + fn from(val: u8) -> Self { + match val { + 0 => Self::ReceiverFirmware, + 1 => Self::RNX2BIN, + 2 => Self::IGS, + 3 => Self::External, + _ => Self::Other, + } + } +} + +impl From for u8 { + fn from(val: MonumentGeoMetadata) -> Self { + match val { + MonumentGeoMetadata::ReceiverFirmware => 0, + MonumentGeoMetadata::RNX2BIN => 1, + MonumentGeoMetadata::IGS => 2, + MonumentGeoMetadata::External => 3, + MonumentGeoMetadata::Other => 4, + } + } +} diff --git a/binex/src/message/time.rs b/binex/src/message/time.rs new file mode 100644 index 00000000..28a55076 --- /dev/null +++ b/binex/src/message/time.rs @@ -0,0 +1,200 @@ +//! Epoch encoding & decoding + +use hifitime::prelude::{Epoch, TimeScale, Unit}; + +use crate::{utils::Utils, Error}; + +/// BINEX Time Resolution +#[derive(Debug, Copy, Clone, Default, PartialEq)] +pub enum TimeResolution { + /// One Byte Second = 250ms resolution on [Epoch]s + #[default] + QuarterSecond = 0, +} + +/// [Epoch] decoding attempt from buffered content. +/// If buffer does not contain enough data (5 bytes needed), this returns [Error::NotEnoughBytes]. +pub(crate) fn decode_epoch( + big_endian: bool, + time_res: TimeResolution, + buf: &[u8], + ts: TimeScale, +) -> Result { + let min; + let qsec; + + if buf.len() < 5 { + return Err(Error::NotEnoughBytes); + } + match time_res { + TimeResolution::QuarterSecond => { + min = Utils::decode_u32(big_endian, buf)?; + qsec = buf[4]; + }, + } + Ok(Epoch::from_duration( + (min as f64) * Unit::Minute + (qsec as f64 / 4.0) * Unit::Second, + ts, + )) +} + +/// [Epoch] encoding method. +/// If buffer is too small (needs 5 bytes), this returns [Error::NotEnoughBytes]. +/// This is the exact [decode_epoch] mirror operation. +/// We only support GPST, GST and BDT at the moment, +/// it will return [Error::NonSupportedTimescale] for any other timescale. +pub(crate) fn encode_epoch(t: Epoch, big_endian: bool, buf: &mut [u8]) -> Result { + if buf.len() < 5 { + return Err(Error::NotEnoughBytes); + } + + let dt_s = t.duration.to_seconds(); + let total_mins = (dt_s / 60.0).round() as u32; + + let total_qsec = (dt_s - (total_mins as f64) * 60.0) / 0.25; + + let bytes = total_mins.to_be_bytes(); + + if big_endian { + buf[0] = bytes[0]; + buf[1] = bytes[1]; + buf[2] = bytes[2]; + buf[3] = bytes[3]; + } else { + buf[0] = bytes[3]; + buf[1] = bytes[2]; + buf[2] = bytes[1]; + buf[3] = bytes[0]; + } + + buf[4] = total_qsec as u8 & 0x7f; // 0xf0-0xff are excluded + Ok(5) +} + +/// GPST [Epoch] decoding attempt from buffered content. +pub(crate) fn decode_gpst_epoch( + big_endian: bool, + time_res: TimeResolution, + buf: &[u8], +) -> Result { + decode_epoch(big_endian, time_res, buf, TimeScale::GPST) +} + +// /// GST [Epoch] decoding attempt from buffered content. +// pub(crate) fn decode_gst_epoch( +// big_endian: bool, +// time_res: TimeResolution, +// buf: &[u8], +// ) -> Result { +// decode_epoch(big_endian, time_res, buf, TimeScale::GST) +// } + +// /// BDT [Epoch] decoding attempt from buffered content. +// pub(crate) fn decode_bdt_epoch( +// big_endian: bool, +// time_res: TimeResolution, +// buf: &[u8], +// ) -> Result { +// decode_epoch(big_endian, time_res, buf, TimeScale::BDT) +// } + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn epoch_fail() { + let buf = [0]; + assert!(decode_gpst_epoch(true, TimeResolution::QuarterSecond, &buf).is_err()); + let buf = [0, 0, 0]; + assert!(decode_gpst_epoch(true, TimeResolution::QuarterSecond, &buf).is_err()); + } + #[test] + fn gpst_sub_minute() { + let big_endian = true; + + let mut buf = [0, 0, 0, 0, 0]; + let t = Epoch::from_gpst_seconds(10.0); + let _ = encode_epoch(t, big_endian, &mut buf).unwrap(); + assert_eq!(buf, [0, 0, 0, 0, 40]); + + let decoded = decode_epoch( + big_endian, + TimeResolution::QuarterSecond, + &buf, + TimeScale::GPST, + ) + .unwrap(); + + assert_eq!(decoded, t); + + let mut buf = [0, 0, 0, 0, 0]; + let t = Epoch::from_gpst_seconds(0.75); + let _ = encode_epoch(t, big_endian, &mut buf).unwrap(); + assert_eq!(buf, [0, 0, 0, 0, 3]); + + let decoded = decode_epoch( + big_endian, + TimeResolution::QuarterSecond, + &buf, + TimeScale::GPST, + ) + .unwrap(); + + assert_eq!(decoded, t); + + let mut buf = [0, 0, 0, 0, 0]; + let t = Epoch::from_gpst_seconds(10.75); + let _ = encode_epoch(t, big_endian, &mut buf).unwrap(); + assert_eq!(buf, [0, 0, 0, 0, 43]); + + let decoded = decode_epoch( + big_endian, + TimeResolution::QuarterSecond, + &buf, + TimeScale::GPST, + ) + .unwrap(); + + assert_eq!(decoded, t); + } + #[test] + fn gpst_epoch_decoding() { + // test QSEC + for (buf, big_endian, gpst_epoch) in [ + ([0, 0, 0, 1, 0], true, Epoch::from_gpst_seconds(60.0)), + ([0, 0, 0, 5, 0], true, Epoch::from_gpst_seconds(300.0)), + ( + [0, 0, 1, 1, 1], + true, + Epoch::from_gpst_seconds(2.0_f64.powf(8.0) * 60.0 + 60.0 + 0.25), + ), + ( + [0, 0, 1, 1, 2], + true, + Epoch::from_gpst_seconds(2.0_f64.powf(8.0) * 60.0 + 60.0 + 0.5), + ), + ( + [0, 0, 1, 1, 0], + true, + Epoch::from_gpst_seconds(2.0_f64.powf(8.0) * 60.0 + 60.0), + ), + ( + [1, 1, 1, 0, 0], + true, + Epoch::from_gpst_seconds( + (2.0_f64.powf(24.0) + 2.0_f64.powf(16.0) + 2.0_f64.powf(8.0)) * 60.0, + ), + ), + ] { + let epoch = decode_gpst_epoch(big_endian, TimeResolution::QuarterSecond, &buf); + assert!(epoch.is_ok(), "to parse valid gpst_epoch"); + let epoch = epoch.unwrap(); + assert_eq!(epoch, gpst_epoch); + + // test mirror op + let mut test = [0, 0, 0, 0, 0]; + let _ = encode_epoch(epoch, big_endian, &mut test).unwrap(); + assert_eq!(test, buf, "encode_epoch failed for {}", epoch); + } + } +} diff --git a/binex/src/utils.rs b/binex/src/utils.rs new file mode 100644 index 00000000..e5f6e1de --- /dev/null +++ b/binex/src/utils.rs @@ -0,0 +1,83 @@ +use crate::Error; + +pub struct Utils; + +impl Utils { + /// Simple usize min() comparison to avoid `std` dependency. + pub fn min_usize(a: usize, b: usize) -> usize { + if a <= b { + a + } else { + b + } + } + /// u16 decoding attempt, as specified by + /// [https://www.unavco.org/data/gps-gnss/data-formats/binex/conventions.html#uint2] + pub fn decode_u16(big_endian: bool, buf: &[u8]) -> Result { + if buf.len() < 2 { + Err(Error::NotEnoughBytes) + } else { + if big_endian { + Ok(u16::from_be_bytes([buf[0], buf[1]])) + } else { + Ok(u16::from_le_bytes([buf[0], buf[1]])) + } + } + } + /// u32 decoding attempt, as specified by + /// [https://www.unavco.org/data/gps-gnss/data-formats/binex/conventions.html#uint4] + pub fn decode_u32(big_endian: bool, buf: &[u8]) -> Result { + if buf.len() < 4 { + Err(Error::NotEnoughBytes) + } else { + if big_endian { + Ok(u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]])) + } else { + Ok(u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]])) + } + } + } + /// i32 decoding attempt, as specified by + /// [https://www.unavco.org/data/gps-gnss/data-formats/binex/conventions.html#sint4] + pub fn decode_i32(big_endian: bool, buf: &[u8]) -> Result { + if buf.len() < 4 { + Err(Error::NotEnoughBytes) + } else { + if big_endian { + Ok(i32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]])) + } else { + Ok(i32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]])) + } + } + } + /// f32 decoding attempt, as specified by + /// [https://www.unavco.org/data/gps-gnss/data-formats/binex/conventions.html#real4] + pub fn decode_f32(big_endian: bool, buf: &[u8]) -> Result { + if buf.len() < 4 { + Err(Error::NotEnoughBytes) + } else { + if big_endian { + Ok(f32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]])) + } else { + Ok(f32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]])) + } + } + } + /// f64 decoding attempt, as specified by + /// [https://www.unavco.org/data/gps-gnss/data-formats/binex/conventions.html#real8] + pub fn decode_f64(big_endian: bool, buf: &[u8]) -> Result { + if buf.len() < 8 { + Err(Error::NotEnoughBytes) + } else { + if big_endian { + Ok(f64::from_be_bytes([ + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], + ])) + } else { + Ok(f64::from_le_bytes([ + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], + ])) + } + } + } +} diff --git a/binex/tests/decoder.rs b/binex/tests/decoder.rs new file mode 100644 index 00000000..623b4b56 --- /dev/null +++ b/binex/tests/decoder.rs @@ -0,0 +1,60 @@ +use binex::prelude::{Decoder, Error}; +use std::fs::File; + +#[test] +fn mfle20190130() { + let mut found = 0; + let fd = File::open("../test_resources/BIN/mfle20190130.bnx").unwrap(); + + let mut decoder = Decoder::new(fd); + + loop { + match decoder.next() { + Some(Ok(msg)) => { + found += 1; + println!("parsed: {:?}", msg); + }, + Some(Err(e)) => match e { + Error::IoError(e) => panic!("i/o error: {}", e), + e => { + //println!("err={}", e); + }, + }, + None => { + println!("EOS"); + break; + }, + } + } + assert!(found > 0, "not a single msg decoded"); +} + +#[cfg(feature = "flate2")] +fn gziped_files() { + let mut found = 0; + for fp in ["mfle20200105.bnx.gz", "mfle20200113.bnx.gz"] { + let fp = format!("../test_resources/BIN/{}", fp); + let fd = File::open(fp).unwrap(); + let mut decoder = Decoder::new_gzip(fd); + + loop { + match decoder.next() { + Some(Ok(msg)) => { + found += 1; + println!("parsed: {:?}", msg); + }, + Some(Err(e)) => match e { + Error::IoError(e) => panic!("i/o error: {}", e), + e => { + println!("err={}", e); + }, + }, + None => { + println!("EOS"); + break; + }, + } + } + assert!(found > 0, "not a single msg decoded"); + } +} diff --git a/binex/tests/geo.rs b/binex/tests/geo.rs new file mode 100644 index 00000000..943d53f7 --- /dev/null +++ b/binex/tests/geo.rs @@ -0,0 +1,79 @@ +use binex::prelude::{ + Epoch, Message, MonumentGeoMetadata, MonumentGeoRecord, Record, TimeResolution, +}; + +#[test] +fn geo_message() { + let big_endian = false; + let reversed = false; + let enhanced_crc = false; + let t = Epoch::from_gpst_seconds(10.0 + 0.75); + let time_res = TimeResolution::QuarterSecond; + + let geo = MonumentGeoRecord::new(t, MonumentGeoMetadata::IGS).with_comment("Hello"); + + let record = Record::new_monument_geo(geo); + let msg = Message::new(big_endian, time_res, enhanced_crc, reversed, record); + + let mut buf = [0; 24]; + msg.encode(&mut buf).unwrap(); + + assert_eq!( + buf, + [0xC2, 0, 13, 0, 0, 0, 0, 43, 2, 0, 5, 72, 101, 108, 108, 111, 0, 0, 0, 0, 0, 0, 0, 0] + ); + + let geo = MonumentGeoRecord::new(t, MonumentGeoMetadata::IGS) + .with_comment("Hello") + .with_comment("World"); + + let record = Record::new_monument_geo(geo); + let msg = Message::new(big_endian, time_res, enhanced_crc, reversed, record); + + let mut buf = [0; 32]; + msg.encode(&mut buf).unwrap(); + + assert_eq!( + buf, + [ + 0xC2, 0, 20, 0, 0, 0, 0, 43, 2, 0, 5, 72, 101, 108, 108, 111, 0, 5, 87, 111, 114, 108, + 100, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + + let geo = MonumentGeoRecord::new(t, MonumentGeoMetadata::IGS) + .with_comment("Hello") + .with_comment("World"); + + let record = Record::new_monument_geo(geo); + let msg = Message::new(big_endian, time_res, enhanced_crc, reversed, record); + + let mut buf = [0; 32]; + msg.encode(&mut buf).unwrap(); + + assert_eq!( + buf, + [ + 0xC2, 0, 20, 0, 0, 0, 0, 43, 2, 0, 5, 72, 101, 108, 108, 111, 0, 5, 87, 111, 114, 108, + 100, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); + + let geo = MonumentGeoRecord::new(t, MonumentGeoMetadata::IGS) + .with_comment("Hello") + .with_climatic_info("Clim"); + + let record = Record::new_monument_geo(geo); + let msg = Message::new(big_endian, time_res, enhanced_crc, reversed, record); + + let mut buf = [0; 32]; + msg.encode(&mut buf).unwrap(); + + assert_eq!( + buf, + [ + 0xC2, 0, 19, 0, 0, 0, 0, 43, 2, 0, 5, 72, 101, 108, 108, 111, 14, 4, 67, 108, 105, 109, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + ); +} diff --git a/binex/tests/lib.rs b/binex/tests/lib.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/binex/tests/lib.rs @@ -0,0 +1 @@ + diff --git a/binex/tests/message.rs b/binex/tests/message.rs new file mode 100644 index 00000000..488d35ce --- /dev/null +++ b/binex/tests/message.rs @@ -0,0 +1,31 @@ +use binex::prelude::{ + EphemerisFrame, Epoch, Message, MonumentGeoMetadata, MonumentGeoRecord, Record, TimeResolution, +}; + +#[test] +fn big_endian_message() { + let t = Epoch::from_gpst_seconds(10.0); + let geo_meta = MonumentGeoMetadata::RNX2BIN; + + for msg in [Message::new( + true, + TimeResolution::QuarterSecond, + false, + false, + Record::new_monument_geo( + MonumentGeoRecord::new(t, geo_meta) + .with_climatic_info("climatic") + .with_comment("comment #1") + .with_comment("#comment 2") + .with_geophysical_info("geophysics") + .with_user_id("Custom ID#"), + ), + )] { + let mut buf = [0; 1024]; + msg.encode(&mut buf).unwrap(); + + let parsed = Message::decode(&buf).unwrap(); + + assert_eq!(msg, parsed); + } +} diff --git a/test_resources/BIN/mfle20190130.bnx b/test_resources/BIN/mfle20190130.bnx new file mode 100644 index 00000000..752e6efb Binary files /dev/null and b/test_resources/BIN/mfle20190130.bnx differ diff --git a/test_resources/BIN/mfle20200105.bnx.gz b/test_resources/BIN/mfle20200105.bnx.gz new file mode 100644 index 00000000..2b2fdecf Binary files /dev/null and b/test_resources/BIN/mfle20200105.bnx.gz differ diff --git a/test_resources/BIN/mfle20200113.bnx.gz b/test_resources/BIN/mfle20200113.bnx.gz new file mode 100644 index 00000000..029302e8 Binary files /dev/null and b/test_resources/BIN/mfle20200113.bnx.gz differ diff --git a/tools/README.md b/tools/README.md index 79f40e85..11ab6621 100644 --- a/tools/README.md +++ b/tools/README.md @@ -23,5 +23,6 @@ that typically sort GNSS data by DOY. Set to development and testing tools. +- `parse_crit_benchmark.py` Python script to parse the results of our Criterion benchmarks (CI/dev purposes). Originally writen by Christopher Rabotin. - `builddoc.sh` builds the API doc exactly how publication with cargo does - `testlib.sh` tests the API built with several different options diff --git a/tools/parse_crit_benchmark.py b/tools/parse_crit_benchmark.py new file mode 100755 index 00000000..474e46f7 --- /dev/null +++ b/tools/parse_crit_benchmark.py @@ -0,0 +1,26 @@ +#! /usr/bin/env python3 +import sys +import re + + +def main(): + input_data = sys.stdin.read() + table_header = '\n\n| Test Description | Times | Outliers |\n| --- | --- | --- |\n' + table_body = '' + + benchmarks = re.findall(r'^(.+)\n?\s+time:\s+\[(.+)\]([\s\S]+?)Found (\d+) outliers', + input_data, re.MULTILINE) + + for benchmark in benchmarks: + test_description = benchmark[0] + time = benchmark[1] + outliers = benchmark[3] + + table_body += f'| {test_description} | {time} | {outliers} |\n' + + markdown_table = table_header + table_body + print(markdown_table) + + +if __name__ == '__main__': + main()