diff --git a/hil_config/presets.json b/hil_config/presets.json new file mode 100644 index 0000000..a3be7a4 --- /dev/null +++ b/hil_config/presets.json @@ -0,0 +1,3 @@ +{ + "All Heartbeats": [ "heartbeats_crit", "heartbeats_non_crit" ] +} \ No newline at end of file diff --git a/hil_config/schema.json b/hil_config/schema.json new file mode 100644 index 0000000..76b0d2d --- /dev/null +++ b/hil_config/schema.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + + "required": ["name", "description", "expect"], + + "properties": { + "$schema": { + "type": "string" + }, + + "name": { + "type": "string" + }, + + "description": { + "type": "string" + }, + + "tx": { + "type": "array", + "items": { + "type": "object", + "required": ["timestamp", "msg_name", "signals"], + + "properties": { + "timestamp": { + "type": "number" + }, + + "msg_name": { + "type": "string" + }, + + "signals": { + "type": "object", + "additionalProperties": { + "type": "number" + } + } + }, + + "additionalProperties": false + } + }, + + "expect": { + "type": "array", + "items": { + "type": "object", + "required": ["window", "msg_name"], + + "properties": { + "window": { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 2, + "maxItems": 2 + }, + + "msg_name": { + "type": "string" + }, + + "signals": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 2, + "maxItems": 2 + } + } + }, + + "additionalProperties": false + } + } + }, + + "additionalProperties": false +} \ No newline at end of file diff --git a/hil_config/tests/heartbeats_crit.json b/hil_config/tests/heartbeats_crit.json new file mode 100644 index 0000000..26f5c7b --- /dev/null +++ b/hil_config/tests/heartbeats_crit.json @@ -0,0 +1,25 @@ +{ + "$schema": "../schema.json", + + "name": "Critical Heartbeats", + "description": "Check that we receive a heartbeat (version hash) from every drive critical board", + + "expect": [ + { + "window": [0, 10000], + "msg_name": "abox_version" + }, + { + "window": [0, 10000], + "msg_name": "dash_version" + }, + { + "window": [0, 10000], + "msg_name": "main_version" + }, + { + "window": [0, 10000], + "msg_name": "pdu_version" + } + ] +} \ No newline at end of file diff --git a/hil_config/tests/heartbeats_non_crit.json b/hil_config/tests/heartbeats_non_crit.json new file mode 100644 index 0000000..dcb9bfb --- /dev/null +++ b/hil_config/tests/heartbeats_non_crit.json @@ -0,0 +1,17 @@ +{ + "$schema": "../schema.json", + + "name": "Non-Critical Heartbeats", + "description": "Check that we receive a heartbeat (version hash) from non-critical boards", + + "expect": [ + { + "window": [0, 10000], + "msg_name": "front_driveline_version" + }, + { + "window": [0, 10000], + "msg_name": "rear_driveline_version" + } + ] +} \ No newline at end of file diff --git a/hil_config/tests/idk.json b/hil_config/tests/idk.json new file mode 100644 index 0000000..5e9441b --- /dev/null +++ b/hil_config/tests/idk.json @@ -0,0 +1,27 @@ +{ + "$schema": "../schema.json", + + "name": "Pack sanity check", + "description": "Verify thermistor response", + + "tx": [ + { + "timestamp": 15, + "msg_name": "pack_stats", + "signals": { + "pack_voltage": 540.0, + "pack_current": 10.0, + "max_temp": 35.0 + } + } + ], + "expect": [ + { + "window": [15, 20], + "msg_name": "thermistor_telemetry", + "signals": { + "temperature": [30.0, 40.0] + } + } + ] +} \ No newline at end of file diff --git a/src/action.rs b/src/action.rs index c24f17b..e349a7c 100644 --- a/src/action.rs +++ b/src/action.rs @@ -25,6 +25,7 @@ pub enum WidgetType { GgPlot, Dynamics, Jitter, + Hil, } impl AppAction { @@ -41,6 +42,7 @@ impl AppAction { ("Spawn G-G Plot", WidgetType::GgPlot), ("Spawn Dynamics", WidgetType::Dynamics), ("Spawn Jitter", WidgetType::Jitter), + ("Spawn HIL", WidgetType::Hil), ] } } diff --git a/src/app.rs b/src/app.rs index ea9615c..89fdabb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -51,6 +51,7 @@ pub struct DAQApp { pub next_gg_plot_num: usize, pub next_dynamics_num: usize, pub next_jitter_num: usize, + pub next_hil_num: usize, pub can_to_ui_rx: std::sync::mpsc::Receiver, pub ui_to_can_tx: std::sync::mpsc::Sender, pub action_queue: Vec, @@ -107,6 +108,7 @@ impl DAQApp { next_gg_plot_num: 1, next_dynamics_num: 1, next_jitter_num: 1, + next_hil_num: 1, can_to_ui_rx, ui_to_can_tx, action_queue: Vec::new(), @@ -211,6 +213,9 @@ impl DAQApp { action::WidgetType::Jitter => { widgets::Widget::Jitter(ui::jitter::Jitter::new(self.next_jitter_num)) } + action::WidgetType::Hil => { + widgets::Widget::Hil(ui::hil::Hil::new(self.next_hil_num)) + } }; self.add_widget_to_tree(widget); @@ -252,6 +257,9 @@ impl DAQApp { action::WidgetType::Jitter => { self.next_jitter_num += 1; } + action::WidgetType::Hil => { + self.next_hil_num += 1; + } } } action::AppAction::ToggleSidebar => { diff --git a/src/hil/config.rs b/src/hil/config.rs new file mode 100644 index 0000000..e43c1d1 --- /dev/null +++ b/src/hil/config.rs @@ -0,0 +1,159 @@ +// Read HIL config files (preset and tests) + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +pub const PRESETS_FILE: &str = "hil_config/presets.json"; +pub const TESTS_FOLDER: &str = "hil_config/tests/"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct PresetsFile(pub HashMap>); + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TestFile { + pub name: String, + pub description: String, + + #[serde(default)] + pub tx: Vec, + + pub expect: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TxMessage { + pub timestamp: f64, + pub msg_name: String, + + pub signals: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Expectation { + pub window: [f64; 2], + + pub msg_name: String, + + #[serde(default)] + pub signals: HashMap, +} + +#[derive(Clone)] +pub struct PresetInfo { + pub name: String, + // base names + pub tests: Vec, +} + +#[derive(Clone)] +pub struct TestInfo { + pub basename: String, + pub name: String, + pub description: String, +} + +pub fn list_available_tests() -> (Vec, Vec, Vec) { + let mut errors = Vec::new(); + + // Load individual tests + let mut individual_tests = Vec::new(); + if let Ok(entries) = std::fs::read_dir(TESTS_FOLDER) { + for entry in entries.flatten() { + let path = entry.path(); + if !path.is_file() || path.extension().and_then(|e| e.to_str()) != Some("json") { + continue; + } + + let basename = match path.file_stem().and_then(|s| s.to_str()) { + Some(stem) if !stem.is_empty() => stem.to_string(), + _ => { + errors.push(format!( + "Failed to determine test basename for file: {:?}", + path + )); + continue; + } + }; + + let content = match std::fs::read_to_string(&path) { + Ok(c) => c, + Err(_) => { + errors.push(format!("Failed to read test file: {:?}", path)); + continue; + } + }; + + match serde_json::from_str::(&content) { + Ok(test_data) => individual_tests.push(TestInfo { + basename, + name: test_data.name, + description: test_data.description, + }), + Err(_) => errors.push(format!("Failed to parse test file: {:?}", path)), + }; + } + } else { + errors.push(format!("Failed to read tests directory: {}", TESTS_FOLDER)); + } + individual_tests.sort_by(|a, b| a.basename.to_lowercase().cmp(&b.basename.to_lowercase())); + + // Load presets + let mut presets = Vec::new(); + if let Ok(presets_file) = std::fs::read_to_string(PRESETS_FILE) { + if let Ok(presets_data) = serde_json::from_str::(&presets_file) { + for (preset_name, subtests) in presets_data.0 { + if subtests.is_empty() { + errors.push(format!("Preset '{}' has no subtests defined", preset_name)); + continue; + } + + if subtests.iter().any(|s| s.trim().is_empty()) { + errors.push(format!( + "Preset '{}' contains empty subtest names", + preset_name + )); + continue; + } + + if subtests + .iter() + .any(|s| individual_tests.iter().all(|t| t.basename != *s)) + { + errors.push(format!( + "Preset '{}' contains subtests that do not exist: {:?}", + preset_name, + subtests + .iter() + .filter(|s| individual_tests.iter().all(|t| t.basename != **s)) + .collect::>() + )); + continue; + } + + presets.push(PresetInfo { + name: preset_name, + tests: subtests, + }); + } + } else { + errors.push(format!("Failed to parse presets file: {}", PRESETS_FILE)); + } + } else { + errors.push(format!("Failed to read presets file: {}", PRESETS_FILE)); + } + presets.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + + (presets, individual_tests, errors) +} + +pub fn load_test_from_file(basename: &str) -> Result { + let path = format!("{}{}.json", TESTS_FOLDER, basename); + match std::fs::read_to_string(&path) { + Ok(content) => match serde_json::from_str::(&content) { + Ok(test_data) => Ok(test_data), + Err(_) => Err(format!("Failed to parse test file: {}", path)), + }, + Err(_) => Err(format!("Failed to read test file: {}", path)), + } +} diff --git a/src/hil/mod.rs b/src/hil/mod.rs new file mode 100644 index 0000000..bc07c78 --- /dev/null +++ b/src/hil/mod.rs @@ -0,0 +1,2 @@ +pub mod config; +pub mod run; diff --git a/src/hil/run.rs b/src/hil/run.rs new file mode 100644 index 0000000..bd4ff61 --- /dev/null +++ b/src/hil/run.rs @@ -0,0 +1,173 @@ +use eframe::egui; + +use crate::{hil, messages}; + +#[derive(PartialEq, Eq)] +pub enum ExpectResult { + NotInWindow, + InProgress, + Passed, + FailedNoMessage, + FailedValueOutOfRange, +} + +pub struct InProgressExpect { + pub expect: hil::config::Expectation, + pub result: ExpectResult, +} + +pub struct HilRunningTest { + pub test_info: hil::config::TestInfo, + pub tx_remaining: Vec, + pub in_progress_expects: Vec, +} + +pub enum HilState { + Idle, + Running { + start_time: std::time::Instant, + preset_info: Option, + tests: Vec, + }, +} + +impl ExpectResult { + pub fn as_str(&self) -> &'static str { + match self { + ExpectResult::NotInWindow => "Not in window", + ExpectResult::InProgress => "In progress", + ExpectResult::Passed => "Passed", + ExpectResult::FailedNoMessage => "Failed (no message)", + ExpectResult::FailedValueOutOfRange => "Failed (value out of range)", + } + } + + pub fn as_color32(&self) -> egui::Color32 { + match self { + ExpectResult::NotInWindow => egui::Color32::GRAY, + ExpectResult::InProgress => egui::Color32::YELLOW, + ExpectResult::Passed => egui::Color32::GREEN, + ExpectResult::FailedNoMessage | ExpectResult::FailedValueOutOfRange => { + egui::Color32::RED + } + } + } + + pub fn is_finished(&self) -> bool { + matches!( + self, + ExpectResult::Passed + | ExpectResult::FailedNoMessage + | ExpectResult::FailedValueOutOfRange + ) + } +} + +impl HilRunningTest { + pub fn new(test_info: &hil::config::TestInfo) -> Result { + let test = hil::config::load_test_from_file(&test_info.basename)?; + let mut in_progress_expects = test + .expect + .into_iter() + .map(InProgressExpect::new) + .collect::>(); + in_progress_expects.sort_by(|a, b| a.expect.window[0].total_cmp(&b.expect.window[0])); + Ok(Self { + test_info: test_info.clone(), + tx_remaining: test.tx, + in_progress_expects, + }) + } + + pub fn update_expect_statuses(&mut self, start_time: std::time::Instant) { + let ts = start_time.elapsed().as_millis(); + for expect in &mut self.in_progress_expects { + match expect.result { + ExpectResult::NotInWindow => { + if ts >= expect.expect.window[0] as u128 { + expect.result = ExpectResult::InProgress; + } + } + ExpectResult::InProgress => { + if ts > expect.expect.window[1] as u128 { + expect.result = ExpectResult::FailedNoMessage; + } + } + _ => {} + } + } + } + + pub fn process_can( + &mut self, + parsed: &messages::ParsedMessage, + start_time: std::time::Instant, + ) { + self.update_expect_statuses(start_time); + + for expect in &mut self.in_progress_expects { + if expect.result == ExpectResult::InProgress && expect.matches_message(&parsed.decoded) + { + if expect.expect.signals.is_empty() { + expect.result = ExpectResult::Passed; + } else { + let mut all_signals_in_range = true; + for (sig_name, sig_range) in &expect.expect.signals { + if let Some(sig_value) = parsed.decoded.signals.get(sig_name) { + if sig_value.value.physical < sig_range[0] + || sig_value.value.physical > sig_range[1] + { + all_signals_in_range = false; + break; + } + } else { + all_signals_in_range = false; + break; + } + } + + expect.result = if all_signals_in_range { + ExpectResult::Passed + } else { + ExpectResult::FailedValueOutOfRange + }; + } + } + } + } + + pub fn expect_counts(&self) -> (usize, usize, usize) { + let mut not_in_window = 0; + let mut in_progress = 0; + let mut completed = 0; + + for expect in &self.in_progress_expects { + match expect.result { + ExpectResult::NotInWindow => not_in_window += 1, + ExpectResult::InProgress => in_progress += 1, + _ => completed += 1, + } + } + + (not_in_window, in_progress, completed) + } + + pub fn is_finished(&self) -> bool { + self.in_progress_expects + .iter() + .all(|e| e.result.is_finished()) + } +} + +impl InProgressExpect { + pub fn new(expect: hil::config::Expectation) -> Self { + Self { + expect, + result: ExpectResult::NotInWindow, + } + } + + pub fn matches_message(&self, decoded: &can_decode::DecodedMessage) -> bool { + self.expect.msg_name == decoded.name + } +} diff --git a/src/main.rs b/src/main.rs index 112fb4d..2ca8198 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod connection; mod daq_log_parse; mod formatter; mod frozen; +mod hil; mod messages; mod settings; mod shortcuts; diff --git a/src/ui/hil.rs b/src/ui/hil.rs new file mode 100644 index 0000000..affef2d --- /dev/null +++ b/src/ui/hil.rs @@ -0,0 +1,334 @@ +use crate::{hil, messages}; +use eframe::egui; + +pub struct Hil { + pub title: String, + pub found_presets: Vec, + pub found_tests: Vec, + pub load_errors: Vec, + pub start_error: Option, + pub hil_state: hil::run::HilState, +} + +impl Hil { + pub fn new(instance_num: usize) -> Self { + let (presets, tests, errors) = hil::config::list_available_tests(); + + Self { + title: format!("HIL #{}", instance_num), + found_presets: presets, + found_tests: tests, + load_errors: errors, + start_error: None, + hil_state: hil::run::HilState::Idle, + } + } + + pub fn handle_can_message(&mut self, msg: &messages::MsgFromCan) { + if let messages::MsgFromCan::ParsedMessage(parsed) = msg + && let hil::run::HilState::Running { + start_time, tests, .. + } = &mut self.hil_state + { + for test in tests { + test.process_can(parsed, *start_time); + } + } + } + + fn reload_tests(&mut self) { + let (presets, tests, errors) = hil::config::list_available_tests(); + self.found_presets = presets; + self.found_tests = tests; + self.load_errors = errors; + self.start_error = None; + } + + fn try_start_from_test(&mut self, test: &hil::config::TestInfo) { + self.start_error = None; + + let test = match hil::run::HilRunningTest::new(test) { + Ok(test) => test, + Err(err) => { + self.start_error = Some(format!("Error starting test: {}", err)); + return; + } + }; + + self.hil_state = hil::run::HilState::Running { + start_time: std::time::Instant::now(), + preset_info: None, + tests: vec![test], + }; + } + + fn try_start_from_preset(&mut self, preset: &hil::config::PresetInfo) { + self.start_error = None; + + let mut tests = Vec::new(); + for test_name in &preset.tests { + let test_info = match self.found_tests.iter().find(|t| t.basename == *test_name) { + Some(info) => info, + None => { + self.start_error = Some(format!( + "Error starting preset: Test '{}' not found", + test_name + )); + return; + } + }; + let test = match hil::run::HilRunningTest::new(test_info) { + Ok(test) => test, + Err(err) => { + self.start_error = Some(format!("Error starting preset: {}", err)); + return; + } + }; + tests.push(test); + } + + self.hil_state = hil::run::HilState::Running { + start_time: std::time::Instant::now(), + preset_info: Some(preset.clone()), + tests, + }; + } + + pub fn show(&mut self, ui: &mut egui::Ui) -> egui_tiles::UiResponse { + let mut idle_requestedd = false; + + egui::ScrollArea::vertical().show(ui, |ui| match &mut self.hil_state { + hil::run::HilState::Idle => { + ui.label("HIL is idle. Select a preset or test to run."); + if ui.button("Reload Tests").clicked() { + self.reload_tests(); + } + ui.separator(); + if !self.load_errors.is_empty() { + ui.label("Errors loading tests:"); + for err in &self.load_errors { + ui.label(format!("- {}", err)); + } + ui.separator(); + } + + if let Some(err) = &self.start_error { + ui.colored_label(egui::Color32::RED, err); + ui.separator(); + } + + if !self.found_presets.is_empty() { + ui.label("Presets:"); + let mut selected_preset = None; + for preset in &self.found_presets { + let preset_info = format!("{} - {}", preset.name, preset.tests.join(", ")); + if ui.button(&preset_info).clicked() { + selected_preset = Some(preset.clone()); + } + } + if let Some(preset) = selected_preset { + self.try_start_from_preset(&preset); + } + ui.separator(); + } + if !self.found_tests.is_empty() { + ui.label("Individual Tests:"); + let mut selected_test = None; + for test in &self.found_tests { + let test_info = + format!("{} [{}]: {}", test.name, test.basename, test.description); + if ui.button(&test_info).clicked() { + selected_test = Some(test.clone()); + } + } + if let Some(test) = selected_test { + self.try_start_from_test(&test); + } + } + } + hil::run::HilState::Running { + start_time, + preset_info, + tests, + } => { + // Actually run the test + tests + .iter_mut() + .for_each(|t| t.update_expect_statuses(*start_time)); + let all_finished = tests.iter().all(|t| t.is_finished()); + + ui.add_space(4.0); + if all_finished { + // ui.label("HIL finished! Review the results below."); + ui.horizontal(|ui| { + if ui.button("Exit").clicked() { + idle_requestedd = true; + } + ui.label("HIL finished! Review the results below."); + }); + } else { + ui.horizontal(|ui| { + if ui.button("Stop").clicked() { + idle_requestedd = true; + } + let time_since_start = start_time.elapsed().as_millis(); + ui.label(format!( + "HIL is running... {:.0} ms since start", + time_since_start + )); + }); + } + ui.separator(); + + if let Some(preset) = preset_info { + ui.label(format!( + "Preset: {} ({} tests)", + preset.name, + preset.tests.len() + )); + Self::show_preset_summary(ui, tests); + } + ui.separator(); + + for test in tests { + Self::show_test(ui, test); + ui.separator(); + } + } + }); + + if idle_requestedd { + self.hil_state = hil::run::HilState::Idle; + } + + egui_tiles::UiResponse::None + } + + fn show_preset_summary(ui: &mut egui::Ui, tests: &[hil::run::HilRunningTest]) { + if tests.is_empty() || !tests.iter().all(|t| t.is_finished()) { + return; + } + + let (mut total_passed, mut total_expects, mut subtests_all_passed) = (0, 0, 0); + for t in tests { + let total = t.in_progress_expects.len(); + let passed = t + .in_progress_expects + .iter() + .filter(|e| matches!(e.result, hil::run::ExpectResult::Passed)) + .count(); + total_passed += passed; + total_expects += total; + if passed == total { + subtests_all_passed += 1; + } + } + + let color = if total_passed == total_expects { + egui::Color32::GREEN + } else { + egui::Color32::RED + }; + + ui.colored_label( + color, + format!( + "Preset finished: {}/{} expects passed, {}/{} subtests fully passed", + total_passed, + total_expects, + subtests_all_passed, + tests.len() + ), + ); + } + + fn show_test(ui: &mut egui::Ui, test: &hil::run::HilRunningTest) { + egui::CollapsingHeader::new(&test.test_info.name) + .id_salt(&test.test_info.basename) + .default_open(true) + .show(ui, |ui| { + ui.label(&test.test_info.description); + + Self::show_test_progress(ui, test); + + ui.label(format!("TX remaining: {}", test.tx_remaining.len())); + + Self::show_test_finished_summary(ui, test); + + ui.add_space(4.0); + Self::show_expects_grid(ui, test); + }); + } + + fn show_test_progress(ui: &mut egui::Ui, test: &hil::run::HilRunningTest) { + let (not_in_window, in_progress, completed) = test.expect_counts(); + let total = not_in_window + in_progress + completed; + if total > 0 { + let frac = completed as f32 / total as f32; + ui.add(egui::ProgressBar::new(frac).text(format!("{}/{} complete", completed, total))); + } + } + + fn show_test_finished_summary(ui: &mut egui::Ui, test: &hil::run::HilRunningTest) { + if !test.is_finished() { + return; + } + + let total = test.in_progress_expects.len(); + let passed = test + .in_progress_expects + .iter() + .filter(|e| matches!(e.result, hil::run::ExpectResult::Passed)) + .count(); + + let color = if passed == total { + egui::Color32::GREEN + } else { + egui::Color32::RED + }; + + ui.colored_label( + color, + format!("Test finished: {} passed of {} expects", passed, total), + ); + } + + fn show_expects_grid(ui: &mut egui::Ui, test: &hil::run::HilRunningTest) { + egui::Grid::new(format!("expects_{}", test.test_info.basename)) + .num_columns(4) + .striped(true) + .show(ui, |ui| { + ui.strong("Message"); + ui.strong("Window (ms)"); + ui.strong("Status"); + ui.strong("Signals"); + ui.end_row(); + + for ipe in &test.in_progress_expects { + Self::show_expect_row(ui, ipe); + } + }); + } + + fn show_expect_row(ui: &mut egui::Ui, ipe: &hil::run::InProgressExpect) { + ui.label(&ipe.expect.msg_name); + ui.label(format!( + "{:.0} - {:.0}", + ipe.expect.window[0], ipe.expect.window[1] + )); + ui.colored_label(ipe.result.as_color32(), ipe.result.as_str()); + + if ipe.expect.signals.is_empty() { + ui.label("—"); + } else { + let s: Vec = ipe + .expect + .signals + .iter() + .map(|(n, r)| format!("{}: [{}, {}]", n, r[0], r[1])) + .collect(); + ui.label(s.join(", ")); + } + ui.end_row(); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 6e40f83..1fe6384 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,6 +5,7 @@ pub mod command_palette; pub mod dbc_msg_picker; pub mod dynamics; pub mod gg_plot; +pub mod hil; pub mod jitter; pub mod log_parser; pub mod scope; diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index 170732e..c6f9627 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -100,6 +100,10 @@ pub fn show(app: &mut app::DAQApp, ctx: &egui::Context) { app.action_queue .push(action::AppAction::SpawnWidget(action::WidgetType::Jitter)); } + if ui.button("Add HIL").clicked() { + app.action_queue + .push(action::AppAction::SpawnWidget(action::WidgetType::Hil)); + } ui.separator(); ui.heading("Connection Settings"); diff --git a/src/widgets.rs b/src/widgets.rs index f8c3cca..da79a8a 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -14,6 +14,7 @@ pub enum Widget { GgPlot(ui::gg_plot::GgPlot), Dynamics(ui::dynamics::Dynamics), Jitter(ui::jitter::Jitter), + Hil(ui::hil::Hil), } impl Widget { @@ -31,6 +32,7 @@ impl Widget { Widget::GgPlot(w) => &w.title, Widget::Dynamics(w) => &w.title, Widget::Jitter(w) => &w.title, + Widget::Hil(w) => &w.title, } } @@ -68,6 +70,7 @@ impl Widget { Widget::GgPlot(w) => w.show(ui), Widget::Dynamics(w) => w.show(ui), Widget::Jitter(w) => w.show(ui, parser), + Widget::Hil(w) => w.show(ui), } } @@ -83,6 +86,7 @@ impl Widget { Widget::GgPlot(w) => w.handle_can_message(msg), Widget::Dynamics(w) => w.handle_can_message(msg), Widget::Jitter(w) => w.handle_can_message(msg), + Widget::Hil(w) => w.handle_can_message(msg), _ => {} } }