diff --git a/Cargo.lock b/Cargo.lock index 1e1adb9..16ddb4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -547,7 +547,6 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" name = "maarco" version = "0.1.0" dependencies = [ - "base64", "chrono", "clap", "clap_derive", @@ -555,6 +554,8 @@ dependencies = [ "csv", "eyre", "nmea", + "ntrip", + "rtkbase", "serde", "serialport", ] @@ -629,6 +630,13 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "ntrip" +version = "0.1.0" +dependencies = [ + "base64", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -713,6 +721,13 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "rtkbase" +version = "0.1.0" +dependencies = [ + "serialport", +] + [[package]] name = "rustc_version" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index dfb9fbf..9617eb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,17 @@ +[workspace] +members = [ + "rtkbase", + "ntrip", +] +resolver = "2" + [package] name = "maarco" version = "0.1.0" edition = "2024" +description = "Logging from Arduino and receiving NTRIP data via RTK2GO in Rust" [dependencies] -base64 = "0.22.1" chrono = "0.4.42" clap = { version = "4.5.48", features = ["derive"] } clap_derive = "4.5.47" @@ -14,4 +21,6 @@ csv = "1.3.1" eyre = "0.6.12" nmea = { version = "0.7.0", features = ["default", "serde"] } serde = "1.0.228" -serialport = { version = "4.7.3", default-features = false } \ No newline at end of file +serialport = { version = "4.7.3", default-features = false } +ntrip = { path = "ntrip" } +rtkbase = { path = "rtkbase" } \ No newline at end of file diff --git a/Cross.toml b/Cross.toml deleted file mode 100644 index 8b7bd09..0000000 --- a/Cross.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.aarch64-unknown-linux-gnu] -dockerfile = "Dockerfile" \ No newline at end of file diff --git a/ntrip/Cargo.toml b/ntrip/Cargo.toml new file mode 100644 index 0000000..d041291 --- /dev/null +++ b/ntrip/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "ntrip" +version = "0.1.0" +edition = "2024" +description = "NTRIP Client for RTK2GO implementation in Rust" + +[dependencies] +base64 = "0.22.1" diff --git a/src/ntrip.rs b/ntrip/src/lib.rs similarity index 100% rename from src/ntrip.rs rename to ntrip/src/lib.rs diff --git a/rtkbase/Cargo.toml b/rtkbase/Cargo.toml new file mode 100644 index 0000000..bb51b7b --- /dev/null +++ b/rtkbase/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rtkbase" +version = "0.1.0" +edition = "2024" +description = "RTK Base Station code" + +[dependencies] +serialport = { version = "4.7.3", default-features = false } \ No newline at end of file diff --git a/rtkbase/src/dispatcher.rs b/rtkbase/src/dispatcher.rs new file mode 100644 index 0000000..8c1a6ac --- /dev/null +++ b/rtkbase/src/dispatcher.rs @@ -0,0 +1,80 @@ +use crate::protocol::response::WireMessage; +use std::sync::mpsc::{Sender}; +use std::sync::{Arc, Mutex}; + +type MatchFn = Box bool + Send + Sync>; + + +struct Waiter { + /// Function to determine if a message matches the waiter's criteria. + matches: MatchFn, + /// Transmitter to send matched messages to the waiter. + tx: Sender, + /// How many messages are still needed before the waiter is satisfied: + remaining: u32, +} + +#[derive(Clone)] +pub struct Dispatcher { + /// The transmitter for the main data stream. Used to forward messages not claimed by waiters. + stream_tx: Option>, + /// The list of waiters listening for specific messages. + waiters: Arc>>, +} + +impl Dispatcher { + pub fn new() -> Self { + Dispatcher { + stream_tx: None, + waiters: Arc::new(Mutex::new(Vec::new())), + } + } + + pub fn set_stream_tx(&mut self, tx: Sender) { + self.stream_tx = Some(tx); + } + + /// Registers a new waiter with a matching function and a transmitter. + pub fn register_waiter(&self, matches: MatchFn, tx: Sender, count: u32) { + let mut waiters = self.waiters.lock().unwrap(); + waiters.push(Waiter { matches, tx, remaining: count }); + } + + pub fn dispatch(&self, msg: WireMessage) { + // Try to satisfy waiters first: + let mut waiters = self.waiters.lock().unwrap(); + let mut i = 0; + + // println!("Waiters count: {}", waiters.len()); + while i < waiters.len() { + if (waiters[i].matches)(&msg) { + // Send the message to the waiter: + // println!("A waiter claimed the message {:?}, sending to waiter.", &msg); + let _ = waiters[i].tx.send(msg.clone()); + // Decrement remaining count + waiters[i].remaining -= 1; + + // Remove if done + if waiters[i].remaining == 0 { + waiters.remove(i); + } + drop(waiters); // Release lock + return; + } else { + i += 1; + } + } + drop(waiters); // Release lock + // No waiter claimed the message, send to stream: + // println!("No waiter claimed the message, sending to stream."); + self.fanout_stream(msg); + } + + fn fanout_stream(&self, msg: WireMessage) { + if let Some(ref tx) = self.stream_tx { + // println!("Sending message to stream."); + let _ = tx.send(msg); + } + } +} + diff --git a/rtkbase/src/lib.rs b/rtkbase/src/lib.rs new file mode 100644 index 0000000..83be637 --- /dev/null +++ b/rtkbase/src/lib.rs @@ -0,0 +1,5 @@ +pub mod parsing; +pub mod port; +pub mod protocol; +pub mod dispatcher; +mod methods; diff --git a/rtkbase/src/main.rs b/rtkbase/src/main.rs new file mode 100644 index 0000000..4c80f93 --- /dev/null +++ b/rtkbase/src/main.rs @@ -0,0 +1,38 @@ +/// This file is just for testing the rtkbase crate. +/// +use rtkbase; +use rtkbase::port::BaseGPS; +use rtkbase::protocol::response::WireMessage; +use std::{path::PathBuf}; + +fn test_reading_sentences() { + let mut rtk = BaseGPS::open_port(PathBuf::from("/dev/ttyUSB0")).unwrap(); + let _ = rtk.start(); + println!("Opened RTK GPS port successfully."); + let timeout = std::time::Duration::from_secs(2); + + let mut count = 0; + while count < 5 { + if let Some(msg) = rtk.get_gps_data(timeout) { + match &msg { + WireMessage::PQTMMessage(resp) => { + println!("Received PQTM Response: {:?}", resp); + } + WireMessage::PairMessage(pair) => { + println!("Received PAIR Message: {:?}", pair); + } + } + } + count += 1; + } + + let ver_no = rtk.verno(timeout).unwrap(); + println!("Module Version: {:?}", ver_no.version); + + let rtcm_output_mode = rtk.pair_get_rtcm_mode(timeout).unwrap(); + println!("Current RTCM Output Mode: {:?}", rtcm_output_mode); +} + +fn main() { + test_reading_sentences(); +} diff --git a/rtkbase/src/methods.rs b/rtkbase/src/methods.rs new file mode 100644 index 0000000..e31d571 --- /dev/null +++ b/rtkbase/src/methods.rs @@ -0,0 +1,186 @@ + +use std::time::Duration; + +use crate::protocol::commands::{PQTMCommand, PQTMCfgMsgRate, PQTMCfgMsgRateGet, PQTMCfgSvin}; +use crate::protocol::response::{PQTMResponse, PQTMVerNo, ParseError, ResponseError}; +use crate::protocol::pair::{PairCommand, PairResponse, PairACK, AckResult, PairRTCMSetOutputMode, PairRTCMSetOutputAntPnt, PairRTCMSetOutputEphemeris, RtcmMode, RtcmAntPnt, RtcmEphemeris}; + + +use crate::port::BaseGPS; + + +macro_rules! command_methods { + ( + $( + $method_name:ident ( + $($arg_name:ident: $arg_type:ty),* + ) -> $return_type:ty { + command: $command_variant:expr, + ok: $ok_pattern:pat => $ok_expr:expr, + err: $err_pattern:pat => $err_expr:expr, + } + )* + ) => { + $( + pub fn $method_name( + &mut self, + $($arg_name: $arg_type,)* + timeout: Duration, + ) -> Result<$return_type, ResponseError> { + let resp = self.send_command($command_variant, timeout)?; + match resp { + $ok_pattern => Ok($ok_expr), + $err_pattern => Err(ResponseError::ModuleError($err_expr)), + _ => Err(ResponseError::ParseError( + ParseError::ParsingError(concat!( + "unexpected response to ", + stringify!($method_name) + )) + )), + } + } + )* + }; +} + +macro_rules! pair_get_methods { + ( + $( + $method_name:ident ( + $($arg_name:ident: $arg_type:ty),* + ) -> $return_type:ty { + command: $command_variant:expr, + response: $response_pattern:pat => $response_expr:expr, + } + )* + ) => { + $( + pub fn $method_name( + &mut self, + $($arg_name: $arg_type,)* + timeout: Duration, + ) -> Result<$return_type, ResponseError> { + let (ack, resp) = self.send_pair_get($command_variant, timeout)?; + + // Validate ACK is success (already checked in send_pair_get, but be explicit) + if ack.result != AckResult::Success { + return Err(ResponseError::ParseError( + ParseError::ParsingError("PAIR command ACK failed") + )); + } + + match resp { + $response_pattern => Ok($response_expr), + _ => Err(ResponseError::ParseError( + ParseError::ParsingError(concat!( + "unexpected response to ", + stringify!($method_name) + )) + )), + } + } + )* + }; +} + +macro_rules! pair_set_methods { + ( + $( + $method_name:ident ( + $($arg_name:ident: $arg_type:ty),* + ) { + command: $command_variant:expr, + } + )* + ) => { + $( + pub fn $method_name( + &mut self, + $($arg_name: $arg_type,)* + timeout: Duration, + ) -> Result { + self.send_pair_set($command_variant, timeout) + } + )* + }; +} + + +impl BaseGPS { + + command_methods! { + verno() -> PQTMVerNo { + command: PQTMCommand::Verno, + ok: PQTMResponse::Verno(info) => info, + err: PQTMResponse::VernoError(e) => e, + } + + save_par() -> () { + command: PQTMCommand::SavePar, + ok: PQTMResponse::SaveParOk => (), + err: PQTMResponse::SaveParError(e) => e, + } + + restore_par() -> () { + command: PQTMCommand::RestorePar, + ok: PQTMResponse::RestoreParOk => (), + err: PQTMResponse::RestoreParError(e) => e, + } + + cfg_svin_read() -> PQTMCfgSvin { + command: PQTMCommand::CfgSvinRead, + ok: PQTMResponse::CfgSvinReadOk(cfg) => cfg, + err: PQTMResponse::CfgSvinError(e) => e, + } + + cfg_svin_write(cfg: PQTMCfgSvin) -> () { + command: PQTMCommand::CfgSvinWrite(cfg), + ok: PQTMResponse::CfgSvinWriteOk => (), + err: PQTMResponse::CfgSvinError(e) => e, + } + + cfg_msgrate_write(rate: PQTMCfgMsgRate) -> () { + command: PQTMCommand::CfgMsgRateWrite(rate), + ok: PQTMResponse::CfgMsgRateWriteOk => (), + err: PQTMResponse::CfgMsgRateError(e) => e, + } + + cfg_msgrate_read(req: PQTMCfgMsgRateGet) -> PQTMCfgMsgRate { + command: PQTMCommand::CfgMsgRateRead(req), + ok: PQTMResponse::CfgMsgRateReadOk(rate) => rate, + err: PQTMResponse::CfgMsgRateError(e) => e, + } + } + // PAIR GET commands (wait for ACK + response) + pair_get_methods! { + pair_get_rtcm_mode() -> RtcmMode { + command: PairCommand::RtcmGetOutputMode, + response: PairResponse::RtcmOutputMode(mode) => mode.mode, + } + + pair_get_rtcm_antpnt() -> RtcmAntPnt { + command: PairCommand::RtcmGetOutputAntPnt, + response: PairResponse::RtcmOutputAntPnt(antpnt) => antpnt.ant_pnt, + } + + pair_get_rtcm_ephemeris() -> RtcmEphemeris { + command: PairCommand::RtcmGetOutputEphemeris, + response: PairResponse::RtcmOutputEphemeris(eph) => eph.ephemeris, + } + } + + // PAIR SET commands (only wait for ACK) + pair_set_methods! { + pair_set_rtcm_mode(mode: PairRTCMSetOutputMode) { + command: PairCommand::RtcmSetOutputMode(mode), + } + + pair_set_rtcm_antpnt(antpnt: PairRTCMSetOutputAntPnt) { + command: PairCommand::RtcmSetOutputAntPnt(antpnt), + } + + pair_set_rtcm_ephemeris(ephemeris: PairRTCMSetOutputEphemeris) { + command: PairCommand::RtcmSetOutputEphemeris(ephemeris), + } + } +} \ No newline at end of file diff --git a/rtkbase/src/parsing.rs b/rtkbase/src/parsing.rs new file mode 100644 index 0000000..4263df5 --- /dev/null +++ b/rtkbase/src/parsing.rs @@ -0,0 +1,78 @@ +use crate::protocol::{pair::{PairResponse}, response::{PQTMResponse, WireMessage}, sentence::Deserialize}; + +pub struct PQTMParser { + incomplete_sentence: String, +} + +impl PQTMParser { + pub fn new() -> Self { + PQTMParser { + incomplete_sentence: String::new(), + } + } + + /// Parses incoming data for complete $PQTM* sentences. + pub fn parse_data(&mut self, data: &str) -> Vec { + let mut complete_parsed_sentences: Vec = Vec::new(); + let mut buffer = self.incomplete_sentence.clone() + data; + + // Loop to find complete sentences in the buffer. Break when the next sentence is + // incomplete. + loop { + let start_index = match buffer.find("$P") { + Some(index) => index, + None => { + // No start found, discard buffer + self.incomplete_sentence.clear(); + break; + } + }; + + // Find the end of the sentence: + let end_index = match buffer[start_index..].find("\r\n") { + Some(index) => start_index + index + 2, // Include \r\n + None => { + // No end found, store incomplete sentence + self.incomplete_sentence = buffer[start_index..].to_string(); + break; + } + }; + + // Extract complete sentence + let complete_sentence = &buffer[start_index..end_index]; + println!("\n\nComplete PQTM sentence: {}", complete_sentence); + complete_parsed_sentences.push(complete_sentence.to_string()); + + // Move the buffer forward: + buffer = buffer[end_index..].to_string(); + } + + let mut pqtm_outputs: Vec = Vec::new(); + + for s in &complete_parsed_sentences { + if s.starts_with("$PQTM") { + let resp = PQTMResponse::from_sentence(&s); + match resp { + Err(e) => { + eprintln!("Failed to parse PQTM Response: {:?}, Error: {:?}", s, e); + } + Ok(resp) => { + pqtm_outputs.push(WireMessage::PQTMMessage(resp)); + } + } + } else if s.starts_with("$PAIR") { + let resp = PairResponse::from_sentence(&s); + match resp { + Err(e) => { + eprintln!("Failed to parse PAIR Message: {:?}, Error: {:?}", s, e); + } + Ok(pair) => { + pqtm_outputs.push(WireMessage::PairMessage(pair)); + } + } + } + } + + pqtm_outputs + } +} diff --git a/rtkbase/src/port.rs b/rtkbase/src/port.rs new file mode 100644 index 0000000..0935f24 --- /dev/null +++ b/rtkbase/src/port.rs @@ -0,0 +1,216 @@ +use serialport::Error; +use serialport::TTYPort; +use std::io::Read; +use std::io::{self, Write}; +use std::io::{BufReader}; +use std::path::PathBuf; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::mpsc; +use std::sync::mpsc::Receiver; +use std::thread; +use std::thread::JoinHandle; +use std::time::Duration; +use crate::dispatcher::Dispatcher; +use crate::parsing::PQTMParser; +use crate::protocol::commands::PQTMCommand; +use crate::protocol::response::PQTMResponse; +use crate::protocol::response::ParseError; +use crate::protocol::response::ResponseError; +use crate::protocol::response::WireMessage; +use crate::protocol::pair::{PairCommand, PairResponse, PairACK, AckResult}; +use crate::protocol::sentence::Serialize; + +pub struct BaseGPS { + base_gps_port: TTYPort, + stream_rx: Option>, + dispatcher: Dispatcher, + stop_signal: Arc, +} + +impl BaseGPS { + const BAUD_RATE: u32 = 115_200; + + /// Starts a thread to read data from the GPS port, extracts complete NMEA sentences. + pub fn start(&mut self) -> JoinHandle<()> { + let (stream_tx, stream_rx) = mpsc::channel(); + self.stream_rx = Some(stream_rx); + self.dispatcher.set_stream_tx(stream_tx); + self.rtk_reader_thread() + } + + /// Pops the next available PqtmOutput from the internal buffer, if any. + pub fn get_gps_data(&mut self, timeout: Duration) -> Option { + // println!("Checking for GPS data (in get_gps_data)..."); + self.stream_rx.as_ref()?.recv_timeout(timeout).ok() + } + + pub fn open_port(port: PathBuf) -> Result { + match serialport::new(port.to_string_lossy(), Self::BAUD_RATE) + .timeout(std::time::Duration::from_millis(5000)) + .open_native() + { + Ok(base_gps_port) => { + println!("Successfully opened port {}", port.to_string_lossy()); + Ok(BaseGPS { + base_gps_port, + stream_rx: None, + dispatcher: Dispatcher::new(), + stop_signal: Arc::new(AtomicBool::new(false)), + }) + } + Err(e) => { + eprintln!( + "Failed to open \"{}\". Error: {}", + port.to_string_lossy(), + e + ); + Err(e) + } + } + } + + pub fn send_command(&mut self, command: PQTMCommand, timeout: Duration) -> Result { + let (wait_tx, wait_rx) = mpsc::channel(); + + // Register a waiter for the expected response: + + self.dispatcher.register_waiter(Box::new( + |m| match m { + WireMessage::PQTMMessage(PQTMResponse::Epe(_)) => false, + WireMessage::PQTMMessage(PQTMResponse::SvinStatus(_)) => false, + WireMessage::PQTMMessage(_) => true, + _ => false, + }), + wait_tx, + 1 + ); + + // Send command: + let sentence = command.to_sentence(); + self.write_all(sentence.as_bytes()).map_err(|_| ResponseError::ParseError(ParseError::ParsingError("writing to GPS port failed")))?; + + // Wait for response: + match wait_rx.recv_timeout(timeout) { + Ok(WireMessage::PQTMMessage(resp)) => Ok(resp), + Ok(_) => Err(ResponseError::ParseError(ParseError::ParsingError("unexpected message type received"))), + Err(_) => Err(ResponseError::ParseError(ParseError::ParsingError("timeout waiting for response"))), + } + } + + /// Sends a PAIR get command (e.g., PAIR433, PAIR435). + /// Waits for ACK, then waits for the actual response. + /// Returns both so you can validate the ACK and get the data. + pub fn send_pair_get( + &mut self, + command: PairCommand, + timeout: Duration, + ) -> Result<(PairACK, PairResponse), ResponseError> { + let (wait_tx, wait_rx) = mpsc::channel(); + + // Register ONCE for any PAIR message + self.dispatcher.register_waiter( + Box::new(|m| matches!(m, WireMessage::PairMessage(_))), + wait_tx, + 2 + ); + + let sentence = command.to_sentence(); + self.write_all(sentence.as_bytes()) + .map_err(|_| ResponseError::ParseError(ParseError::ParsingError("write failed")))?; + + // Wait for ACK first + let ack = match wait_rx.recv_timeout(timeout) { + Ok(WireMessage::PairMessage(PairResponse::ACK(ack))) => { + if ack.result != AckResult::Success { + return Err(ResponseError::ParseError(ParseError::ParsingError("ACK failed"))); + } + ack + } + Ok(_) => return Err(ResponseError::ParseError(ParseError::ParsingError("expected ACK, got something else"))), + Err(_) => return Err(ResponseError::ParseError(ParseError::ParsingError("timeout waiting for ACK"))), + }; + + // Now wait for the actual response (same waiter, same channel) + match wait_rx.recv_timeout(timeout) { + Ok(WireMessage::PairMessage(resp)) => Ok((ack, resp)), + Ok(_) => Err(ResponseError::ParseError(ParseError::ParsingError("unexpected message type"))), + Err(e) => Err(ResponseError::ParseError(ParseError::ParsingError("timeout waiting for response"))), + } + } + + pub fn send_pair_set( + &mut self, + command: PairCommand, + timeout: Duration, + ) -> Result { + let (wait_tx, wait_rx) = mpsc::channel(); + + // Wait for ACK only + self.dispatcher.register_waiter( + Box::new(|m| matches!(m, WireMessage::PairMessage(PairResponse::ACK(_)))), + wait_tx, + 1, + ); + + let sentence = command.to_sentence(); + self.write_all(sentence.as_bytes()) + .map_err(|_| ResponseError::ParseError(ParseError::ParsingError("write failed")))?; + + match wait_rx.recv_timeout(timeout) { + Ok(WireMessage::PairMessage(PairResponse::ACK(ack))) => { + if ack.result == AckResult::Success { + Ok(ack) + } else { + // ACK failed - return error with the ACK result embedded + Err(ResponseError::ParseError(ParseError::ParsingError("ACK failed"))) + } + } + Ok(_) => Err(ResponseError::ParseError(ParseError::ParsingError("unexpected message"))), + Err(_) => Err(ResponseError::ParseError(ParseError::ParsingError("timeout"))), + } + } + + fn rtk_reader_thread(&self) -> JoinHandle<()> { + let mut reader = BufReader::new( + self.base_gps_port + .try_clone_native() + .expect("Failed to clone GPS port"), + ); + let mut serial_buf: Vec = vec![0; 512]; + let stop_signal = self.stop_signal.clone(); + let mut parser = PQTMParser::new(); + let dispatcher = self.dispatcher.clone(); + + thread::spawn(move || { + while !stop_signal.load(Ordering::Acquire) { + match reader.read(&mut serial_buf) { + Ok(t) if t > 0 => { + let chunk = String::from_utf8_lossy(&serial_buf[..t]).into_owned(); + for msg in parser.parse_data(&chunk) { + // println!("Parsed GPS message: {:?}", msg); + dispatcher.dispatch(msg); + } + } + + Ok(_) => continue, // No data read, continue + Err(e) => { + eprintln!("Error reading from GPS port: {}", e); + break; + } + } + } + }) + } +} + +impl Write for BaseGPS { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.base_gps_port.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.base_gps_port.flush() + } +} diff --git a/rtkbase/src/protocol.rs b/rtkbase/src/protocol.rs new file mode 100644 index 0000000..d2e63d3 --- /dev/null +++ b/rtkbase/src/protocol.rs @@ -0,0 +1,5 @@ +pub mod commands; +mod helpers; +pub mod pair; +pub mod response; +pub mod sentence; diff --git a/rtkbase/src/protocol/commands.rs b/rtkbase/src/protocol/commands.rs new file mode 100644 index 0000000..98f4885 --- /dev/null +++ b/rtkbase/src/protocol/commands.rs @@ -0,0 +1,176 @@ +use crate::protocol::response::ParseError; + +/// Represents the commands which can be sent to the LC29H-BS device via PQTM sentences. +#[derive(Debug, Clone)] +pub enum PQTMCommand { + CfgSvinWrite(PQTMCfgSvin), + CfgSvinRead, + + SavePar, + + RestorePar, + + Verno, + + CfgMsgRateWrite(PQTMCfgMsgRate), + CfgMsgRateRead(PQTMCfgMsgRateGet), +} + +#[derive(Debug, Clone)] +pub struct PQTMCfgSvin { + pub mode: u8, // 0/1/2 + pub min_dur: u32, // seconds + pub acc_limit_m: f32, // meters + pub ecef_x: f64, + pub ecef_y: f64, + pub ecef_z: f64, +} + +#[derive(Debug, Clone)] +pub struct PQTMCfgMsgRate { + pub msg_name: PQTMMsgName, + pub rate: u8, + pub msg_ver: u8, +} + +#[derive(Debug, Clone)] +pub enum PQTMMsgName { + Epe, + SvinStatus, +} + +#[derive(Debug, Clone)] +pub struct PQTMCfgMsgRateGet { + pub msg_name: String, + pub msg_ver: String, +} + +impl PQTMCfgMsgRateGet { + pub fn to_fields(&self) -> String { + format!("PQTMCFGMSGRATE,R,{},{}", self.msg_name, self.msg_ver,) + } +} + +impl PQTMCfgMsgRate { + pub fn to_fields(&self) -> String { + format!( + "PQTMCFGMSGRATE,W,{},{},{}", + self.msg_name.clone().as_str(), + self.rate, + self.msg_ver, + ) + } + + pub fn from_fields<'a, I>(it: &mut I) -> Result + where + I: Iterator, + { + let msg_name_str = it + .next() + .ok_or(ParseError::ParsingError("msg_name not found"))?; + let msg_name = + PQTMMsgName::parse(msg_name_str).ok_or(ParseError::ParsingError("invalid msg_name"))?; + + let rate_str = it + .next() + .ok_or(ParseError::ParsingError("rate not found"))?; + let rate: u8 = rate_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid rate"))?; + + let msg_ver_str = it + .next() + .ok_or(ParseError::ParsingError("msg_ver not found"))?; + let msg_ver: u8 = msg_ver_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid msg_ver"))?; + + Ok(PQTMCfgMsgRate { + msg_name, + rate, + msg_ver, + }) + } +} + +impl PQTMCfgSvin { + pub fn to_fields(&self) -> String { + format!( + "PQTMCFGSVIN,W,{},{},{:.1},{:.4},{:.4},{:.4}", + self.mode, self.min_dur, self.acc_limit_m, self.ecef_x, self.ecef_y, self.ecef_z, + ) + } + + pub fn from_fields<'a, I>(it: &mut I) -> Result + where + I: Iterator, + { + let mode_str = it + .next() + .ok_or(ParseError::ParsingError("mode not found"))?; + let mode: u8 = mode_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid mode"))?; + + let min_dur_str = it + .next() + .ok_or(ParseError::ParsingError("min_dur not found"))?; + let min_dur: u32 = min_dur_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid min_dur"))?; + + let acc_limit_str = it + .next() + .ok_or(ParseError::ParsingError("acc_limit_m not found"))?; + let acc_limit_m: f32 = acc_limit_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid acc_limit_m"))?; + + let ecef_x_str = it + .next() + .ok_or(ParseError::ParsingError("ecef_x not found"))?; + let ecef_x: f64 = ecef_x_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid ecef_x"))?; + + let ecef_y_str = it + .next() + .ok_or(ParseError::ParsingError("ecef_y not found"))?; + let ecef_y: f64 = ecef_y_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid ecef_y"))?; + + let ecef_z_str = it + .next() + .ok_or(ParseError::ParsingError("ecef_z not found"))?; + let ecef_z: f64 = ecef_z_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid ecef_z"))?; + + Ok(PQTMCfgSvin { + mode, + min_dur, + acc_limit_m, + ecef_x, + ecef_y, + ecef_z, + }) + } +} + +impl PQTMMsgName { + pub fn as_str(self) -> &'static str { + match self { + PQTMMsgName::SvinStatus => "PQTMSVINSTATUS", + PQTMMsgName::Epe => "PQTMEPE", + } + } + + pub fn parse(s: &str) -> Option { + match s { + "PQTMSVINSTATUS" => Some(PQTMMsgName::SvinStatus), + "PQTMEPE" => Some(PQTMMsgName::Epe), + _ => None, + } + } +} diff --git a/rtkbase/src/protocol/helpers.rs b/rtkbase/src/protocol/helpers.rs new file mode 100644 index 0000000..ab13f41 --- /dev/null +++ b/rtkbase/src/protocol/helpers.rs @@ -0,0 +1,70 @@ +use crate::protocol::response::{PQTMModuleError, ParseError}; + +#[derive(Debug)] +pub enum StatusField<'a> { + Ok(core::str::Split<'a, &'a str>), + Err(PQTMModuleError), +} + +/// Parses the status field from a PQTM sentence. +/// The first part is expected to be the status ("OK" or "ERR"). +/// This avoids code duplication in the higher level. +/// Returns a StatusField enum containing the status and the remaining parts. +pub fn parse_status_and_rest<'a>( + mut parts: core::str::Split<'a, &'a str>, +) -> Result, ParseError> { + let status = parts.next().ok_or(ParseError::NoStatusField)?; + + match status { + "OK" => Ok(StatusField::Ok(parts)), + "ERROR" => { + let code_str = parts.next().ok_or(ParseError::NoErrorCode)?; + let code: u8 = code_str.parse().map_err(|_| ParseError::NoErrorCode)?; + let err = match code { + 1 => PQTMModuleError::InvalidParameters, + 2 => PQTMModuleError::ExecutionFailed, + _ => PQTMModuleError::Unknown(code), + }; + Ok(StatusField::Err(err)) + } + _ => Err(ParseError::InvalidStatusField), + } +} + +fn calc_checksum(input: &str) -> u8 { + input.bytes().fold(0u8, |acc, i| acc ^ i) +} + +pub fn wrap_sentence(payload: &str) -> String { + let checksum = calc_checksum(payload); + format!("${}*{:02X}\r\n", payload, checksum) +} + +/// Unwraps a PQTM sentence, verifying its checksum and returning the payload if valid. +/// Example: +/// let sentence = "$PQTMVERNO,1.0,2.5,3*4A\r\n"; +/// let payload = unwrap_sentence(sentence).unwrap(); +/// assert_eq!(payload, "PQTMVERNO,1.0,2.5,3"); +pub fn unwrap_sentence(sentence: &str) -> Result<&str, ParseError> { + // Trim \r\n: + let mut sentence = sentence.trim(); + sentence = sentence + .strip_prefix("$") + .ok_or(ParseError::StartDelimiterNotFound)?; + let (payload, checksum_str) = sentence + .split_once("*") + .ok_or(ParseError::ChecksumNotFound)?; + + if checksum_str.len() != 2 { + return Err(ParseError::ChecksumLengthInvalid); + } + + let expected_checksum = + u8::from_str_radix(checksum_str, 16).map_err(|_| ParseError::ChecksumLengthInvalid)?; + + if calc_checksum(payload) != expected_checksum { + return Err(ParseError::ChecksumMismatch); + } + + Ok(payload) +} diff --git a/rtkbase/src/protocol/pair.rs b/rtkbase/src/protocol/pair.rs new file mode 100644 index 0000000..eefeec6 --- /dev/null +++ b/rtkbase/src/protocol/pair.rs @@ -0,0 +1,291 @@ +use crate::protocol::{response::ParseError}; + +#[derive(Debug, Clone)] +pub enum PairCommand { + RtcmSetOutputMode(PairRTCMSetOutputMode), // PAIR432 + RtcmGetOutputMode, // PAIR433 + RtcmSetOutputAntPnt(PairRTCMSetOutputAntPnt), // PAIR434 + RtcmGetOutputAntPnt, // PAIR435 + RtcmSetOutputEphemeris(PairRTCMSetOutputEphemeris), // PAIR436 + RtcmGetOutputEphemeris, // PAIR437 +} + + +#[derive(Debug, Clone)] +pub enum PairResponse { + ACK(PairACK), // PAIR001 + RtcmOutputMode(PairRTCMSetOutputMode), // PAIR433 response + RtcmOutputAntPnt(PairRTCMSetOutputAntPnt), // PAIR435 response + RtcmOutputEphemeris(PairRTCMSetOutputEphemeris),// PAIR437 response + RequestAiding(PairRequestAiding), // PAIR010 + SystemWakeUp, // PAIR012 +} + + +#[derive(Debug, Clone)] +pub struct PairACK { + pub command_id: u16, + pub result: AckResult, +} + +impl PairACK { + pub fn from_fields<'a, I>(it: &mut I) -> Result + where + I: Iterator, + { + let command_id = it + .next() + .ok_or(ParseError::ParsingError("command_id not found"))?; + let command_id: u16 = command_id + .parse() + .map_err(|_| ParseError::ParsingError("invalid command_id"))?; + let result_str = it + .next() + .ok_or(ParseError::ParsingError("result not found"))?; + let result_u8: u8 = result_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid result"))?; + let result = match result_u8 { + 0 => AckResult::Success, + 1 => AckResult::Processing, + 2 => AckResult::Failed, + 3 => AckResult::NotSupported, + 4 => AckResult::Error, + 5 => AckResult::Busy, + _ => return Err(ParseError::ParsingError("invalid result value")), + }; + Ok(PairACK { + command_id, + result, + }) + + } + + pub fn to_fields(&self) -> String { + format!("PAIR001,{},{}", self.command_id, self.result.clone() as u8) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum AckResult { + Success = 0, + Processing = 1, + Failed = 2, + NotSupported = 3, + Error = 4, + Busy = 5, +} + +#[derive(Debug, Clone)] +pub struct PairRTCMSetOutputMode { + pub mode: RtcmMode, +} + +impl PairRTCMSetOutputMode { + pub fn from_fields<'a, I>(it: &mut I) -> Result + where + I: Iterator, + { + let mode_str = it + .next() + .ok_or(ParseError::ParsingError("mode not found"))?; + let mode_i8: i8 = mode_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid mode"))?; + let mode = match mode_i8 { + -1 => RtcmMode::Disable, + 0 => RtcmMode::Rtcm3Msm4, + 1 => RtcmMode::Rtcm3Msm7, + _ => return Err(ParseError::ParsingError("invalid mode value")), + }; + Ok(PairRTCMSetOutputMode { + mode, + }) + } + + pub fn to_fields(&self) -> String { + format!("PAIR432,{}", self.mode.clone() as i8) + } +} + +#[derive(Debug, Clone)] +pub enum RtcmMode { + Disable = -1, + Rtcm3Msm4 = 0, + Rtcm3Msm7 = 1, +} + +pub type PairRTCMGetOutputMode = PairRTCMSetOutputMode; + +/// Enable/disable outputting stationary RTK reference station ARP (message type 1005). +#[derive(Debug, Clone)] +pub struct PairRTCMSetOutputAntPnt { + pub ant_pnt: RtcmAntPnt, +} + +impl PairRTCMSetOutputAntPnt { + pub fn from_fields<'a, I>(it: &mut I) -> Result + where + I: Iterator, + { + let ant_pnt_str = it + .next() + .ok_or(ParseError::ParsingError("ant_pnt not found"))?; + let ant_pnt_u8: u8 = ant_pnt_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid ant_pnt"))?; + let ant_pnt = match ant_pnt_u8 { + 0 => RtcmAntPnt::Disable, + 1 => RtcmAntPnt::Enable, + _ => return Err(ParseError::ParsingError("invalid ant_pnt value")), + }; + Ok(PairRTCMSetOutputAntPnt { + ant_pnt, + }) + } + + pub fn to_fields(&self) -> String { + format!("PAIR434,{}", self.ant_pnt.clone() as u8) + } +} + +pub type PairRTCMGetOutputAntPnt = PairRTCMSetOutputAntPnt; + +#[derive(Debug, Clone)] +pub enum RtcmAntPnt { + Disable = 0, + Enable = 1, +} + +#[derive(Debug, Clone)] +pub struct PairRTCMSetOutputEphemeris { + pub ephemeris: RtcmEphemeris, +} + +impl PairRTCMSetOutputEphemeris { + pub fn from_fields<'a, I>(it: &mut I) -> Result + where + I: Iterator, + { + let ephemeris_str = it + .next() + .ok_or(ParseError::ParsingError("ephemeris not found"))?; + let ephemeris_u8: u8 = ephemeris_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid ephemeris"))?; + let ephemeris = match ephemeris_u8 { + 0 => RtcmEphemeris::Disable, + 1 => RtcmEphemeris::Enable, + _ => return Err(ParseError::ParsingError("invalid ephemeris value")), + }; + Ok(PairRTCMSetOutputEphemeris { + ephemeris, + }) + } + + pub fn to_fields(&self) -> String { + format!("PAIR436,{}", self.ephemeris.clone() as u8) + } +} + +pub type PairRTCMGetOutputEphemeris = PairRTCMSetOutputEphemeris; + +#[derive(Debug, Clone)] +pub enum RtcmEphemeris { + Disable = 0, + Enable = 1, +} + +#[derive(Debug, Clone)] +pub struct PairRequestAiding { + /// Type of data to be updated + pub aiding_type: AidingType, + /// Type of required GNSS data + pub gnss_system: GnssSystem, + /// Week number (accommodating rollover) + pub week_number: u16, + /// Time of week in seconds + pub time_of_week: u64, +} + +impl PairRequestAiding { + pub fn from_fields<'a, I>(it: &mut I) -> Result + where + I: Iterator, + { + let aiding_type_str = it + .next() + .ok_or(ParseError::ParsingError("aiding_type not found"))?; + let aiding_type_u8: u8 = aiding_type_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid aiding_type"))?; + let aiding_type = match aiding_type_u8 { + 0 => AidingType::EpoData, + 1 => AidingType::Time, + 2 => AidingType::Location, + _ => return Err(ParseError::ParsingError("invalid aiding_type value")), + }; + + let gnss_system_str = it + .next() + .ok_or(ParseError::ParsingError("gnss_system not found"))?; + let gnss_system_u8: u8 = gnss_system_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid gnss_system"))?; + let gnss_system = match gnss_system_u8 { + 0 => GnssSystem::Gps, + 1 => GnssSystem::Glonass, + 2 => GnssSystem::Galileo, + 3 => GnssSystem::BeiDou, + 4 => GnssSystem::Qzss, + _ => return Err(ParseError::ParsingError("invalid gnss_system value")), + }; + + let week_number_str = it + .next() + .ok_or(ParseError::ParsingError("week_number not found"))?; + let week_number: u16 = week_number_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid week_number"))?; + + let time_of_week_str = it + .next() + .ok_or(ParseError::ParsingError("time_of_week not found"))?; + let time_of_week: u64 = time_of_week_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid time_of_week"))?; + + Ok(PairRequestAiding { + aiding_type, + gnss_system, + week_number, + time_of_week, + }) + } + + pub fn to_fields(&self) -> String { + format!( + "PAIR010,{},{},{},{}", + self.aiding_type.clone() as u8, + self.gnss_system.clone() as u8, + self.week_number, + self.time_of_week, + ) + } +} + +#[derive(Debug, Clone)] +pub enum AidingType { + EpoData = 0, + Time = 1, + Location = 2, +} + +#[derive(Debug, Clone)] +pub enum GnssSystem { + Gps = 0, + Glonass = 1, + Galileo = 2, + BeiDou = 3, + Qzss = 4, +} diff --git a/rtkbase/src/protocol/response.rs b/rtkbase/src/protocol/response.rs new file mode 100644 index 0000000..44b1d75 --- /dev/null +++ b/rtkbase/src/protocol/response.rs @@ -0,0 +1,277 @@ +use crate::protocol::pair::PairResponse; + +use super::commands::{PQTMCfgMsgRate, PQTMCfgSvin}; + +#[derive(Debug, Clone)] +pub enum WireMessage { + PQTMMessage(PQTMResponse), + PairMessage(PairResponse), +} + + +/// Represents the output from the LC29H-BS device. +#[derive(Debug, Clone)] +pub enum PQTMResponse { + CfgSvinWriteOk, + CfgSvinReadOk(PQTMCfgSvin), + CfgSvinError(PQTMModuleError), + + SaveParOk, + SaveParError(PQTMModuleError), + + RestoreParOk, + RestoreParError(PQTMModuleError), + + Verno(PQTMVerNo), + VernoError(PQTMModuleError), + + CfgMsgRateWriteOk, + CfgMsgRateReadOk(PQTMCfgMsgRate), + CfgMsgRateError(PQTMModuleError), + + Epe(PQTMEpe), + SvinStatus(PQTMSvinStatus), +} + +/// Represents errors returned by the GPS module. +#[derive(Debug, Clone)] +pub enum PQTMModuleError { + InvalidParameters, + ExecutionFailed, + Unknown(u8), +} + +/// Represents errors that can occur when parsing a PQTM sentence. +#[derive(Debug, Clone)] +pub enum ParseError { + StartDelimiterNotFound, + NoSentence, + NoStatusField, + InvalidStatusField, + ChecksumNotFound, + ChecksumLengthInvalid, + ChecksumMismatch, + NoErrorCode, + ParsingError(&'static str), +} + +/// Represents errors that can occur when processing a PQTM response. +#[derive(Debug, Clone)] +pub enum ResponseError { + ModuleError(PQTMModuleError), + ParseError(ParseError), +} + +#[derive(Debug, Clone)] +pub struct PQTMSvinStatus { + _msg_ver: String, + pub time_of_week: u64, // ms + pub valid: u8, // 0 - invalid, 1 - in-progress, 2 - valid + _reserved1: String, + _reserved2: String, + pub observations: u32, + pub config_duration: u32, + pub mean_x: f64, // mean position in ECEF (m) + pub mean_y: f64, // mean position in ECEF (m) + pub mean_z: f64, // mean position in ECEF (m) + pub mean_acc: f32, // mean accuracy (m) +} + +#[derive(Debug, Clone)] +pub struct PQTMEpe { + _msg_ver: String, + pub epe_north: f32, // North position error (m) + pub epe_east: f32, // East position error (m) + pub epe_down: f32, // Down position error (m) + pub epe_2d: f32, // 2D position error (m) + pub epe_3d: f32, // 3D position error (m) +} + +#[derive(Debug, Clone)] +pub struct PQTMVerNo { + pub version: String, + pub build_date: String, + pub build_time: String, +} + +impl PQTMVerNo { + pub fn from_fields<'a, I>(it: &mut I) -> Result + where + I: Iterator, + { + let version = it + .next() + .ok_or(ParseError::ParsingError("version not found"))? + .to_string(); + let build_date = it + .next() + .ok_or(ParseError::ParsingError("build_date not found"))? + .to_string(); + let build_time = it + .next() + .ok_or(ParseError::ParsingError("build_time not found"))? + .to_string(); + Ok(PQTMVerNo { + version, + build_date, + build_time, + }) + } +} + +impl PQTMEpe { + pub fn from_fields<'a, I>(it: &mut I) -> Result + where + I: Iterator, + { + let _msg_ver = it + .next() + .ok_or(ParseError::ParsingError("msg_ver not found"))? + .to_string(); + + let epe_north_str = it + .next() + .ok_or(ParseError::ParsingError("epe_north not found"))?; + let epe_north: f32 = epe_north_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid epe_north"))?; + + let epe_east_str = it + .next() + .ok_or(ParseError::ParsingError("epe_east not found"))?; + let epe_east: f32 = epe_east_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid epe_east"))?; + + let epe_down_str = it + .next() + .ok_or(ParseError::ParsingError("epe_down not found"))?; + let epe_down: f32 = epe_down_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid epe_down"))?; + + let epe_2d_str = it + .next() + .ok_or(ParseError::ParsingError("epe_2d not found"))?; + let epe_2d: f32 = epe_2d_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid epe_2d"))?; + + let epe_3d_str = it + .next() + .ok_or(ParseError::ParsingError("epe_3d not found"))?; + let epe_3d: f32 = epe_3d_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid epe_3d"))?; + + Ok(PQTMEpe { + _msg_ver, + epe_north, + epe_east, + epe_down, + epe_2d, + epe_3d, + }) + } +} + +impl PQTMSvinStatus { + pub fn from_fields<'a, I>(it: &mut I) -> Result + where + I: Iterator, + { + let _msg_ver = it + .next() + .ok_or(ParseError::ParsingError("msg_ver not found"))? + .to_string(); + + let time_of_week_str = it + .next() + .ok_or(ParseError::ParsingError("time_of_week not found"))?; + let time_of_week: u64 = time_of_week_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid time_of_week"))?; + + let valid_str = it + .next() + .ok_or(ParseError::ParsingError("valid not found"))?; + let valid: u8 = valid_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid valid"))?; + + let _reserved1 = it + .next() + .ok_or(ParseError::ParsingError("reserved1 not found"))? + .to_string(); + + let _reserved2 = it + .next() + .ok_or(ParseError::ParsingError("reserved2 not found"))? + .to_string(); + + let observations_str = it + .next() + .ok_or(ParseError::ParsingError("observations not found"))?; + let observations: u32 = observations_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid observations"))?; + + let config_duration_str = it + .next() + .ok_or(ParseError::ParsingError("config_duration not found"))?; + let config_duration: u32 = config_duration_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid config_duration"))?; + + let mean_x_str = it + .next() + .ok_or(ParseError::ParsingError("mean_x not found"))?; + let mean_x: f64 = mean_x_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid mean_x"))?; + + let mean_y_str = it + .next() + .ok_or(ParseError::ParsingError("mean_y not found"))?; + let mean_y: f64 = mean_y_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid mean_y"))?; + + let mean_z_str = it + .next() + .ok_or(ParseError::ParsingError("mean_z not found"))?; + let mean_z: f64 = mean_z_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid mean_z"))?; + + let mean_acc_str = it + .next() + .ok_or(ParseError::ParsingError("mean_acc not found"))?; + let mean_acc: f32 = mean_acc_str + .parse() + .map_err(|_| ParseError::ParsingError("invalid mean_acc"))?; + Ok(PQTMSvinStatus { + _msg_ver, + time_of_week, + valid, + _reserved1, + _reserved2, + observations, + config_duration, + mean_x, + mean_y, + mean_z, + mean_acc, + }) + } +} + +impl From for PQTMModuleError { + fn from(code: u8) -> Self { + match code { + 1 => PQTMModuleError::InvalidParameters, + 2 => PQTMModuleError::ExecutionFailed, + _ => PQTMModuleError::Unknown(code), + } + } +} diff --git a/rtkbase/src/protocol/sentence.rs b/rtkbase/src/protocol/sentence.rs new file mode 100644 index 0000000..f2d6270 --- /dev/null +++ b/rtkbase/src/protocol/sentence.rs @@ -0,0 +1,152 @@ +use crate::protocol::commands::{PQTMCfgMsgRate, PQTMCfgSvin}; +use crate::protocol::helpers::{StatusField, parse_status_and_rest, wrap_sentence}; +use crate::protocol::response::{PQTMEpe, PQTMResponse, PQTMSvinStatus, PQTMVerNo, ParseError, PQTMModuleError}; +use crate::protocol::pair::{PairCommand, PairResponse, PairRequestAiding, PairACK, PairRTCMSetOutputMode, PairRTCMSetOutputAntPnt, PairRTCMSetOutputEphemeris}; + +use super::commands::PQTMCommand; +use super::helpers::unwrap_sentence; + +pub trait Serialize { + fn to_sentence(&self) -> String; +} + +pub trait Deserialize: Sized { + type Error; + fn from_sentence(s: &str) -> Result; +} + +impl Serialize for PQTMCommand { + fn to_sentence(&self) -> String { + match self { + PQTMCommand::CfgSvinWrite(cfg) => wrap_sentence(&cfg.to_fields()), + PQTMCommand::CfgSvinRead => wrap_sentence("PQTMCFGSVIN,R"), + PQTMCommand::SavePar => wrap_sentence("PQTMSAVEPAR"), + PQTMCommand::RestorePar => wrap_sentence("PQTMRESTOREPAR"), + PQTMCommand::Verno => wrap_sentence("PQTMVERNO"), + PQTMCommand::CfgMsgRateWrite(cfg) => wrap_sentence(&cfg.to_fields()), + PQTMCommand::CfgMsgRateRead(cfg_get) => wrap_sentence(&cfg_get.to_fields()), + } + } +} + +impl Deserialize for PQTMResponse { + type Error = ParseError; + + fn from_sentence(s: &str) -> Result { + let payload = unwrap_sentence(s)?; + let mut parts = payload.split(","); + let header = parts.next().ok_or(ParseError::NoSentence)?; + + match header { + "PQTMSAVEPAR" => match parse_status_and_rest(parts)? { + StatusField::Ok(_) => Ok(PQTMResponse::SaveParOk), + StatusField::Err(e) => Ok(PQTMResponse::SaveParError(e)), + }, + "PQTMRESTOREPAR" => match parse_status_and_rest(parts)? { + StatusField::Ok(_) => Ok(PQTMResponse::RestoreParOk), + StatusField::Err(e) => Ok(PQTMResponse::RestoreParError(e)), + }, + "PQTMVERNO" => { // This does unfortunately not follow the OK/ERROR pattern + // Check if first field is "ERROR" + let first = parts.clone().next().ok_or(ParseError::NoSentence)?; + if first == "ERROR" { + parts.next(); // skip "ERROR" + let code: u8 = parts.next() + .ok_or(ParseError::NoErrorCode)? + .parse() + .map_err(|_| ParseError::NoErrorCode)?; + Ok(PQTMResponse::VernoError(PQTMModuleError::from(code))) + } else { + // Direct data: VerStr,BuildDate,BuildTime + let verno = PQTMVerNo::from_fields(&mut parts)?; + Ok(PQTMResponse::Verno(verno)) + } + }, + "PQTMCFGSVIN" => { + match parse_status_and_rest(parts)? { + StatusField::Ok(mut rest) => { + if rest.clone().next().is_none() { + // Write response: OK only: + Ok(PQTMResponse::CfgSvinWriteOk) + } else { + // Read response: + Ok(PQTMResponse::CfgSvinReadOk(PQTMCfgSvin::from_fields( + &mut rest, + )?)) + } + } + StatusField::Err(e) => Ok(PQTMResponse::CfgSvinError(e)), + } + } + "PQTMCFGMSGRATE" => { + match parse_status_and_rest(parts)? { + StatusField::Ok(mut rest) => { + if rest.clone().next().is_none() { + // Write response: OK only: + Ok(PQTMResponse::CfgMsgRateWriteOk) + } else { + // Read response: + Ok(PQTMResponse::CfgMsgRateReadOk(PQTMCfgMsgRate::from_fields( + &mut rest, + )?)) + } + } + StatusField::Err(e) => Ok(PQTMResponse::CfgMsgRateError(e)), + } + } + "PQTMEPE" => Ok(PQTMResponse::Epe(PQTMEpe::from_fields(&mut parts)?)), + "PQTMSVINSTATUS" => Ok(PQTMResponse::SvinStatus(PQTMSvinStatus::from_fields( + &mut parts, + )?)), + _ => Err(ParseError::ParsingError("Unknown sentence header")), + } + } +} + +impl Serialize for PairCommand { + fn to_sentence(&self) -> String { + match self { + PairCommand::RtcmSetOutputMode(cfg) => wrap_sentence(&cfg.to_fields()), + PairCommand::RtcmGetOutputMode => wrap_sentence("PAIR433"), + PairCommand::RtcmSetOutputAntPnt(cfg) => wrap_sentence(&cfg.to_fields()), + PairCommand::RtcmGetOutputAntPnt => wrap_sentence("PAIR435"), + PairCommand::RtcmSetOutputEphemeris(cfg) => wrap_sentence(&cfg.to_fields()), + PairCommand::RtcmGetOutputEphemeris => wrap_sentence("PAIR437"), + } + } +} + +impl Deserialize for PairResponse { + type Error = ParseError; + + fn from_sentence(s: &str) -> Result { + let payload = unwrap_sentence(s)?; + let mut parts = payload.split(','); + let header = parts.next().ok_or(ParseError::NoSentence)?; + + match header { + "PAIR001" => { + let ack = PairACK::from_fields(&mut parts)?; + Ok(PairResponse::ACK(ack)) + } + "PAIR433" => { + let mode = PairRTCMSetOutputMode::from_fields(&mut parts)?; + Ok(PairResponse::RtcmOutputMode(mode)) + } + "PAIR435" => { + let antpnt = PairRTCMSetOutputAntPnt::from_fields(&mut parts)?; + Ok(PairResponse::RtcmOutputAntPnt(antpnt)) + } + "PAIR437" => { + let ephemeris = PairRTCMSetOutputEphemeris::from_fields(&mut parts)?; + Ok(PairResponse::RtcmOutputEphemeris(ephemeris)) + } + "PAIR012" => Ok(PairResponse::SystemWakeUp), + "PAIR010" => { + let aiding = PairRequestAiding::from_fields(&mut parts)?; + Ok(PairResponse::RequestAiding(aiding)) + } + _ => Err(ParseError::ParsingError("Unknown PAIR sentence header")), + } + } +} \ No newline at end of file diff --git a/src/gps/parser.rs b/src/gps/parser.rs index 854732f..9e2148a 100644 --- a/src/gps/parser.rs +++ b/src/gps/parser.rs @@ -1,7 +1,6 @@ use nmea::{self, Nmea}; pub fn build_parser() -> Nmea { - Nmea::default() } diff --git a/src/logging.rs b/src/logging.rs index c39667d..92f2051 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -116,9 +116,10 @@ impl Logger { /// Log a NMEA sentence pub fn log_nmea(&self, parser: Nmea, gga_fix_quality: Option) { - let _ = self - .tx - .send(LoggerPackets::NmeaSentence(Box::new(parser), gga_fix_quality)); + let _ = self.tx.send(LoggerPackets::NmeaSentence( + Box::new(parser), + gga_fix_quality, + )); } /// Log RTCM correction data diff --git a/src/main.rs b/src/main.rs index 5bce48e..95be8c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,11 +5,13 @@ use std::io::{Write, stdout}; use std::path::PathBuf; use std::sync::mpsc::{self}; +use ntrip; + mod display; mod gps; mod gps_serial; mod logging; -mod ntrip; +// mod ntrip; mod usb_serial; #[derive(Parser, Debug)] @@ -100,16 +102,15 @@ fn main() -> std::io::Result<()> { // If the time has changed, it means we've started a new epoch. // We should log the *previous* epoch's fully accumulated data. - if next_parser.fix_time != parser.fix_time - && parser.fix_time.is_some() { - logger.log_nmea(parser.clone(), gga_fix_quality.clone()); - display.update_gps( - &mut stdout, - &parser, - gga_fix_quality.clone(), - &ntrip_status, - )?; - } + if next_parser.fix_time != parser.fix_time && parser.fix_time.is_some() { + logger.log_nmea(parser.clone(), gga_fix_quality.clone()); + display.update_gps( + &mut stdout, + &parser, + gga_fix_quality.clone(), + &ntrip_status, + )?; + } parser = next_parser; gga_fix_quality = next_gga_fix_quality; diff --git a/src/usb_serial.rs b/src/usb_serial.rs index e4316ec..2aec347 100644 --- a/src/usb_serial.rs +++ b/src/usb_serial.rs @@ -5,8 +5,7 @@ use std::io::{self, BufReader, Read, Write}; use std::path::PathBuf; use std::time::Duration; -#[derive(Debug, Clone, Serialize)] -#[derive(Default)] +#[derive(Debug, Clone, Serialize, Default)] pub struct SensorData { #[serde(skip_deserializing)] pub timestamp_ns: u64, @@ -31,7 +30,6 @@ pub struct SensorData { pub rotations_right: Option, } - #[derive(Debug)] pub struct ArduinoSerialPort { reader: BufReader, // Buffered reader for line-based reads