Files
2026-03-24 21:16:24 +01:00

1036 lines
51 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Mutex<ConnectionConfig>>,
app_tree: Option<TreeItem>,
id_to_meta: Arc<Mutex<HashMap<u32, SignalMetadata>>>,
traced_signals: Arc<Mutex<HashMap<String, TraceData>>>,
plots: Vec<PlotInstance>,
forced_signals: HashMap<String, String>,
logs: VecDeque<LogEntry>,
log_filters: LogFilters,
active_bottom_tab: BottomTab,
app_states: HashMap<String, String>, // 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<u32, u64>,
forcing_dialog: Option<ForcingDialog>,
monitoring_dialog: Option<MonitorDialog>,
message_dialog: Option<MessageDialog>,
style_editor: Option<(usize, usize)>,
tx_cmd: Sender<String>,
rx_events: Receiver<InternalEvent>,
internal_tx: Sender<InternalEvent>,
shared_x_range: Option<[f64; 2]>,
scope: ScopeSettings,
active_main_tab: MainTab,
full_config: Option<serde_json::Value>,
raw_config_str: String,
state_configs: HashMap<String, serde_json::Value>,
bottom_panel_height: f32,
}
impl MarteDebugApp {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
let (tx_cmd, rx_cmd_internal) = unbounded::<String>();
let (tx_events, rx_events) = unbounded::<InternalEvent>();
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<String> {
let mut objects = Vec::new();
if let Some(tree) = &self.app_tree {
fn collect(item: &TreeItem, path: String, objects: &mut Vec<String>) {
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::<usize>().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(&current_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(&current_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) = &regex {
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::<Vec<_>>()));
}
}
} 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::<Vec<_>>()));
}
}
} 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::<String>(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::<String>(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)))),
)
}