From 2fd972fa77b8d77d8e3ab0e69da9d2046f4eea02 Mon Sep 17 00:00:00 2001 From: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com> Date: Sun, 12 Jan 2025 11:46:32 +0100 Subject: [PATCH] add a simple example TUI based on ratatui - show NAV-PVT msg as in u-center - show ESF-STATUS and ESF-ALG statuses similar to u-center - show MON-VER - a few other things are work in progress, such as: show a graph of the sensor values, properly capture the log using tracing and show it in the UI Signed-off-by: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com> --- examples/adr-message-parsing/src/main.rs | 4 +- examples/tui/Cargo.toml | 31 ++ examples/tui/README.md | 17 + examples/tui/images/ublox-tui.png | Bin 0 -> 111490 bytes examples/tui/src/app.rs | 244 +++++++++ examples/tui/src/cli.rs | 172 +++++++ examples/tui/src/device.rs | 452 +++++++++++++++++ examples/tui/src/logging.rs | 116 +++++ examples/tui/src/main.rs | 44 ++ examples/tui/src/tui.rs | 136 ++++++ examples/tui/src/ui.rs | 597 +++++++++++++++++++++++ 11 files changed, 1811 insertions(+), 2 deletions(-) create mode 100644 examples/tui/Cargo.toml create mode 100644 examples/tui/README.md create mode 100644 examples/tui/images/ublox-tui.png create mode 100644 examples/tui/src/app.rs create mode 100644 examples/tui/src/cli.rs create mode 100644 examples/tui/src/device.rs create mode 100644 examples/tui/src/logging.rs create mode 100644 examples/tui/src/main.rs create mode 100644 examples/tui/src/tui.rs create mode 100644 examples/tui/src/ui.rs diff --git a/examples/adr-message-parsing/src/main.rs b/examples/adr-message-parsing/src/main.rs index 352942d..6fde7a0 100644 --- a/examples/adr-message-parsing/src/main.rs +++ b/examples/adr-message-parsing/src/main.rs @@ -173,10 +173,10 @@ fn main() { ) .expect("Could not write UBX-CFG-ESFALG msg due to: {e}"); - // Send a packet request for the MonVer packet + // Send packet request to read the new CfgEsfAlg device .write_all(&UbxPacketRequest::request_for::().into_packet_bytes()) - .expect("Unable to write request/poll for CFG-ESFALG message"); + .expect("Unable to write request/poll for UBX-CFG-ESFALG message"); // Start reading data println!("Opened uBlox device, waiting for messages..."); diff --git a/examples/tui/Cargo.toml b/examples/tui/Cargo.toml new file mode 100644 index 0000000..69c5031 --- /dev/null +++ b/examples/tui/Cargo.toml @@ -0,0 +1,31 @@ +[package] +authors = ["Andrei Gherghescu "] +edition = "2021" +name = "ublox-tui" +publish = false +rust-version = "1.70" +version = "0.0.1" + +[dependencies] +anyhow = "1.0" +chrono = "0.4" +clap = { version = "4.5.23", features = ["derive", "cargo"] } +crossterm = { version = "0.28", features = ["event-stream"] } +env_logger = "0.11" +indoc = "2" +log = "0.4" +ratatui = "0.29" +serialport = "4.2" +strum = { version = "0.26", features = ["derive"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +unicode-width = "0.2" +lazy_static = "1.5" +tracing-error = "0.2" +tui-logger = { version = "0.14", features = ["crossterm", "tracing-support"] } +directories = "0.10" + +ublox = { path = "../../ublox" } + +[features] +alloc = ["ublox/alloc"] diff --git a/examples/tui/README.md b/examples/tui/README.md new file mode 100644 index 0000000..8b39b97 --- /dev/null +++ b/examples/tui/README.md @@ -0,0 +1,17 @@ +# TUI example + +This TUI is based on the [Ratatui demo app](https://github.com/ratatui/ratatui/tree/main/examples/apps/demo) + +It is implemented ony for the `crossterm` backend. + +It will show the NAV-PVT and ESF-ALG, ESF-STATUS messages similar to the u-center UI from uBlox. + +```shell +cargo run -p /dev/ttyACM0 +``` + +You should see a TUI like this + +![images/ublox-tui.png](images/ublox-tui.png) + + diff --git a/examples/tui/images/ublox-tui.png b/examples/tui/images/ublox-tui.png new file mode 100644 index 0000000000000000000000000000000000000000..7e9e053b97ee44752845edc007378f85018ffa0b GIT binary patch literal 111490 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 G { + pub titles: Vec<&'a str>, + pub index: usize, +} + +impl<'a> TabsState<'a> { + pub const fn new(titles: Vec<&'a str>) -> Self { + Self { titles, index: 0 } + } + pub fn next(&mut self) { + self.index = (self.index + 1) % self.titles.len(); + } + + pub fn previous(&mut self) { + if self.index > 0 { + self.index -= 1; + } else { + self.index = self.titles.len() - 1; + } + } +} + +pub enum UbxStatus { + Pvt(Box), + MonVer(Box), + EsfAlgImu(EsfAlgImuAlignmentWidgetState), + EsfAlgSensors(EsfSensorsWidgetState), + EsfAlgStatus(EsfAlgStatusWidgetState), +} + +#[derive(Debug)] +#[allow(dead_code)] +pub struct NavPvtWidgetState { + pub time_tag: f64, + pub year: u16, + pub month: u8, + pub day: u8, + pub hour: u8, + pub min: u8, + pub sec: u8, + pub valid: u8, + pub time_accuracy: u32, + pub nanosecond: i32, + pub utc_time_accuracy: u32, + pub lat: f64, + pub lon: f64, + pub height: f64, + pub msl: f64, + pub vel_ned: (f64, f64, f64), + pub speed_over_ground: f64, + pub heading_motion: f64, + pub heading_vehicle: f64, + pub magnetic_declination: f64, + + pub pdop: f64, + pub satellites_used: u8, + + pub position_fix_type: GpsFix, + pub fix_flags: NavPvtFlags, + pub invalid_llh: bool, + pub position_accuracy: (f64, f64), + pub velocity_accuracy: f64, + pub heading_accuracy: f64, + pub magnetic_declination_accuracy: f64, + pub flags2: NavPvtFlags2, +} + +impl Default for NavPvtWidgetState { + fn default() -> Self { + Self { + time_tag: f64::NAN, + year: 0, + month: 0, + day: 0, + hour: 0, + min: 0, + sec: 0, + valid: 0, + time_accuracy: 0, + nanosecond: 0, + lat: f64::NAN, + lon: f64::NAN, + height: f64::NAN, + msl: f64::NAN, + vel_ned: (f64::NAN, f64::NAN, f64::NAN), + speed_over_ground: f64::NAN, + heading_motion: f64::NAN, + heading_vehicle: f64::NAN, + magnetic_declination: f64::NAN, + pdop: f64::NAN, + satellites_used: 0, + utc_time_accuracy: 0, + invalid_llh: true, + position_accuracy: (f64::NAN, f64::NAN), + velocity_accuracy: f64::NAN, + heading_accuracy: f64::NAN, + magnetic_declination_accuracy: f64::NAN, + position_fix_type: GpsFix::NoFix, + fix_flags: NavPvtFlags::empty(), + flags2: NavPvtFlags2::empty(), + } + } +} + +#[derive(Debug, Default)] +pub struct EsfSensorsWidgetState { + pub sensors: Vec, +} + +#[derive(Debug, Clone)] +pub struct EsfSensorWidget { + pub sensor_type: EsfSensorType, + pub calib_status: EsfSensorStatusCalibration, + pub time_status: EsfSensorStatusTime, + pub freq: u16, + pub faults: EsfSensorFaults, +} + +impl Default for EsfSensorWidget { + fn default() -> Self { + Self { + sensor_type: EsfSensorType::Unknown, + calib_status: EsfSensorStatusCalibration::NotCalibrated, + time_status: EsfSensorStatusTime::NoData, + freq: 0, + faults: EsfSensorFaults::default(), + } + } +} + +pub struct EsfAlgStatusWidgetState { + pub time_tag: f64, + pub fusion_mode: EsfStatusFusionMode, + pub imu_status: EsfStatusImuInit, + pub wheel_tick_sensor_status: EsfStatusWheelTickInit, + pub ins_status: EsfStatusInsInit, + pub imu_mount_alignment_status: EsfStatusMountAngle, +} + +impl Default for EsfAlgStatusWidgetState { + fn default() -> Self { + Self { + time_tag: f64::NAN, + fusion_mode: EsfStatusFusionMode::Disabled, + imu_status: EsfStatusImuInit::Off, + wheel_tick_sensor_status: EsfStatusWheelTickInit::Off, + ins_status: EsfStatusInsInit::Off, + imu_mount_alignment_status: EsfStatusMountAngle::Off, + } + } +} +#[derive(Debug)] +pub struct EsfAlgImuAlignmentWidgetState { + pub time_tag: f64, + pub auto_alignment: bool, + pub alignment_status: EsfAlgStatus, + pub angle_singularity: bool, + pub roll: f64, + pub pitch: f64, + pub yaw: f64, +} + +impl Default for EsfAlgImuAlignmentWidgetState { + fn default() -> Self { + Self { + time_tag: f64::NAN, + auto_alignment: false, + alignment_status: EsfAlgStatus::UserDefinedAngles, + angle_singularity: false, + roll: f64::NAN, + pitch: f64::NAN, + yaw: f64::NAN, + } + } +} + +#[derive(Debug, Default)] +pub struct MonVersionWidgetState { + pub software_version: [u8; 30], + pub hardware_version: [u8; 10], + pub extensions: String, +} + +#[allow(dead_code)] +pub struct App<'a> { + pub title: &'a str, + pub log_file: PathBuf, + pub pvt_state: NavPvtWidgetState, + pub mon_ver_state: MonVersionWidgetState, + pub esf_sensors_state: EsfSensorsWidgetState, + pub esf_alg_state: EsfAlgStatusWidgetState, + pub esf_alg_imu_alignment_state: EsfAlgImuAlignmentWidgetState, + pub should_quit: bool, + pub tabs: TabsState<'a>, + pub log_widget: LogWidget, +} + +impl<'a> App<'a> { + pub fn new(title: &'a str, log_file: PathBuf) -> Self { + App { + title, + log_file, + pvt_state: NavPvtWidgetState::default(), + mon_ver_state: MonVersionWidgetState::default(), + esf_sensors_state: EsfSensorsWidgetState::default(), + esf_alg_state: EsfAlgStatusWidgetState::default(), + esf_alg_imu_alignment_state: EsfAlgImuAlignmentWidgetState::default(), + should_quit: false, + log_widget: LogWidget, + tabs: TabsState::new(vec!["PVT & ESF Status", "Version Info", "World Map"]), + } + } + + pub fn on_right(&mut self) { + self.tabs.next(); + } + + pub fn on_left(&mut self) { + self.tabs.previous(); + } + + pub fn on_key(&mut self, c: char) { + match c { + 'q' => { + self.should_quit = true; + }, + 'Q' => { + self.should_quit = true; + }, + _ => {}, + } + } +} diff --git a/examples/tui/src/cli.rs b/examples/tui/src/cli.rs new file mode 100644 index 0000000..9ea205c --- /dev/null +++ b/examples/tui/src/cli.rs @@ -0,0 +1,172 @@ +use clap::{value_parser, Arg, Command}; + +pub fn parse_args() -> clap::ArgMatches { + Command::new("uBlox TUI") + .author(clap::crate_authors!()) + .about("Simple TUI to show PVT and ESF statuses") + .arg_required_else_help(true) + .arg( + Arg::new("debug-mode") + .value_name("debug-mode") + .long("debug-mode") + .action(clap::ArgAction::SetTrue) + .help("Bypass TUI altogether and run the u-blox connection only. Useful for debugging issues with u-blox connectivity and message parsing."), + ) + .arg( + Arg::new("log-file") + .value_name("log-file") + .long("log-file") + .action(clap::ArgAction::SetTrue) + .help("Log to file besides showing partial logs in the TUI"), + ) + .arg( + Arg::new("tui-rate") + .value_name("tui-rate") + .long("tui-rate") + .required(false) + .default_value("100") + .value_parser(value_parser!(u64)) + .help("TUI refresh rate in milliseconds"), + ) + .arg( + Arg::new("port") + .value_name("port") + .short('p') + .long("port") + .required(true) + .help("Serial port to open"), + ) + .arg( + Arg::new("baud") + .value_name("baud") + .short('s') + .long("baud") + .required(false) + .default_value("9600") + .value_parser(value_parser!(u32)) + .help("Baud rate of the port to open"), + ) + .arg( + Arg::new("stop-bits") + .long("stop-bits") + .help("Number of stop bits to use for opened port") + .required(false) + .value_parser(["1", "2"]) + .default_value("1"), + ) + .arg( + Arg::new("data-bits") + .long("data-bits") + .help("Number of data bits to use for opened port") + .required(false) + .value_parser(["7", "8"]) + .default_value("8"), + ) + .arg( + Arg::new("parity") + .long("parity") + .help("Parity to use for open port") + .required(false) + .value_parser(["even", "odd"]), + ) + .subcommand( + Command::new("configure") + .about("Configure settings for specific UART/USB port") + .arg( + Arg::new("port") + .long("select") + .required(true) + .default_value("usb") + .value_parser(value_parser!(String)) + .long_help( + "Apply specific configuration to the selected port. Supported: usb, uart1, uart2. +Configuration includes: protocol in/out, data-bits, stop-bits, parity, baud-rate", + ), + ) + .arg( + Arg::new("cfg-baud") + .value_name("baud") + .long("baud") + .required(false) + .default_value("9600") + .value_parser(value_parser!(u32)) + .help("Baud rate to set"), + ) + .arg( + Arg::new("stop-bits") + .long("stop-bits") + .help("Number of stop bits to set") + .required(false) + .value_parser(["1", "2"]) + .default_value("1"), + ) + .arg( + Arg::new("data-bits") + .long("data-bits") + .help("Number of data bits to set") + .required(false) + .value_parser(["7", "8"]) + .default_value("8"), + ) + .arg( + Arg::new("parity") + .long("parity") + .help("Parity to set") + .required(false) + .value_parser(["even", "odd"]), + ) + .arg( + Arg::new("in-ublox") + .long("in-ublox") + .default_value("true") + .action(clap::ArgAction::SetTrue) + .help("Toggle receiving UBX proprietary protocol on port"), + ) + .arg( + Arg::new("in-nmea") + .long("in-nmea") + .default_value("false") + .action(clap::ArgAction::SetTrue) + .help("Toggle receiving NMEA protocol on port"), + ) + .arg( + Arg::new("in-rtcm") + .long("in-rtcm") + .default_value("false") + .action(clap::ArgAction::SetTrue) + .help("Toggle receiving RTCM protocol on port"), + ) + .arg( + Arg::new("in-rtcm3") + .long("in-rtcm3") + .default_value("false") + .action(clap::ArgAction::SetTrue) + .help( + "Toggle receiving RTCM3 protocol on port. + Not supported on uBlox protocol versions below 20", + ), + ) + .arg( + Arg::new("out-ublox") + .long("out-ublox") + .action(clap::ArgAction::SetTrue) + .help("Toggle sending UBX proprietary protocol on port"), + ) + .arg( + Arg::new("out-nmea") + .long("out-nmea") + .action(clap::ArgAction::SetTrue) + .help("Toggle sending NMEA protocol on port"), + ) + .arg( + Arg::new("out-rtcm3") + .long("out-rtcm3") + .action(clap::ArgAction::SetTrue) + .help( + "Toggle seding RTCM3 protocol on port. + Not supported on uBlox protocol versions below 20", + ), + ), + ) + .get_matches() +} diff --git a/examples/tui/src/device.rs b/examples/tui/src/device.rs new file mode 100644 index 0000000..fe0fc8c --- /dev/null +++ b/examples/tui/src/device.rs @@ -0,0 +1,452 @@ +use clap::ArgMatches; +use serialport::{ + DataBits as SerialDataBits, FlowControl as SerialFlowControl, Parity as SerialParity, + StopBits as SerialStopBits, +}; +use std::sync::mpsc::Sender; +use std::thread; +use std::time::Duration; +use tracing::{debug, error, info, trace, warn}; +use ublox::*; + +use crate::app::{ + EsfAlgImuAlignmentWidgetState, EsfAlgStatusWidgetState, EsfSensorWidget, EsfSensorsWidgetState, + MonVersionWidgetState, NavPvtWidgetState, UbxStatus, +}; + +pub struct Device { + port: Box, + parser: Parser>, +} + +impl Device { + pub fn new(port: Box) -> Device { + let parser = Parser::default(); + Device { port, parser } + } + + pub fn build(cli: &ArgMatches) -> Device { + let port = cli + .get_one::("port") + .expect("Expected required 'port' cli argumnet"); + + let baud = cli.get_one::("baud").cloned().unwrap_or(9600); + let stop_bits = match cli.get_one::("stop-bits").map(|s| s.as_str()) { + Some("2") => SerialStopBits::Two, + _ => SerialStopBits::One, + }; + let data_bits = match cli.get_one::("data-bits").map(|s| s.as_str()) { + Some("7") => SerialDataBits::Seven, + Some("8") => SerialDataBits::Eight, + _ => { + error!("Number of DataBits supported by uBlox is either 7 or 8"); + std::process::exit(1); + }, + }; + + let parity = match cli.get_one::("parity").map(|s| s.as_str()) { + Some("odd") => SerialParity::Even, + Some("even") => SerialParity::Odd, + _ => SerialParity::None, + }; + + let serialport_builder = serialport::new(port, baud) + .stop_bits(stop_bits) + .data_bits(data_bits) + .timeout(Duration::from_millis(10)) + .parity(parity) + .flow_control(SerialFlowControl::None); + + debug!("{:?}", &serialport_builder); + let port = serialport_builder.open().unwrap_or_else(|e| { + error!("Failed to open \"{}\". Error: {}", port, e); + ::std::process::exit(1); + }); + + let mut device = Device::new(port); + device.configure_uart_ports(cli); + device.configure_ubx_msgs(cli); + device + } + + fn configure_uart_ports(&mut self, cli: &ArgMatches) { + // Parse cli for configuring specific uBlox UART port + if let Some(("configure", sub_matches)) = cli.subcommand() { + let (port_id, port_name) = + match sub_matches.get_one::("port").map(|s| s.as_str()) { + Some(x) if x == "usb" => (Some(UartPortId::Usb), x), + Some(x) if x == "uart1" => (Some(UartPortId::Uart1), x), + Some(x) if x == "uart2" => (Some(UartPortId::Uart2), x), + _ => (None, ""), + }; + + let baud = sub_matches.get_one::("baud").cloned().unwrap_or(9600); + + let stop_bits = match sub_matches + .get_one::("stop-bits") + .map(|s| s.as_str()) + { + Some("2") => SerialStopBits::Two, + _ => SerialStopBits::One, + }; + + let data_bits = match sub_matches + .get_one::("data-bits") + .map(|s| s.as_str()) + { + Some("7") => SerialDataBits::Seven, + Some("8") => SerialDataBits::Eight, + _ => { + error!("Number of DataBits supported by uBlox is either 7 or 8"); + std::process::exit(1); + }, + }; + + let parity = match sub_matches.get_one::("parity").map(|s| s.as_str()) { + Some("odd") => SerialParity::Even, + Some("even") => SerialParity::Odd, + _ => SerialParity::None, + }; + let inproto = match ( + sub_matches.get_flag("in-ublox"), + sub_matches.get_flag("in-nmea"), + sub_matches.get_flag("in-rtcm"), + sub_matches.get_flag("in-rtcm3"), + ) { + (true, false, false, false) => InProtoMask::UBLOX, + (false, true, false, false) => InProtoMask::NMEA, + (false, false, true, false) => InProtoMask::RTCM, + (false, false, false, true) => InProtoMask::RTCM3, + (true, true, false, false) => { + InProtoMask::union(InProtoMask::UBLOX, InProtoMask::NMEA) + }, + (true, false, true, false) => { + InProtoMask::union(InProtoMask::UBLOX, InProtoMask::RTCM) + }, + (true, false, false, true) => { + InProtoMask::union(InProtoMask::UBLOX, InProtoMask::RTCM3) + }, + (false, true, true, false) => { + InProtoMask::union(InProtoMask::NMEA, InProtoMask::RTCM) + }, + (false, true, false, true) => { + InProtoMask::union(InProtoMask::NMEA, InProtoMask::RTCM3) + }, + (true, true, true, false) => InProtoMask::union( + InProtoMask::union(InProtoMask::UBLOX, InProtoMask::NMEA), + InProtoMask::RTCM, + ), + (true, true, false, true) => InProtoMask::union( + InProtoMask::union(InProtoMask::UBLOX, InProtoMask::NMEA), + InProtoMask::RTCM3, + ), + (_, _, true, true) => { + error!("Cannot use RTCM and RTCM3 simultaneously. Choose one or the other"); + std::process::exit(1) + }, + (false, false, false, false) => InProtoMask::UBLOX, + }; + + let outproto = match ( + sub_matches.get_flag("out-ublox"), + sub_matches.get_flag("out-nmea"), + sub_matches.get_flag("out-rtcm3"), + ) { + (true, false, false) => OutProtoMask::UBLOX, + (false, true, false) => OutProtoMask::NMEA, + (false, false, true) => OutProtoMask::RTCM3, + (true, true, false) => OutProtoMask::union(OutProtoMask::UBLOX, OutProtoMask::NMEA), + (true, false, true) => { + OutProtoMask::union(OutProtoMask::UBLOX, OutProtoMask::RTCM3) + }, + (false, true, true) => OutProtoMask::union(OutProtoMask::NMEA, OutProtoMask::RTCM3), + (true, true, true) => OutProtoMask::union( + OutProtoMask::union(OutProtoMask::UBLOX, OutProtoMask::NMEA), + OutProtoMask::RTCM3, + ), + (false, false, false) => OutProtoMask::UBLOX, + }; + + if let Some(port_id) = port_id { + info!("Configuring '{}' port ...", port_name.to_uppercase()); + self.write_all( + &CfgPrtUartBuilder { + portid: port_id, + reserved0: 0, + tx_ready: 0, + mode: UartMode::new( + ublox_databits(data_bits), + ublox_parity(parity), + ublox_stopbits(stop_bits), + ), + baud_rate: baud, + in_proto_mask: inproto, + out_proto_mask: outproto, + flags: 0, + reserved5: 0, + } + .into_packet_bytes(), + ) + .expect("Could not configure UBX-CFG-PRT-UART"); + self.wait_for_ack::() + .expect("Could not acknowledge UBX-CFG-PRT-UART msg"); + } + } + } + + fn configure_ubx_msgs(&mut self, _cli: &ArgMatches) { + // Enable the NavPvt packet + // By setting 1 in the array below, we enable the NavPvt message for Uart1, Uart2 and USB + // The other positions are for I2C, SPI, etc. Consult your device manual. + info!("Enable UBX-NAV-PVT message on all serial ports: USB, UART1 and UART2 ..."); + self.write_all( + &CfgMsgAllPortsBuilder::set_rate_for::([0, 1, 1, 1, 0, 0]).into_packet_bytes(), + ) + .expect("Could not configure ports for UBX-NAV-PVT"); + + self.wait_for_ack::() + .expect("Could not acknowledge UBX-CFG-PRT-UART msg"); + + // Send a packet request for the MonVer packet + self.write_all(&UbxPacketRequest::request_for::().into_packet_bytes()) + .expect("Unable to write request/poll for UBX-MON-VER message"); + + self.write_all(&UbxPacketRequest::request_for::().into_packet_bytes()) + .expect("Unable to write request/poll for UBX-ESF-ALG message"); + + self.write_all(&UbxPacketRequest::request_for::().into_packet_bytes()) + .expect("Unable to write request/poll for UBX-ESF-STATUS message"); + } + + pub fn run(mut self, sender: Sender) { + info!("Opened uBlox device, waiting for messages..."); + thread::spawn(move || loop { + let res = self.update(|packet| match packet { + PacketRef::MonVer(pkg) => { + trace!("{:?}", pkg); + info!( + "SW version: {} HW version: {}; Extensions: {:?}", + pkg.software_version(), + pkg.hardware_version(), + pkg.extension().collect::>() + ); + let mut state = MonVersionWidgetState::default(); + + state + .software_version + .copy_from_slice(pkg.software_version_raw()); + state + .hardware_version + .copy_from_slice(pkg.hardware_version_raw()); + + for s in pkg.extension() { + state.extensions.push_str(s); + } + + sender.send(UbxStatus::MonVer(Box::new(state))).unwrap(); + }, + PacketRef::NavPvt(pkg) => { + let mut state = NavPvtWidgetState { + time_tag: (pkg.itow() / 1000) as f64, + ..Default::default() + }; + + state.flags2 = pkg.flags2(); + + if pkg.flags2().contains(NavPvtFlags2::CONFIRMED_AVAI) { + state.day = pkg.day(); + state.month = pkg.month(); + state.year = pkg.year(); + state.hour = pkg.hour(); + state.min = pkg.min(); + state.sec = pkg.sec(); + state.nanosecond = pkg.nanosecond(); + + state.utc_time_accuracy = pkg.time_accuracy(); + } + + state.position_fix_type = pkg.fix_type(); + state.fix_flags = pkg.flags(); + + state.lat = pkg.lat_degrees(); + state.lon = pkg.lon_degrees(); + state.height = pkg.height_meters(); + state.msl = pkg.height_msl(); + + state.vel_ned = (pkg.vel_north(), pkg.vel_east(), pkg.vel_down()); + + state.speed_over_ground = pkg.ground_speed(); + state.heading_motion = pkg.heading_motion(); + state.heading_vehicle = pkg.heading_of_vehicle(); + + state.magnetic_declination = pkg.magnetic_declination(); + + state.pdop = pkg.pdop(); + + state.satellites_used = pkg.num_satellites(); + + state.invalid_llh = pkg.flags3().invalid_llh(); + state.position_accuracy = (pkg.horiz_accuracy(), pkg.vert_accuracy()); + state.velocity_accuracy = pkg.speed_accuracy_estimate(); + state.heading_accuracy = pkg.heading_accuracy_estimate(); + state.magnetic_declination_accuracy = pkg.magnetic_declination_accuracy(); + + sender.send(UbxStatus::Pvt(Box::new(state))).unwrap(); + debug!("{:?}", pkg); + }, + PacketRef::EsfAlg(pkg) => { + let mut state = EsfAlgImuAlignmentWidgetState { + time_tag: (pkg.itow() / 1000) as f64, + ..Default::default() + }; + state.roll = pkg.roll(); + state.pitch = pkg.pitch(); + state.yaw = pkg.yaw(); + + state.auto_alignment = pkg.flags().auto_imu_mount_alg_on(); + state.alignment_status = pkg.flags().status(); + + if pkg.error().contains(EsfAlgError::ANGLE_ERROR) { + state.angle_singularity = true; + } + + sender.send(UbxStatus::EsfAlgImu(state)).unwrap(); + // debug!("{:?}", pkg); + }, + + PacketRef::EsfStatus(pkg) => { + let mut alg_state = EsfAlgStatusWidgetState { + time_tag: (pkg.itow() / 1000) as f64, + ..Default::default() + }; + alg_state.fusion_mode = pkg.fusion_mode(); + + alg_state.imu_status = pkg.init_status2().imu_init_status(); + alg_state.ins_status = pkg.init_status1().ins_initialization_status(); + alg_state.ins_status = pkg.init_status1().ins_initialization_status(); + alg_state.wheel_tick_sensor_status = + pkg.init_status1().wheel_tick_init_status(); + + let mut sensors = EsfSensorsWidgetState::default(); + let mut sensor_state = EsfSensorWidget::default(); + for s in pkg.data() { + if s.sensor_used() { + sensor_state.sensor_type = s.sensor_type(); + sensor_state.freq = s.freq(); + sensor_state.faults = s.faults(); + sensor_state.calib_status = s.calibration_status(); + sensor_state.time_status = s.time_status(); + sensors.sensors.push(sensor_state.clone()); + } + } + + sender.send(UbxStatus::EsfAlgStatus(alg_state)).unwrap(); + sender.send(UbxStatus::EsfAlgSensors(sensors)).unwrap(); + // debug!("{:?}", pkg); + }, + + _ => { + trace!("{:?}", packet); + }, + }); + if let Err(e) = res { + error!("Stopping UBX messages parsing thread. Failed to parse incoming UBX packet: {e}"); + } + }); + } + pub fn write_all(&mut self, data: &[u8]) -> std::io::Result<()> { + self.port.write_all(data) + } + + pub fn update(&mut self, mut cb: T) -> std::io::Result<()> { + loop { + const MAX_PAYLOAD_LEN: usize = 1240; + let mut local_buf = [0; MAX_PAYLOAD_LEN]; + let nbytes = self.read_port(&mut local_buf)?; + if nbytes == 0 { + break; + } + + // Parser.consume adds the buffer to its internal buffer, and + // returns an iterator-like object we can use to process the packets + let mut it = self.parser.consume(&local_buf[..nbytes]); + loop { + match it.next() { + Some(Ok(packet)) => { + cb(packet); + }, + Some(Err(e)) => { + trace!("Received malformed packet, ignoring it. Error: {e}"); + }, + None => { + // We've eaten all the packets we have + break; + }, + } + } + } + Ok(()) + } + + pub fn wait_for_ack(&mut self) -> std::io::Result<()> { + let mut found_packet = false; + let start = std::time::SystemTime::now(); + let timeout = Duration::from_secs(3); + while !found_packet { + self.update(|packet| { + if let PacketRef::AckAck(ack) = packet { + if ack.class() == T::CLASS && ack.msg_id() == T::ID { + found_packet = true; + } + } + })?; + + if start.elapsed().unwrap().as_millis() > timeout.as_millis() { + error!("Did not receive ACK message for request"); + break; + } + } + Ok(()) + } + + /// Reads the serial port, converting timeouts into "no data received" + fn read_port(&mut self, output: &mut [u8]) -> std::io::Result { + match self.port.read(output) { + Ok(b) => Ok(b), + Err(e) => { + if e.kind() == std::io::ErrorKind::TimedOut { + Ok(0) + } else { + Err(e) + } + }, + } + } +} + +fn ublox_stopbits(s: SerialStopBits) -> StopBits { + // Seriaport crate doesn't support the other StopBits option of uBlox + match s { + SerialStopBits::One => StopBits::One, + SerialStopBits::Two => StopBits::Two, + } +} + +fn ublox_databits(d: SerialDataBits) -> DataBits { + match d { + SerialDataBits::Seven => DataBits::Seven, + SerialDataBits::Eight => DataBits::Eight, + _ => { + warn!("uBlox only supports Seven or Eight data bits. Setting to DataBits to 8"); + DataBits::Eight + }, + } +} + +fn ublox_parity(v: SerialParity) -> Parity { + match v { + SerialParity::Even => Parity::Even, + SerialParity::Odd => Parity::Odd, + SerialParity::None => Parity::None, + } +} diff --git a/examples/tui/src/logging.rs b/examples/tui/src/logging.rs new file mode 100644 index 0000000..4ca8253 --- /dev/null +++ b/examples/tui/src/logging.rs @@ -0,0 +1,116 @@ +use std::path::PathBuf; + +use anyhow::{Ok, Result}; +use clap::ArgMatches; +use directories::ProjectDirs; +use lazy_static::lazy_static; +use log::LevelFilter; +use tracing::info; +use tracing_error::ErrorLayer; +use tracing_subscriber::{self, layer::SubscriberExt, util::SubscriberInitExt, Layer}; + +lazy_static! { + pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string(); + pub static ref DATA_FOLDER: Option = + std::env::var(format!("{}_DATA", PROJECT_NAME.clone())) + .ok() + .map(PathBuf::from); + pub static ref LOG_ENV: String = "TUI_LOGLEVEL".to_string(); + pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); +} + +fn project_directory() -> Option { + Some(ProjectDirs::from("com", "ublox", env!("CARGO_PKG_NAME"))) +} + +pub fn get_data_dir() -> PathBuf { + let directory = if let Some(s) = DATA_FOLDER.clone() { + s + } else if let Some(proj_dirs) = project_directory() { + proj_dirs.data_local_dir().to_path_buf() + } else { + PathBuf::from(".").join(".data") + }; + directory +} + +pub fn initialize(cli: &ArgMatches) -> Result { + std::env::set_var( + "RUST_LOG", + std::env::var("RUST_LOG") + .or_else(|_| std::env::var(LOG_ENV.clone())) + .unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))), + ); + + let log_file = if cli.get_flag("log-file") { + let directory = get_data_dir(); + info!("Log to file : {:?}", directory); + std::fs::create_dir_all(directory.clone())?; + let log_path = directory.join(LOG_FILE.clone()); + let log_file = std::fs::File::create(log_path)?; + + let file_subscriber = tracing_subscriber::fmt::layer() + .with_file(true) + .with_line_number(true) + .with_writer(log_file) + .with_target(false) + .with_ansi(false) + .with_filter(tracing_subscriber::filter::EnvFilter::from_default_env()); + tracing_subscriber::registry() + .with(file_subscriber) + .with(ErrorLayer::default()) + .with(tui_logger::tracing_subscriber_layer()) + .init(); + info!("Full log available in: {}", directory.to_string_lossy()); + directory + } else { + tracing_subscriber::registry() + .with(ErrorLayer::default()) + .with(tui_logger::tracing_subscriber_layer()) + .init(); + PathBuf::new() + }; + + let level = std::env::var("RUST_LOG") + .unwrap_or("info".to_string()) + .to_ascii_lowercase(); + let level = match level.as_str() { + "off" => LevelFilter::Off, + "warn" => LevelFilter::Warn, + "error" => LevelFilter::Error, + "debug" => LevelFilter::Debug, + "trace" => LevelFilter::Trace, + "info" => LevelFilter::Info, + _ => LevelFilter::Info, + }; + + tui_logger::init_logger(level)?; + tui_logger::set_default_level(level); + Ok(log_file) +} + +/// Similar to the `std::dbg!` macro, but generates `tracing` events rather +/// than printing to stdout. +/// +/// By default, the verbosity level for the generated events is `DEBUG`, but +/// this can be customized. +#[macro_export] +macro_rules! trace_dbg { + (target: $target:expr, level: $level:expr, $ex:expr) => {{ + match $ex { + value => { + tracing::event!(target: $target, $level, ?value, stringify!($ex)); + value + } + } + }}; + (level: $level:expr, $ex:expr) => { + trace_dbg!(target: module_path!(), level: $level, $ex) + }; + (target: $target:expr, $ex:expr) => { + trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex) + }; + ($ex:expr) => { + trace_dbg!(level: tracing::Level::DEBUG, $ex) + }; +} diff --git a/examples/tui/src/main.rs b/examples/tui/src/main.rs new file mode 100644 index 0000000..f7eb992 --- /dev/null +++ b/examples/tui/src/main.rs @@ -0,0 +1,44 @@ +use std::{error::Error, sync::mpsc::channel}; + +use clap::ArgMatches; +use device::Device; + +mod app; +mod cli; +mod device; +mod logging; +mod tui; +mod ui; + +fn main() -> Result<(), Box> { + let cli = cli::parse_args(); + + if cli.get_flag("debug-mode") { + debug_mode(&cli); + } else { + crate::tui::run(&cli)?; + } + Ok(()) +} + +fn debug_mode(cli: &ArgMatches) { + use log::error; + env_logger::Builder::new() + .filter_level(log::LevelFilter::Info) + .parse_env("TUI_LOGLEVEL") + .init(); + + let (ubx_msg_tx, ubx_msg_rs) = channel(); + + let device = Device::build(cli); + device.run(ubx_msg_tx); + + loop { + match ubx_msg_rs.recv() { + Ok(_) => { + // We don't do anything with the received messages as data as this is intended for the TUI Widgets; + }, + Err(e) => error!("Error: {e}"), + } + } +} diff --git a/examples/tui/src/tui.rs b/examples/tui/src/tui.rs new file mode 100644 index 0000000..dfd4ccb --- /dev/null +++ b/examples/tui/src/tui.rs @@ -0,0 +1,136 @@ +use std::{ + error::Error, + io, + sync::mpsc::{channel, Receiver}, + time::{Duration, Instant}, +}; + +use log::error; + +use clap::ArgMatches; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + }, + Terminal, +}; + +use anyhow::Result; +use tracing::{debug, info, instrument}; + +use crate::{ + app::{App, UbxStatus}, + device::Device, + logging, ui, +}; + +pub fn run(cli: &ArgMatches) -> Result<(), Box> { + let log_file = logging::initialize(cli)?; + let tick_rate: u64 = *cli.get_one("tui-rate").ok_or("Missing tui-rate cli arg")?; + let tick_rate = Duration::from_millis(tick_rate); + + // trace_dbg!(level: tracing::Level::INFO,"test"); + + // Setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let (ubx_msg_tx, ubx_msg_rs) = channel(); + + let device = Device::build(cli); + device.run(ubx_msg_tx); + + let app = App::new("uBlox TUI", log_file); + let app_result = run_app(&mut terminal, app, tick_rate, ubx_msg_rs); + + // Restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = app_result { + error!("{err:?}"); + } + + Ok(()) +} + +fn run_app( + terminal: &mut Terminal, + mut app: App, + tick_rate: Duration, + receiver: Receiver, +) -> Result<()> { + let mut last_tick = Instant::now(); + loop { + update_states(&mut app, &receiver); + terminal.draw(|frame| ui::draw(frame, &mut app))?; + + let timeout = tick_rate.saturating_sub(last_tick.elapsed()); + + if event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Left | KeyCode::Char('h') => app.on_left(), + KeyCode::Right | KeyCode::Char('l') => app.on_right(), + KeyCode::Char(c) => app.on_key(c), + _ => {}, + } + } + } + } + if last_tick.elapsed() >= tick_rate { + last_tick = Instant::now(); + } + if app.should_quit { + info!("Q/q pressed. Exiting application."); + println!("See the log file logs"); + return Ok(()); + } + } +} + +fn update_states(app: &mut App, receiver: &Receiver) { + match receiver.try_recv() { + Ok(UbxStatus::Pvt(v)) => { + app.pvt_state = *v; + }, + Ok(UbxStatus::MonVer(v)) => { + app.mon_ver_state = *v; + }, + Ok(UbxStatus::EsfAlgImu(v)) => { + app.esf_alg_imu_alignment_state = v; + }, + Ok(UbxStatus::EsfAlgStatus(v)) => { + app.esf_alg_state = v; + }, + Ok(UbxStatus::EsfAlgSensors(v)) => { + app.esf_sensors_state = v; + }, + _ => {}, // Err(e) => println!("Not value from channel"), + } +} + +/// Handle events and insert them into the events vector keeping only the last 10 events +#[instrument(skip(events))] +fn handle_events(events: &mut Vec) -> Result<()> { + // Render the UI at least once every 100ms + if event::poll(Duration::from_millis(100))? { + let event = event::read()?; + debug!(?event); + events.insert(0, event); + } + events.truncate(10); + Ok(()) +} diff --git a/examples/tui/src/ui.rs b/examples/tui/src/ui.rs new file mode 100644 index 0000000..0ab2f3d --- /dev/null +++ b/examples/tui/src/ui.rs @@ -0,0 +1,597 @@ +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Layout, Rect}, + style::{Color, Modifier, Style, Stylize}, + symbols, + text::{Line, Span}, + widgets::{ + canvas::{Canvas, Circle, Map, MapResolution}, + Block, Cell, Paragraph, Row, Table, Tabs, Widget, Wrap, + }, + Frame, +}; + +use tui_logger::{TuiLoggerLevelOutput, TuiLoggerWidget}; +use ublox::{ + EsfAlgStatus, EsfSensorFaults, EsfSensorStatusCalibration, EsfSensorStatusTime, EsfSensorType, + EsfStatusFusionMode, EsfStatusImuInit, EsfStatusInsInit, EsfStatusMountAngle, + EsfStatusWheelTickInit, GpsFix, NavPvtFlags, NavPvtFlags2, +}; + +use crate::app::App; + +#[derive(Debug, Default)] +pub struct LogWidget; + +impl Widget for &mut LogWidget { + fn render(self, area: Rect, buf: &mut Buffer) { + TuiLoggerWidget::default() + .block(Block::bordered().title("Log")) + .style_error(Style::default().fg(Color::Red)) + .style_warn(Style::default().fg(Color::Yellow)) + .style_info(Style::default().fg(Color::Green)) + .style_debug(Style::default().fg(Color::White)) + .style_trace(Style::default().fg(Color::Magenta)) + .output_separator(':') + .output_timestamp(Some("%F %H:%M:%S%.3f".to_string())) + .output_level(Some(TuiLoggerLevelOutput::Long)) + .output_target(false) + .output_file(false) + .output_line(false) + .style(Style::default().fg(ratatui::style::Color::White)) + .render(area, buf); + + // TuiLoggerSmartWidget::default() + // .title_log("Log") + // .style_error(Style::default().fg(Color::Red)) + // .style_debug(Style::default().fg(Color::Green)) + // .style_warn(Style::default().fg(Color::Yellow)) + // .style_trace(Style::default().fg(Color::Magenta)) + // .style_info(Style::default().fg(Color::Cyan)) + // .output_separator(':') + // .output_timestamp(Some("%H:%M:%S".to_string())) + // .output_level(Some(TuiLoggerLevelOutput::Abbreviated)) + // .output_target(true) + // .output_file(true) + // .output_line(true) + // .state(self.selected_state()) + // .render(area, buf); + } +} + +pub fn draw(frame: &mut Frame, app: &mut App) { + let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(frame.area()); + let tabs = app + .tabs + .titles + .iter() + .map(|t| Line::from(Span::styled(*t, Style::default().fg(Color::Green)))) + .collect::() + .block(Block::bordered().title(app.title)) + .highlight_style(Style::default().fg(Color::Yellow)) + .select(app.tabs.index); + frame.render_widget(tabs, chunks[0]); + match app.tabs.index { + 0 => draw_state_tab(frame, app, chunks[1]), + 1 => draw_version_info(frame, app, chunks[1]), + 2 => draw_map(frame, app, chunks[1]), + _ => {}, + }; +} + +fn draw_state_tab(frame: &mut Frame, app: &mut App, area: Rect) { + let chunks = Layout::vertical([Constraint::Length(24), Constraint::Min(7)]).split(area); + render_pvt_and_esf_statuses(frame, chunks[0], app); + frame.render_widget(&mut app.log_widget, chunks[1]); +} + +fn draw_version_info(frame: &mut Frame, app: &mut App, area: Rect) { + let chunks = Layout::vertical([Constraint::Length(24), Constraint::Min(7)]).split(area); + render_monver(frame, chunks[0], app); + frame.render_widget(&mut app.log_widget, chunks[1]); +} + +fn render_pvt_and_esf_statuses(frame: &mut Frame, area: Rect, app: &mut App) { + let chunks = + Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]).split(area); + + render_pvt_state(frame, chunks[0], app); + render_esf_status(frame, chunks[1], app); +} + +fn render_pvt_state(frame: &mut Frame, area: Rect, app: &mut App) { + let time_tag = format!("{:.3}", app.pvt_state.time_tag); + let position = format!( + "{:.4},{:.4}, {:.4}, {:.4}", + app.pvt_state.lat, app.pvt_state.lon, app.pvt_state.height, app.pvt_state.msl + ); + let time_accuracy = format!("{}", app.pvt_state.utc_time_accuracy); + let position_accuracy = format!( + "{:.2}, {:.2}", + app.pvt_state.position_accuracy.0, app.pvt_state.position_accuracy.1 + ); + + let velocity_ned = format!( + "{:.3}, {:.3}, {:.3}", + app.pvt_state.vel_ned.0, app.pvt_state.vel_ned.1, app.pvt_state.vel_ned.2 + ); + + let velocity_heading_acc = format!( + "{:.3}, {:.3}", + app.pvt_state.velocity_accuracy, app.pvt_state.heading_accuracy + ); + + let heading_info = format!( + "{:.3}, {:.3}", + app.pvt_state.heading_motion, app.pvt_state.heading_vehicle + ); + + let magnetic_declination = format!( + "{:.2}, {:.2}", + app.pvt_state.magnetic_declination, app.pvt_state.magnetic_declination_accuracy + ); + + let gps_fix = match app.pvt_state.position_fix_type { + GpsFix::DeadReckoningOnly => "DR", + GpsFix::Fix2D => "2D Fix", + GpsFix::Fix3D => "3D Fix", + GpsFix::GPSPlusDeadReckoning => "3D + DR", + GpsFix::TimeOnlyFix => "Time Only", + _ => "No Fix", + }; + + let mut fix_flags = String::default(); + if app.pvt_state.fix_flags.contains(NavPvtFlags::GPS_FIX_OK) { + fix_flags = "FixOK".to_string(); + } + if app.pvt_state.fix_flags.contains(NavPvtFlags::DIFF_SOLN) { + fix_flags.push_str(" + DGNSS"); + } + + let utc_date_time = format!( + "{:02}-{:02}-{} {:02}:{:02}:{:02} {:09}", + app.pvt_state.day, + app.pvt_state.month, + app.pvt_state.year, + app.pvt_state.hour, + app.pvt_state.min, + app.pvt_state.sec, + app.pvt_state.nanosecond, + ); + + let mut time_date_confirmation = if app.pvt_state.flags2.contains(NavPvtFlags2::CONFIRMED_DATE) + { + "Date: CONFIRMED".to_string() + } else { + "Date: ?".to_string() + }; + + if app.pvt_state.flags2.contains(NavPvtFlags2::CONFIRMED_TIME) { + time_date_confirmation.push_str(", Time: CONFIRMED"); + } else { + time_date_confirmation.push_str(", Time: ?"); + } + let rows = [ + Row::new(["GPS Time Tag", &time_tag, "[s]"]), + Row::new(["UTC Date Time", &utc_date_time, ""]), + Row::new(["UTC Date Time Confirmation", &time_date_confirmation, ""]), + Row::new(["UTC Time Accuracy", &time_accuracy, "[ns]"]), + Row::new(["Position Fix Type", gps_fix, ""]), + Row::new(["Fix Flags", &fix_flags, ""]), + Row::new(["PSM State", "n/a", ""]), + Row::new(["Lat,Lon,Height,MSL", &position, "[deg,deg,m,m]"]), + Row::new([ + "Invalid Position", + if app.pvt_state.invalid_llh { + "Yes" + } else { + "No" + }, + "", + ]), + Row::new(["Position Accuracy Horiz, Vert", &position_accuracy, "[m,m]"]), + Row::new(["Velocity NED", &velocity_ned, "[m/s,m/s,m/s]"]), + Row::new([ + "Velocity, Heading Accuracy", + &velocity_heading_acc, + "[m/s, deg]", + ]), + Row::new([ + Cell::from("Speed over Ground"), + Cell::from(format!("{:.4}", app.pvt_state.speed_over_ground)), + Cell::from("[m/s]"), + ]), + Row::new([ + "Heading Motion, Heading Vehicle", + &heading_info, + "[deg,deg]", + ]), + Row::new([ + "Magnetic Declination, Declination Accuracy", + &magnetic_declination, + "[deg,deg]", + ]), + Row::new([ + Cell::from("PDOP"), + Cell::from(format!("{:.3}", app.pvt_state.pdop)), + Cell::from(""), + ]), + Row::new([ + Cell::from("#SVs Used"), + Cell::from(app.pvt_state.satellites_used.to_string()), + Cell::from(""), + ]), + Row::new(["Carrier Range Status", "Not Used", ""]), + Row::new(["Age of recent differential correction", "???", "[sec]"]), + Row::new(["NMA Fix Status", "???", ""]), + Row::new(["Time Authentication Status", "???", ""]), + ]; + + let widths = [ + Constraint::Percentage(50), + Constraint::Percentage(35), + Constraint::Percentage(15), + ]; + + let table = Table::new(rows, widths) + .block(Block::bordered().title(Span::styled( + "NAV-PVT", + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + ))) + .row_highlight_style(Style::default().fg(Color::Yellow)) + .header( + Row::new(["Param", "Value", "Units"]) + .style(Style::new().bold()) + .bottom_margin(1) + .fg(Color::Yellow), + ) + .column_spacing(1) + .style(Color::White); + + frame.render_widget(table, area); +} + +fn render_esf_status(frame: &mut Frame, area: Rect, app: &mut App) { + let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]); + let [top, bottom] = vertical.areas(area); + let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]); + let [top_left, top_right] = horizontal.areas(top); + + render_esf_alg_status(frame, top_left, app); + render_esf_imu_alignment_status(frame, top_right, app); + render_esf_sensor_status(frame, bottom, app); +} + +fn render_esf_alg_status(frame: &mut Frame, area: Rect, app: &mut App) { + let time_tag = format!("{:.3}", app.esf_alg_state.time_tag); + let fusion_mode = match app.esf_alg_state.fusion_mode { + EsfStatusFusionMode::Disabled => "DISABLED", + EsfStatusFusionMode::Initializing => "INITIALIZING", + EsfStatusFusionMode::Fusion => "FUSION", + EsfStatusFusionMode::Suspended => "SUSPENDED", + _ => "UNKNOWN", + }; + + let ins_status = match app.esf_alg_state.ins_status { + EsfStatusInsInit::Off => "OFF", + EsfStatusInsInit::Initialized => "INITIALIZED", + EsfStatusInsInit::Initializing => "INITIALIZING", + }; + + let imu_status = match app.esf_alg_state.imu_status { + EsfStatusImuInit::Off => "OFF", + EsfStatusImuInit::Initialized => "INITIALIZED", + EsfStatusImuInit::Initializing => "INITIALIZING", + }; + + let wt_status = match app.esf_alg_state.wheel_tick_sensor_status { + EsfStatusWheelTickInit::Off => "OFF", + EsfStatusWheelTickInit::Initialized => "INITIALIZED", + EsfStatusWheelTickInit::Initializing => "INITIALIZING", + }; + + let mount_angle_status = match app.esf_alg_state.imu_mount_alignment_status { + EsfStatusMountAngle::Off => "OFF", + EsfStatusMountAngle::Initialized => "INITIALIZED", + EsfStatusMountAngle::Initializing => "INITIALIZING", + }; + + let rows = [ + Row::new(["GPS Time Tag (s)", &time_tag]), + Row::new(["Fusion Filter Mode", fusion_mode]), + Row::new(["IMU Status", imu_status]), + Row::new(["Wheel-tick Sensor Status", wt_status]), + Row::new(["INS Status", ins_status]), + Row::new(["IMU-mount Alignment Status", mount_angle_status]), + ]; + + let widths = [Constraint::Percentage(65), Constraint::Percentage(35)]; + + let table = Table::new(rows, widths) + .block(Block::bordered().title(Span::styled( + "ESF-ALG-STATUS", + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + ))) + .row_highlight_style(Style::default().fg(Color::Yellow)) + .header( + Row::new(["Name", "Status"]) + .style(Style::new().bold()) + .bottom_margin(1) + .fg(Color::Yellow), + ) + .column_spacing(1) + .style(Color::White); + + frame.render_widget(table, area); +} + +fn render_esf_imu_alignment_status(frame: &mut Frame, area: Rect, app: &mut App) { + let time_tag = format!("{:.3}", app.esf_alg_imu_alignment_state.time_tag); + let aligment_status = match app.esf_alg_imu_alignment_state.alignment_status { + EsfAlgStatus::CoarseAlignment => "COARSE", + EsfAlgStatus::FineAlignment => "FINE", + EsfAlgStatus::UserDefinedAngles => "---", + EsfAlgStatus::RollPitchAlignmentOngoing => "INITIALIZING", // "ROLL-PITCH-ONGOING", + EsfAlgStatus::RollPitchYawAlignmentOngoing => "INITIALIZING", //"ROLL-PITCH-YAW-ONGOING", + }; + + let rows = [ + Row::new(["GPS Time Tag (s)", &time_tag]), + Row::new([ + "Auto Alignment", + if app.esf_alg_imu_alignment_state.auto_alignment { + "ON" + } else { + "OFF" + }, + ]), + Row::new(["Alignment Status", aligment_status]), + Row::new([ + "Angle Singularity", + if app.esf_alg_imu_alignment_state.angle_singularity { + "YES" + } else { + "NO" + }, + ]), + Row::new([ + Cell::from("Mounting-Roll (deg)"), + Cell::from(format!("{:.4}", app.esf_alg_imu_alignment_state.roll)), + ]), + Row::new([ + Cell::from("Mounting-Pith (deg)"), + Cell::from(format!("{:.4}", app.esf_alg_imu_alignment_state.pitch)), + ]), + Row::new([ + Cell::from("Mounting-Yaw (deg)"), + Cell::from(format!("{:.4}", app.esf_alg_imu_alignment_state.yaw)), + ]), + ]; + + // Cell::from(sensor_type).style(Style::new().white()), + + let widths = [Constraint::Percentage(60), Constraint::Percentage(40)]; + + let table = Table::new(rows, widths) + .block(Block::bordered().title(Span::styled( + "ESF-ALG-IMU-Alignment", + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + ))) + .row_highlight_style(Style::default().fg(Color::Yellow)) + .header( + Row::new(["Name", "Status"]) + .style(Style::new().bold()) + .bottom_margin(1) + .fg(Color::Yellow), + ) + .column_spacing(1) + .style(Color::White); + + frame.render_widget(table, area); +} + +fn render_esf_sensor_status(frame: &mut Frame, area: Rect, app: &mut App) { + let mut rows = vec![]; + + for s in &app.esf_sensors_state.sensors { + let sensor_type = match s.sensor_type { + EsfSensorType::AccX => "Acc X", + EsfSensorType::AccY => "Acc Y", + EsfSensorType::AccZ => "Acc Z", + EsfSensorType::GyroX => "Gyro X", + EsfSensorType::GyroY => "Gyro Y", + EsfSensorType::GyroZ => "Gyro Z", + EsfSensorType::FrontLeftWheelTicks => "FL WheelTick", + EsfSensorType::FrontRightWheelTicks => "FR WheelTick", + EsfSensorType::RearLeftWheelTicks => "RL WheelTick", + EsfSensorType::RearRightWheelTicks => "RR WheelTick", + EsfSensorType::GyroTemp => "Gyro Temp", + EsfSensorType::Speed => "Speed", + EsfSensorType::SpeedTick => "Speed Tick", + EsfSensorType::Unknown | EsfSensorType::None => "UNKNOWN", + }; + + let calibration_status = match s.calib_status { + EsfSensorStatusCalibration::Calibrated => { + Cell::from("CALIBRATED").style(Style::new().green()) + }, + + EsfSensorStatusCalibration::NotCalibrated => { + Cell::from("NOT CALIBRATED").style(Style::new().red()) + }, + EsfSensorStatusCalibration::Calibrating => { + Cell::from("CALIBRATING").style(Style::new().yellow()) + }, + }; + + let time_status = match s.time_status { + EsfSensorStatusTime::NoData => "NoData", + EsfSensorStatusTime::OnEventInput => "OnEventInput", + EsfSensorStatusTime::TimeTagFromData => "DataTimeTag", + EsfSensorStatusTime::OnReceptionFirstByte => "OnFirstByte", + }; + + let fault = if s.faults.contains(EsfSensorFaults::BAD_MEASUREMENT) { + Cell::from("BAD MEASUREMENT").style(Style::new().yellow()) + } else if s.faults.contains(EsfSensorFaults::BAD_TIME_TAG) { + Cell::from("BAD TIME TAG").style(Style::new().yellow()) + } else if s.faults.contains(EsfSensorFaults::MISSING_MEASUREMENT) { + Cell::from("MISSING MEASUREMENT").style(Style::new().yellow()) + } else if s.faults.contains(EsfSensorFaults::NOISY_MEASUREMENT) { + Cell::from("NOISY MEASUREMENT").style(Style::new().yellow()) + } else { + Cell::from("").style(Style::new().white()) + }; + + let row = Row::new(vec![ + Cell::from(sensor_type).style(Style::new().white()), + calibration_status, + Cell::from(time_status).style(Style::new().white()), + Cell::from(s.freq.to_string()).style(Style::new().white()), + fault, + ]); + rows.push(row); + } + + let widths = [ + Constraint::Percentage(10), + Constraint::Percentage(30), + Constraint::Percentage(15), + Constraint::Percentage(10), + Constraint::Percentage(35), + ]; + + let table = Table::new(rows, widths) + .block(Block::bordered().title(Span::styled( + "ESF-SENSOR-STATUS", + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + ))) + .row_highlight_style(Style::default().fg(Color::Yellow)) + .header( + Row::new(["Sensor", "Status", "Time", "Freq", "Faults"]) + .style(Style::new().bold()) + .bottom_margin(1) + .fg(Color::Yellow), + ) + .column_spacing(1) + .style(Color::White); + + frame.render_widget(table, area); +} + +fn render_monver(frame: &mut Frame, area: Rect, app: &mut App) { + let extensions_src = app.mon_ver_state.extensions.clone(); + + let mut extensions_lines = Vec::new(); + let mut extensions = extensions_src.to_string(); + let mut extensions = if let Some(p) = extensions.find("FWVER") { + let suffix = extensions.split_off(p); + extensions_lines.push(Line::from(extensions)); + suffix + } else { + String::default() + }; + + let mut extensions = if let Some(p) = extensions.find("PROTVER") { + let suffix = extensions.split_off(p); + extensions_lines.push(Line::from(extensions)); + suffix + } else { + String::default() + }; + + let mut extensions = if let Some(p) = extensions.find("MOD") { + let suffix = extensions.split_off(p); + extensions_lines.push(Line::from(extensions)); + suffix + } else { + String::default() + }; + + let mut extensions = if let Some(p) = extensions.find("FIS") { + let suffix = extensions.split_off(p); + extensions_lines.push(Line::from(extensions)); + suffix + } else { + String::default() + }; + + let extensions = if let Some(p) = extensions.find(")") { + let suffix = extensions.split_off(p + 1); + extensions_lines.push(Line::from(extensions)); + suffix + } else { + String::default() + }; + + // Remaining content of extensions string + extensions_lines.push(Line::from(extensions)); + + let software_version = std::str::from_utf8(&app.mon_ver_state.software_version).unwrap(); + let hardware_version = std::str::from_utf8(&app.mon_ver_state.hardware_version).unwrap(); + + let mut text = vec![ + Line::from(Span::styled( + "Software Version", + Style::default().fg(Color::Red), + )), + Line::from(vec![Span::from(" "), Span::from(software_version)]), + Line::from(""), + Line::from(Span::styled( + "Hardware Version", + Style::default().fg(Color::Red), + )), + Line::from(vec![Span::raw(""), Span::from(hardware_version)]), + Line::from(""), + Line::from(Span::styled( + "Extensions", + Style::default().fg(Color::Yellow), + )), + ]; + text.append(&mut extensions_lines); + + let mut raw_extensions = vec![ + Line::from(""), + Line::from("Extensions as raw string:"), + Line::from(extensions_src), + ]; + + text.append(&mut raw_extensions); + + let block = Block::bordered().title(Span::styled( + "MON-VERSION", + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + )); + let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true }); + frame.render_widget(paragraph, area); +} + +fn draw_map(frame: &mut Frame, app: &mut App, area: Rect) { + // let pos = app.pvt_state.lat + let map = Canvas::default() + .block(Block::bordered().title("World")) + .paint(|ctx| { + ctx.draw(&Map { + color: Color::White, + resolution: MapResolution::High, + }); + ctx.layer(); + ctx.draw(&Circle { + x: app.pvt_state.lon, + y: app.pvt_state.lat, + radius: 10.0, + color: Color::Green, + }); + ctx.print( + app.pvt_state.lon, + app.pvt_state.lat, + Span::styled("X", Style::default().fg(Color::Green)), + ); + }) + .marker(symbols::Marker::Braille) + .x_bounds([-180.0, 180.0]) + .y_bounds([-90.0, 90.0]); + frame.render_widget(map, area); +}