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 0000000..7e9e053 Binary files /dev/null and b/examples/tui/images/ublox-tui.png differ diff --git a/examples/tui/src/app.rs b/examples/tui/src/app.rs new file mode 100644 index 0000000..d241973 --- /dev/null +++ b/examples/tui/src/app.rs @@ -0,0 +1,244 @@ +use core::f64; +use std::{path::PathBuf, vec}; + +use ublox::{ + EsfAlgStatus, EsfSensorFaults, EsfSensorStatusCalibration, EsfSensorStatusTime, EsfSensorType, + EsfStatusFusionMode, EsfStatusImuInit, EsfStatusInsInit, EsfStatusMountAngle, + EsfStatusWheelTickInit, GpsFix, NavPvtFlags, NavPvtFlags2, +}; + +use crate::ui::LogWidget; + +pub struct TabsState<'a> { + 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); +}