mod models; mod workers; use crate::models::*; use crate::workers::*; use chrono::Local; use crossbeam_channel::{unbounded, Receiver, Sender}; use eframe::egui; use egui_plot::{Line, Plot, PlotPoints}; use std::collections::{HashMap, VecDeque}; use std::sync::{Arc, Mutex}; use std::thread; use rfd::FileDialog; use regex::Regex; #[allow(dead_code)] struct MarteDebugApp { connected: bool, is_breaking: bool, config: ConnectionConfig, shared_config: Arc>, app_tree: Option, id_to_meta: Arc>>, traced_signals: Arc>>, plots: Vec, forced_signals: HashMap, logs: VecDeque, log_filters: LogFilters, active_bottom_tab: BottomTab, app_states: HashMap, // ObjectPath -> StateName show_left_panel: bool, show_right_panel: bool, show_bottom_panel: bool, selected_node: String, node_info: String, udp_dropped: u64, telem_match_count: HashMap, forcing_dialog: Option, monitoring_dialog: Option, message_dialog: Option, style_editor: Option<(usize, usize)>, tx_cmd: Sender, rx_events: Receiver, internal_tx: Sender, shared_x_range: Option<[f64; 2]>, scope: ScopeSettings, active_main_tab: MainTab, full_config: Option, raw_config_str: String, state_configs: HashMap, bottom_panel_height: f32, } impl MarteDebugApp { fn new(_cc: &eframe::CreationContext<'_>) -> Self { let (tx_cmd, rx_cmd_internal) = unbounded::(); let (tx_events, rx_events) = unbounded::(); let internal_tx = tx_events.clone(); let config = ConnectionConfig { ip: "127.0.0.1".to_string(), tcp_port: "8080".to_string(), udp_port: "8081".to_string(), log_port: "8082".to_string(), version: 0, }; let shared_config = Arc::new(Mutex::new(config.clone())); let id_to_meta = Arc::new(Mutex::new(HashMap::new())); let traced_signals = Arc::new(Mutex::new(HashMap::new())); let id_to_meta_clone = id_to_meta.clone(); let traced_signals_clone = traced_signals.clone(); let shared_config_cmd = shared_config.clone(); let tx_events_c = tx_events.clone(); thread::spawn(move || { tcp_command_worker(shared_config_cmd, rx_cmd_internal, tx_events_c); }); let shared_config_log = shared_config.clone(); let tx_events_log = tx_events.clone(); thread::spawn(move || { tcp_log_worker(shared_config_log, tx_events_log); }); let shared_config_udp = shared_config.clone(); let tx_events_udp = tx_events.clone(); thread::spawn(move || { udp_worker( shared_config_udp, id_to_meta_clone, traced_signals_clone, tx_events_udp, ); }); Self { connected: false, is_breaking: false, config, shared_config, app_tree: None, id_to_meta, traced_signals, plots: vec![PlotInstance { id: "Plot 1".to_string(), plot_type: PlotType::Normal, signals: Vec::new(), auto_bounds: true, }], forced_signals: HashMap::new(), logs: VecDeque::with_capacity(2000), log_filters: LogFilters { show_debug: true, show_info: true, show_warning: true, show_error: true, paused: false, content_regex: "".to_string(), }, active_bottom_tab: BottomTab::Logs, app_states: HashMap::new(), show_left_panel: true, show_right_panel: true, show_bottom_panel: true, selected_node: "".to_string(), node_info: "".to_string(), udp_dropped: 0, telem_match_count: HashMap::new(), forcing_dialog: None, monitoring_dialog: None, message_dialog: None, style_editor: None, tx_cmd, rx_events, internal_tx, shared_x_range: None, scope: ScopeSettings { enabled: false, window_ms: 1000.0, mode: AcquisitionMode::FreeRun, paused: false, trigger_type: TriggerType::Continuous, trigger_source: "".to_string(), trigger_edge: TriggerEdge::Rising, trigger_threshold: 0.0, pre_trigger_percent: 25.0, trigger_active: false, last_trigger_time: 0.0, is_armed: true, }, active_main_tab: MainTab::Plots, full_config: None, raw_config_str: "".to_string(), state_configs: HashMap::new(), bottom_panel_height: 250.0, } } fn next_color(idx: usize) -> egui::Color32 { let colors = [ egui::Color32::from_rgb(100, 200, 255), egui::Color32::from_rgb(255, 100, 100), egui::Color32::from_rgb(100, 255, 100), egui::Color32::from_rgb(255, 200, 100), egui::Color32::from_rgb(255, 100, 255), egui::Color32::from_rgb(100, 255, 255), egui::Color32::from_rgb(200, 255, 100), egui::Color32::WHITE, ]; colors[idx % colors.len()] } fn apply_trigger_logic(&mut self) { if self.scope.mode != AcquisitionMode::Triggered || !self.scope.is_armed { return; } if self.scope.trigger_source.is_empty() { return; } let data_map = self.traced_signals.lock().unwrap(); if let Some(data) = data_map.get(&self.scope.trigger_source) { if data.values.len() < 2 { return; } let start_idx = if data.values.len() > 100 { data.values.len() - 100 } else { 0 }; for i in (start_idx + 1..data.values.len()).rev() { let v_prev = data.values[i - 1][1]; let v_curr = data.values[i][1]; let t_curr = data.values[i][0]; if t_curr <= self.scope.last_trigger_time { continue; } let triggered = match self.scope.trigger_edge { TriggerEdge::Rising => { v_prev < self.scope.trigger_threshold && v_curr >= self.scope.trigger_threshold } TriggerEdge::Falling => { v_prev > self.scope.trigger_threshold && v_curr <= self.scope.trigger_threshold } TriggerEdge::Both => { (v_prev < self.scope.trigger_threshold && v_curr >= self.scope.trigger_threshold) || (v_prev > self.scope.trigger_threshold && v_curr <= self.scope.trigger_threshold) } }; if triggered { self.scope.last_trigger_time = t_curr; self.scope.trigger_active = true; if self.scope.trigger_type == TriggerType::Single { self.scope.is_armed = false; } break; } } } } fn get_all_objects(&self) -> Vec { let mut objects = Vec::new(); if let Some(tree) = &self.app_tree { fn collect(item: &TreeItem, path: String, objects: &mut Vec) { let current_path = if path.is_empty() { if item.name == "Root" { "".to_string() } else { item.name.clone() } } else { format!("{}.{}", path, item.name) }; if !current_path.is_empty() && !item.class.contains("Signal") { objects.push(current_path.clone()); } if let Some(children) = &item.children { for child in children { collect(child, current_path.clone(), objects); } } } collect(tree, "".to_string(), &mut objects); } objects.sort(); objects } fn marte_config_to_string(val: &serde_json::Value, indent: usize) -> String { let mut s = String::new(); let pad = " ".repeat(indent); if let Some(obj) = val.as_object() { let mut keys: Vec<_> = obj.keys().collect(); keys.sort_by(|a, b| { if *a == "Class" { return std::cmp::Ordering::Less; } if *b == "Class" { return std::cmp::Ordering::Greater; } a.cmp(b) }); for key in keys { if key == "Name" && indent > 0 { continue; } // Usually name is in the parent key prefix let v = obj.get(key).unwrap(); let mut display_key = key.clone(); if (key.parse::().is_ok() || key == "unnamed") && v.is_object() { if let Some(name) = v.get("Name").and_then(|n| n.as_str()) { display_key = format!("+{}", name); } } if display_key.starts_with('+') { s.push_str(&format!("{}{} = ", pad, display_key)); s.push_str(&Self::marte_config_to_string(v, indent + 1)); } else if v.is_object() { s.push_str(&format!("{}{} = ", pad, display_key)); s.push_str(&Self::marte_config_to_string(v, indent + 1)); } else { let val_str = v.as_str().unwrap_or(&v.to_string()).to_string(); if !val_str.is_empty() { s.push_str(&format!("{}{} = {}\n", pad, display_key, val_str)); } } } } format!("{{\n{}{}}}\n", s, " ".repeat(indent.saturating_sub(1))) } fn get_config_at_path<'a>(config: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> { let mut current = config; for (i, part) in path.split('.').enumerate() { if part.is_empty() { continue; } if let Some(next) = Self::get_cfg(current, part) { current = next; } else if i == 0 { // If it's the first part, it might be the root itself (exported by ORD) // or we are ALREADY inside it. But usually ORD export includes the root name. // Let's just return None if not found. return None; } else { return None; } } Some(current) } fn get_cfg<'a>(val: &'a serde_json::Value, key: &str) -> Option<&'a serde_json::Value> { val.get(key).or_else(|| val.get(format!("+{}", key))) } fn render_tree(&mut self, ui: &mut egui::Ui, item: &TreeItem, path: String) { let current_path = if path.is_empty() { if item.name == "Root" { "".to_string() } else { item.name.clone() } } else { format!("{}.{}", path, item.name) }; let label = if item.class == "Signal" { format!("📈 {}", item.name) } else { item.name.clone() }; if let Some(children) = &item.children { let header = egui::CollapsingHeader::new(format!("{} [{}]", label, item.class)) .id_salt(¤t_path); header.show(ui, |ui| { ui.horizontal(|ui| { if !current_path.is_empty() { if ui.selectable_label(self.selected_node == current_path, "ℹ Info").clicked() { self.selected_node = current_path.clone(); let _ = self.tx_cmd.send(format!("INFO {}", current_path)); } } }); for child in children { self.render_tree(ui, child, current_path.clone()); } }); } else { ui.horizontal(|ui| { if ui.selectable_label(self.selected_node == current_path, format!("{} [{}]", label, item.class)).clicked() { self.selected_node = current_path.clone(); let _ = self.tx_cmd.send(format!("INFO {}", current_path)); } if item.class.contains("Signal") { let elements = item.elements.unwrap_or(1); if elements > 1 { let header = egui::CollapsingHeader::new(format!("{} [{}] ({} elems)", label, item.class, elements)) .id_salt(¤t_path); header.show(ui, |ui| { for i in 0..elements { let elem_path = format!("{}[{}]", current_path, i); ui.horizontal(|ui| { ui.label(format!("{}[{}]", item.name, i)); if ui.button("Trace").clicked() { let _ = self.tx_cmd.send(format!("TRACE {} 1", current_path)); let _ = self.internal_tx.send(InternalEvent::TraceRequested(elem_path.clone(), false)); } if item.class == "Signal" { if ui.button("Monitor").clicked() { self.monitoring_dialog = Some(MonitorDialog { signal_path: current_path.clone(), period_ms: "100".to_string(), }); let _ = self.internal_tx.send(InternalEvent::TraceRequested(elem_path.clone(), true)); } } }); } }); } else { let traceable = item.is_traceable.unwrap_or(false); let forcable = item.is_forcable.unwrap_or(false); if traceable && ui.button("Trace").clicked() { let _ = self.tx_cmd.send(format!("TRACE {} 1", current_path)); let _ = self.internal_tx.send(InternalEvent::TraceRequested(current_path.clone(), false)); } if item.class == "Signal" { if ui.button("Monitor").clicked() { self.monitoring_dialog = Some(MonitorDialog { signal_path: current_path.clone(), period_ms: "100".to_string(), }); } } if forcable && ui.button("⚡ Force").clicked() { self.forcing_dialog = Some(ForcingDialog { signal_path: current_path.clone(), value: "".to_string(), }); } } } }); } } } impl eframe::App for MarteDebugApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { while let Ok(event) = self.rx_events.try_recv() { match event { InternalEvent::Log(log) => { if !self.log_filters.paused { self.logs.push_back(log); if self.logs.len() > 2000 { self.logs.pop_front(); } } } InternalEvent::InternalLog(msg) => { self.logs.push_back(LogEntry { time: Local::now().format("%H:%M:%S.%3f").to_string(), level: "GUI_INFO".to_string(), message: msg, }); if self.logs.len() > 2000 { self.logs.pop_front(); } } InternalEvent::Discovery(signals) => { let mut metas = self.id_to_meta.lock().unwrap(); metas.clear(); for s in &signals { if s.is_state { let _ = self.internal_tx.send(InternalEvent::InternalLog(format!("Discovered stateful object: {}", s.name))); if let Some(cfg) = &s.config { self.state_configs.insert(s.name.clone(), cfg.clone()); } } let meta = metas.entry(s.id).or_insert_with(|| SignalMetadata { names: Vec::new(), sig_type: s.sig_type.clone(), dimensions: s.dimensions, elements: s.elements, is_state: s.is_state, }); if !meta.names.contains(&s.name) { meta.names.push(s.name.clone()); } } self.logs.push_back(LogEntry { time: Local::now().format("%H:%M:%S").to_string(), level: "GUI_INFO".to_string(), message: format!("Discovery complete: {} signals mapped", signals.len()), }); } InternalEvent::Tree(tree) => { self.app_tree = Some(tree); } InternalEvent::NodeInfo(info) => { self.node_info = info; } InternalEvent::TraceRequested(name, is_monitored) => { let mut data_map = self.traced_signals.lock().unwrap(); let entry = data_map.entry(name.clone()).or_insert_with(|| TraceData { values: VecDeque::with_capacity(10000), last_value: 0.0, recording_tx: None, recording_path: None, is_monitored, }); entry.is_monitored = is_monitored; } InternalEvent::ClearTrace(name) => { let mut data_map = self.traced_signals.lock().unwrap(); data_map.remove(&name); for plot in &mut self.plots { plot.signals.retain(|s| s.source_name != name); } } InternalEvent::Connected => { self.connected = true; let _ = self.internal_tx.send(InternalEvent::InternalLog("Connected to server".to_string())); let _ = self.tx_cmd.send("SERVICE_INFO".to_string()); let _ = self.tx_cmd.send("CONFIG".to_string()); let _ = self.tx_cmd.send("TREE".to_string()); let _ = self.tx_cmd.send("DISCOVER".to_string()); } InternalEvent::Disconnected => { self.connected = false; self.full_config = None; self.app_states.clear(); self.state_configs.clear(); let _ = self.internal_tx.send(InternalEvent::InternalLog("Disconnected from server".to_string())); } InternalEvent::ServiceConfig { udp_port, log_port } => { let mut changed = false; if !udp_port.is_empty() && self.config.udp_port != udp_port { self.config.udp_port = udp_port; changed = true; } if !log_port.is_empty() && self.config.log_port != log_port { self.config.log_port = log_port; changed = true; } if changed { self.config.version += 1; *self.shared_config.lock().unwrap() = self.config.clone(); } } InternalEvent::StateUpdate(path, state) => { let changed = if let Some(old) = self.app_states.get(&path) { old != &state } else { true }; if changed { let _ = self.internal_tx.send(InternalEvent::InternalLog(format!("⭐⭐ STATE CHANGE: {} -> {} ⭐⭐", path, state))); self.app_states.insert(path, state); } } InternalEvent::Config(val) => { self.full_config = Some(val.clone()); self.raw_config_str = Self::marte_config_to_string(&val, 1); // Remove first { and last } since it's the root ORD which usually doesn't have them in MARTe cfg files self.raw_config_str = self.raw_config_str.trim().trim_start_matches('{').trim_end_matches('}').trim().to_string(); let _ = self.internal_tx.send(InternalEvent::InternalLog("Full configuration received".to_string())); } InternalEvent::TelemMatched(id) => { *self.telem_match_count.entry(id).or_insert(0) += 1; } InternalEvent::UdpStats(_) => {} InternalEvent::UdpDropped(count) => { self.udp_dropped += count as u64; } _ => {} } ctx.request_repaint(); } if self.scope.enabled { self.apply_trigger_logic(); } // --- UI Rendering --- egui::TopBottomPanel::top("top").show(ctx, |ui| { ui.horizontal(|ui| { ui.toggle_value(&mut self.show_left_panel, "🗂 Tree"); ui.toggle_value(&mut self.show_right_panel, "📊 Signals"); ui.toggle_value(&mut self.show_bottom_panel, "📜 Bottom"); ui.separator(); if ui.button("➕ Plot").clicked() { self.plots.push(PlotInstance { id: format!("Plot {}", self.plots.len() + 1), plot_type: PlotType::Normal, signals: Vec::new(), auto_bounds: true, }); } ui.separator(); let (btn_text, btn_color) = if self.is_breaking { ("▶ Resume App", egui::Color32::GREEN) } else { ("⏸ Pause App", egui::Color32::YELLOW) }; if ui.button(egui::RichText::new(btn_text).color(btn_color)).clicked() { self.is_breaking = !self.is_breaking; let _ = self.tx_cmd.send(if self.is_breaking { "PAUSE".to_string() } else { "RESUME".to_string() }); } ui.separator(); ui.checkbox(&mut self.scope.enabled, "🔭 Scope"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if ui.button("✉ Send Msg").clicked() { self.message_dialog = Some(MessageDialog { destination: "".to_string(), function: "".to_string(), payload: "".to_string(), expect_reply: false, }); } ui.separator(); ui.menu_button("🔌 Conn", |ui| { egui::Grid::new("conn_grid").num_columns(2).show(ui, |ui| { ui.label("IP:"); ui.text_edit_singleline(&mut self.config.ip); ui.end_row(); ui.label("Control:"); ui.text_edit_singleline(&mut self.config.tcp_port); ui.end_row(); }); if ui.button("🔄 Apply").clicked() { self.config.version += 1; *self.shared_config.lock().unwrap() = self.config.clone(); ui.close_menu(); } }); }); }); }); // Always show Status Bar egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| { ui.horizontal(|ui| { if self.connected { ui.label(egui::RichText::new("✔ Connected").color(egui::Color32::GREEN)); if let Some(state) = self.app_states.get("App") { ui.separator(); ui.label(format!("Active State: {}", state)); } } else { ui.label(egui::RichText::new("✘ Disconnected").color(egui::Color32::RED)); } }); }); if self.show_left_panel { egui::SidePanel::left("left").resizable(true).show(ctx, |ui| { ui.horizontal(|ui| { ui.heading("MARTe Tree"); if ui.button("🔄").clicked() { let _ = self.tx_cmd.send("TREE".to_string()); let _ = self.tx_cmd.send("DISCOVER".to_string()); } }); ui.separator(); egui::ScrollArea::vertical().show(ui, |ui| { if let Some(tree) = self.app_tree.clone() { self.render_tree(ui, &tree, "".to_string()); } else { ui.label("No tree data. Connect or Refresh."); } }); }); } if self.show_right_panel { egui::SidePanel::right("right").resizable(true).show(ctx, |ui| { ui.heading("Signals"); let mut names: Vec<_> = { let data_map = self.traced_signals.lock().unwrap(); data_map.keys().cloned().collect() }; names.sort(); egui::ScrollArea::vertical().show(ui, |ui| { for key in names { let mut data_map = self.traced_signals.lock().unwrap(); if let Some(entry) = data_map.get_mut(&key) { ui.horizontal(|ui| { let is_recording = entry.recording_tx.is_some(); if is_recording { ui.label(egui::RichText::new("●").color(egui::Color32::RED)); } let label = format!("{}: {:.2}", key, entry.last_value); let resp = ui.add(egui::Label::new(label).sense(egui::Sense::drag().union(egui::Sense::click()))); if resp.drag_started() { ctx.data_mut(|d| d.insert_temp(egui::Id::new("drag_signal"), key.clone())); } resp.context_menu(|ui| { if !is_recording { if ui.button("⏺ Record to Parquet").clicked() { let tx = self.internal_tx.clone(); let name_clone = key.clone(); thread::spawn(move || { if let Some(path) = FileDialog::new() .add_filter("Parquet", &["parquet"]) .save_file() { let _ = tx.send(InternalEvent::RecordPathChosen( name_clone, path.to_string_lossy().to_string(), )); } }); ui.close_menu(); } } else { if ui.button("⏹ Stop").clicked() { entry.recording_tx = None; ui.close_menu(); } } }); if ui.button("❌").clicked() { if entry.is_monitored { let _ = self.tx_cmd.send(format!("UNMONITOR SIGNAL {}", key)); } else { let _ = self.tx_cmd.send(format!("TRACE {} 0", key)); } let _ = self.internal_tx.send(InternalEvent::ClearTrace(key.clone())); } }); } } }); }); } if self.show_bottom_panel { let panel_resp = egui::TopBottomPanel::bottom("bottom_tab_panel") .resizable(true) .default_height(self.bottom_panel_height) .show(ctx, |ui| { ui.horizontal(|ui| { ui.selectable_value(&mut self.active_bottom_tab, BottomTab::Logs, "📜 Logs"); ui.selectable_value(&mut self.active_bottom_tab, BottomTab::State, "⚙ State"); }); ui.separator(); match self.active_bottom_tab { BottomTab::Logs => { ui.horizontal(|ui| { ui.checkbox(&mut self.log_filters.show_debug, "Debug"); ui.checkbox(&mut self.log_filters.show_info, "Info"); ui.checkbox(&mut self.log_filters.show_warning, "Warning"); ui.checkbox(&mut self.log_filters.show_error, "Error"); ui.separator(); ui.checkbox(&mut self.log_filters.paused, "⏸ Pause"); if ui.button("🗑 Clear").clicked() { self.logs.clear(); } ui.separator(); ui.label("Filter:"); ui.text_edit_singleline(&mut self.log_filters.content_regex); }); ui.separator(); egui::ScrollArea::vertical() .stick_to_bottom(true) .auto_shrink([false, false]) .show(ui, |ui| { let regex = if !self.log_filters.content_regex.is_empty() { Regex::new(&self.log_filters.content_regex).ok() } else { None }; for log in &self.logs { let show = match log.level.as_str() { "Debug" => self.log_filters.show_debug, "Information" | "GUI_INFO" | "GUI_WARN" | "CMD_RESP" => self.log_filters.show_info, "Warning" => self.log_filters.show_warning, "FatalError" | "OSError" | "ParametersError" | "GUI_ERROR" | "REC_ERROR" | "STATE_CHG" => self.log_filters.show_error, _ => true, }; if !show { continue; } if let Some(re) = ®ex { if !re.is_match(&log.message) && !re.is_match(&log.level) { continue; } } ui.horizontal(|ui| { ui.label(egui::RichText::new(&log.time).weak()); ui.label(egui::RichText::new(format!("[{}]", log.level)).strong()); ui.add(egui::Label::new(&log.message).wrap()); }); } }); } BottomTab::State => { ui.horizontal(|ui| { if ui.button("🔄 Refresh Config").clicked() { let _ = self.tx_cmd.send("CONFIG".to_string()); } if let Some(_) = &self.full_config { ui.label(egui::RichText::new("✅ Config Loaded").color(egui::Color32::GREEN)); } else { ui.label(egui::RichText::new("❌ No Config").color(egui::Color32::RED)); } }); ui.separator(); egui::ScrollArea::vertical() .auto_shrink([false, false]) .show(ui, |ui| { if self.app_states.is_empty() { ui.label("No state updates received yet."); ui.label(format!("Telemetry matches: {:?}", self.telem_match_count)); } let mut paths: Vec<_> = self.app_states.keys().cloned().collect(); paths.sort(); for path in paths { let state = self.app_states.get(&path).unwrap(); ui.group(|ui| { ui.horizontal(|ui| { ui.heading(&path); ui.separator(); ui.label(egui::RichText::new(format!("State: {}", state)).color(egui::Color32::GREEN).strong()); }); let obj_cfg_opt = self.state_configs.get(&path).or_else(|| { self.full_config.as_ref().and_then(|config| Self::get_config_at_path(config, &path)) }); if let Some(obj_cfg) = obj_cfg_opt { if let Some(states_cfg) = Self::get_cfg(obj_cfg, "States") { if let Some(cur_state_cfg) = Self::get_cfg(states_cfg, state) { if let Some(threads_cfg) = Self::get_cfg(cur_state_cfg, "Threads") { if let Some(threads_obj) = threads_cfg.as_object() { for (th_name, th_cfg) in threads_obj { let clean_th_name = th_name.trim_start_matches('+'); ui.horizontal(|ui| { let mut th_label = egui::RichText::new(clean_th_name).strong(); if let Some(cpus) = th_cfg.get("CPUs").and_then(|v| v.as_str()) { th_label = egui::RichText::new(format!("{} (CPUs: {})", clean_th_name, cpus)).strong(); } ui.label(th_label); }); if let Some(funcs) = th_cfg.get("Functions").and_then(|v| v.as_str()) { ui.horizontal(|ui| { ui.label(" GAMs:"); // Split by comma or space let gam_list: Vec<&str> = if funcs.contains(',') { funcs.split(',').map(|s| s.trim()).collect() } else { funcs.split_whitespace().collect() }; for (i, gam) in gam_list.iter().enumerate() { if i > 0 { ui.label("➡"); } let _ = ui.button(*gam); } }); } ui.separator(); } } else { ui.label(egui::RichText::new("Threads node is not an object.").color(egui::Color32::RED)); } } else { ui.label(egui::RichText::new(format!("Could not find 'Threads' node in state '{}'.", state)).weak()); } } else { ui.label(egui::RichText::new(format!("Could not find configuration for state '{}'.", state)).weak()); if let Some(obj) = states_cfg.as_object() { ui.label(format!("Available states: {:?}", obj.keys().collect::>())); } } } else { ui.label(egui::RichText::new("Could not find 'States' node in application config.").weak()); if let Some(obj) = obj_cfg.as_object() { ui.label(format!("Available nodes: {:?}", obj.keys().collect::>())); } } } else { ui.label(egui::RichText::new("Configuration not available for this object.").weak()); } }); } }); } } }); self.bottom_panel_height = panel_resp.response.rect.height(); } egui::CentralPanel::default().show(ctx, |ui| { ui.horizontal(|ui| { ui.selectable_value(&mut self.active_main_tab, MainTab::Plots, "📈 Plots"); ui.selectable_value(&mut self.active_main_tab, MainTab::Config, "📝 Config"); }); ui.separator(); match self.active_main_tab { MainTab::Plots => { let n_plots = self.plots.len(); let mut to_remove = None; for (p_idx, plot_inst) in self.plots.iter_mut().enumerate() { ui.group(|ui| { ui.horizontal(|ui| { ui.label(egui::RichText::new(&plot_inst.id).strong()); if ui.button("🗑").clicked() { to_remove = Some(p_idx); } }); let mut plot = Plot::new(&plot_inst.id).height(ui.available_height() / (n_plots - p_idx) as f32 - 30.0); if self.scope.enabled { let data_map = self.traced_signals.lock().unwrap(); let mut latest_t = 0.0; for sig_cfg in &plot_inst.signals { if let Some(data) = data_map.get(&sig_cfg.source_name) { if let Some(last) = data.values.back() { if last[0] > latest_t { latest_t = last[0]; } } } } let window_s = self.scope.window_ms / 1000.0; let x_min = latest_t - window_s; plot = plot.include_x(x_min).include_x(latest_t); } let plot_resp = plot.show(ui, |plot_ui| { let data_map = self.traced_signals.lock().unwrap(); for sig_cfg in &plot_inst.signals { if let Some(data) = data_map.get(&sig_cfg.source_name) { let points: Vec<_> = data.values.iter().rev().take(5000).rev().map(|p| [p[0], p[1]]).collect(); plot_ui.line(Line::new(PlotPoints::from(points)).color(sig_cfg.color).name(&sig_cfg.label)); } } }); if plot_resp.response.hovered() && ctx.input(|i| i.pointer.any_released()) { if let Some(dropped) = ctx.data_mut(|d| d.get_temp::(egui::Id::new("drag_signal"))) { let color = Self::next_color(plot_inst.signals.len()); plot_inst.signals.push(SignalPlotConfig { source_name: dropped.clone(), label: dropped.clone(), unit: "".to_string(), color, line_style: egui_plot::LineStyle::Solid, marker_type: MarkerType::None, gain: 1.0, offset: 0.0, }); ctx.data_mut(|d| d.remove_temp::(egui::Id::new("drag_signal"))); } } }); } if let Some(idx) = to_remove { self.plots.remove(idx); } } MainTab::Config => { egui::ScrollArea::vertical().show(ui, |ui| { let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| { let mut job = egui::text::LayoutJob::default(); for line in string.lines() { if let Some(eq_idx) = line.find('=') { let key = &line[..eq_idx]; let val = &line[eq_idx..]; let key_color = if key.trim().starts_with('+') { egui::Color32::from_rgb(255, 165, 0) // Orange for nodes } else { egui::Color32::from_rgb(100, 200, 255) // Blue for properties }; job.append(key, 0.0, egui::TextFormat { font_id: egui::FontId::monospace(14.0), color: key_color, ..Default::default() }); if val.contains('{') || val.contains('}') { job.append(val, 0.0, egui::TextFormat { font_id: egui::FontId::monospace(14.0), color: egui::Color32::GRAY, ..Default::default() }); } else { let val_color = if key.trim() == "Class" { egui::Color32::from_rgb(255, 100, 255) // Pink for classes } else { egui::Color32::from_rgb(150, 255, 150) // Green for values }; job.append(val, 0.0, egui::TextFormat { font_id: egui::FontId::monospace(14.0), color: val_color, ..Default::default() }); } } else { job.append(line, 0.0, egui::TextFormat { font_id: egui::FontId::monospace(14.0), color: egui::Color32::GRAY, ..Default::default() }); } job.append("\n", 0.0, Default::default()); } ui.fonts(|f| f.layout_job(job)) }; ui.add( egui::TextEdit::multiline(&mut self.raw_config_str) .font(egui::FontId::monospace(14.0)) .code_editor() .lock_focus(true) .desired_width(f32::INFINITY) .layouter(&mut layouter) ); }); } } }); // Dialogs handling if self.forcing_dialog.is_some() { let mut dialog = self.forcing_dialog.take().unwrap(); let mut close = false; egui::Window::new("Force Signal").show(ctx, |ui| { ui.label(format!("Signal: {}", dialog.signal_path)); ui.horizontal(|ui| { ui.label("Value:"); ui.text_edit_singleline(&mut dialog.value); }); ui.horizontal(|ui| { if ui.button("⚡ Force").clicked() { let _ = self.tx_cmd.send(format!("FORCE {} {}", dialog.signal_path, dialog.value)); close = true; } if ui.button("🔓 Unforce").clicked() { let _ = self.tx_cmd.send(format!("UNFORCE {}", dialog.signal_path)); close = true; } if ui.button("Cancel").clicked() { close = true; } }); }); if !close { self.forcing_dialog = Some(dialog); } } if self.monitoring_dialog.is_some() { let mut dialog = self.monitoring_dialog.take().unwrap(); let mut close = false; egui::Window::new("Monitor Signal").show(ctx, |ui| { ui.label(format!("Signal: {}", dialog.signal_path)); ui.horizontal(|ui| { ui.label("Period (ms):"); ui.text_edit_singleline(&mut dialog.period_ms); }); ui.horizontal(|ui| { if ui.button("📈 Monitor").clicked() { let _ = self.tx_cmd.send(format!("MONITOR SIGNAL {} {}", dialog.signal_path, dialog.period_ms)); let _ = self.internal_tx.send(InternalEvent::TraceRequested(dialog.signal_path.clone(), true)); close = true; } if ui.button("⏹ Stop").clicked() { let _ = self.tx_cmd.send(format!("UNMONITOR SIGNAL {}", dialog.signal_path)); let _ = self.internal_tx.send(InternalEvent::ClearTrace(dialog.signal_path.clone())); close = true; } if ui.button("Cancel").clicked() { close = true; } }); }); if !close { self.monitoring_dialog = Some(dialog); } } if self.message_dialog.is_some() { let mut dialog = self.message_dialog.take().unwrap(); let mut close = false; egui::Window::new("Send Message").show(ctx, |ui| { let objects = self.get_all_objects(); egui::ComboBox::from_label("Target").selected_text(&dialog.destination).show_ui(ui, |ui| { for obj in objects { ui.selectable_value(&mut dialog.destination, obj.clone(), obj); } }); ui.text_edit_singleline(&mut dialog.function); ui.text_edit_multiline(&mut dialog.payload); if ui.button("Send").clicked() { let cmd = format!("MSG {} {} {} {}", dialog.destination, dialog.function, if dialog.expect_reply { "1" } else { "0" }, dialog.payload.replace('\n', "\\n")); let _ = self.tx_cmd.send(cmd); close = true; } if ui.button("Cancel").clicked() { close = true; } }); if !close { self.message_dialog = Some(dialog); } } ctx.request_repaint(); } } fn main() -> Result<(), eframe::Error> { eframe::run_native( "MARTe2 Debug Explorer", eframe::NativeOptions::default(), Box::new(|cc| Ok(Box::new(MarteDebugApp::new(cc)))), ) }