Files
marte-debug/Tools/gui_client/src/main.rs
2026-02-23 18:01:24 +01:00

772 lines
41 KiB
Rust
Raw 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.
use eframe::egui;
use egui_plot::{Line, Plot, PlotPoints, MarkerShape, LineStyle, PlotBounds, VLine};
use std::collections::{HashMap, VecDeque};
use std::net::{TcpStream, UdpSocket};
use std::io::{Write, BufReader, BufRead};
use std::sync::{Arc, Mutex};
use std::thread;
use serde::{Deserialize, Serialize};
use chrono::Local;
use crossbeam_channel::{unbounded, Receiver, Sender};
use socket2::{Socket, Domain, Type, Protocol};
use regex::Regex;
use once_cell::sync::Lazy;
static APP_START_TIME: Lazy<std::time::Instant> = Lazy::new(std::time::Instant::now);
// --- Models ---
#[derive(Serialize, Deserialize, Clone, Debug)]
struct Signal {
name: String,
id: u32,
#[serde(rename = "type")]
sig_type: String,
}
#[derive(Deserialize)]
struct DiscoverResponse {
#[serde(rename = "Signals")]
signals: Vec<Signal>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct TreeItem {
#[serde(rename = "Name")]
name: String,
#[serde(rename = "Class")]
class: String,
#[serde(rename = "Children")]
children: Option<Vec<TreeItem>>,
#[serde(rename = "Type")]
sig_type: Option<String>,
#[serde(rename = "Dimensions")]
dimensions: Option<u8>,
#[serde(rename = "Elements")]
elements: Option<u32>,
}
#[derive(Clone)]
struct LogEntry {
time: String,
level: String,
message: String,
}
struct TraceData {
values: VecDeque<[f64; 2]>,
last_value: f64,
}
struct SignalMetadata {
names: Vec<String>,
sig_type: String,
}
#[derive(Clone)]
struct ConnectionConfig {
ip: String,
tcp_port: String,
udp_port: String,
log_port: String,
version: u64,
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum PlotType {
Normal,
LogicAnalyzer,
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum AcquisitionMode {
FreeRun,
Triggered,
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum TriggerEdge {
Rising,
Falling,
Both,
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum TriggerType {
Single,
Continuous,
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum MarkerType {
None,
Circle,
Square,
}
impl MarkerType {
fn to_shape(&self) -> Option<MarkerShape> {
match self {
MarkerType::None => None,
MarkerType::Circle => Some(MarkerShape::Circle),
MarkerType::Square => Some(MarkerShape::Square),
}
}
}
#[derive(Clone)]
struct SignalPlotConfig {
source_name: String,
label: String,
unit: String,
color: egui::Color32,
line_style: LineStyle,
marker_type: MarkerType,
gain: f64,
offset: f64,
}
struct PlotInstance {
id: String,
plot_type: PlotType,
signals: Vec<SignalPlotConfig>,
auto_bounds: bool,
}
enum InternalEvent {
Log(LogEntry),
Discovery(Vec<Signal>),
Tree(TreeItem),
CommandResponse(String),
NodeInfo(String),
Connected,
Disconnected,
InternalLog(String),
TraceRequested(String),
ClearTrace(String),
UdpStats(u64),
UdpDropped(u32),
}
// --- App State ---
struct ForcingDialog {
signal_path: String,
value: String,
}
struct LogFilters {
show_debug: bool,
show_info: bool,
show_warning: bool,
show_error: bool,
paused: bool,
content_regex: String,
}
struct ScopeSettings {
enabled: bool,
window_ms: f64,
mode: AcquisitionMode,
paused: bool,
// Trigger Settings
trigger_type: TriggerType,
trigger_source: String,
trigger_edge: TriggerEdge,
trigger_threshold: f64,
pre_trigger_percent: f64,
// Internal State
trigger_active: bool,
last_trigger_time: f64,
is_armed: bool,
}
struct MarteDebugApp {
connected: bool,
is_breaking: bool,
config: ConnectionConfig,
shared_config: Arc<Mutex<ConnectionConfig>>,
app_tree: Option<TreeItem>,
traced_signals: Arc<Mutex<HashMap<String, TraceData>>>,
id_to_meta: Arc<Mutex<HashMap<u32, SignalMetadata>>>,
plots: Vec<PlotInstance>,
forced_signals: HashMap<String, String>,
logs: VecDeque<LogEntry>,
log_filters: LogFilters,
show_left_panel: bool,
show_right_panel: bool,
show_bottom_panel: bool,
selected_node: String,
node_info: String,
udp_packets: u64,
udp_dropped: u64,
forcing_dialog: Option<ForcingDialog>,
style_editor: Option<(usize, usize)>, // plot_idx, signal_idx
tx_cmd: Sender<String>,
rx_events: Receiver<InternalEvent>,
internal_tx: Sender<InternalEvent>,
shared_x_range: Option<[f64; 2]>,
scope: ScopeSettings,
}
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 shared_config_log = shared_config.clone();
let shared_config_udp = 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 tx_events_log = tx_events.clone();
thread::spawn(move || { tcp_log_worker(shared_config_log, tx_events_log); });
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(),
},
show_left_panel: true,
show_right_panel: true,
show_bottom_panel: true,
selected_node: "".to_string(), node_info: "".to_string(),
udp_packets: 0, udp_dropped: 0,
forcing_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,
},
}
}
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 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 { if path.is_empty() { 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") {
if ui.button("Trace").clicked() {
let _ = self.tx_cmd.send(format!("TRACE {} 1", current_path));
let _ = self.internal_tx.send(InternalEvent::TraceRequested(current_path.clone()));
}
if ui.button("⚡ Force").clicked() {
self.forcing_dialog = Some(ForcingDialog { signal_path: current_path.clone(), value: "".to_string() });
}
}
});
}
}
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; }
// Search for edge crossing in the last 100 samples
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];
// Avoid re-triggering on the same exact sample
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 tcp_command_worker(shared_config: Arc<Mutex<ConnectionConfig>>, rx_cmd: Receiver<String>, tx_events: Sender<InternalEvent>) {
let mut current_version = 0;
let mut current_addr = String::new();
loop {
{ let config = shared_config.lock().unwrap(); if config.version != current_version { current_version = config.version; current_addr = format!("{}:{}", config.ip, config.tcp_port); } }
if current_addr.is_empty() || current_addr.starts_with(":") { thread::sleep(std::time::Duration::from_secs(1)); continue; }
if let Ok(mut stream) = TcpStream::connect(&current_addr) {
let _ = stream.set_nodelay(true);
let mut reader = BufReader::new(stream.try_clone().unwrap());
let _ = tx_events.send(InternalEvent::Connected);
let stop_flag = Arc::new(Mutex::new(false));
let stop_flag_reader = stop_flag.clone();
let tx_events_inner = tx_events.clone();
thread::spawn(move || {
let mut line = String::new();
let mut json_acc = String::new();
let mut in_json = false;
while reader.read_line(&mut line).is_ok() {
if *stop_flag_reader.lock().unwrap() { break; }
let trimmed = line.trim();
if trimmed.is_empty() { line.clear(); continue; }
if !in_json && trimmed.starts_with("{") { in_json = true; json_acc.clear(); }
if in_json {
json_acc.push_str(trimmed);
if trimmed == "OK DISCOVER" { in_json = false; let json_clean = json_acc.trim_end_matches("OK DISCOVER").trim(); if let Ok(resp) = serde_json::from_str::<DiscoverResponse>(json_clean) { let _ = tx_events_inner.send(InternalEvent::Discovery(resp.signals)); } json_acc.clear(); }
else if trimmed == "OK TREE" { in_json = false; let json_clean = json_acc.trim_end_matches("OK TREE").trim(); if let Ok(resp) = serde_json::from_str::<TreeItem>(json_clean) { let _ = tx_events_inner.send(InternalEvent::Tree(resp)); } json_acc.clear(); }
else if trimmed == "OK INFO" { in_json = false; let json_clean = json_acc.trim_end_matches("OK INFO").trim(); let _ = tx_events_inner.send(InternalEvent::NodeInfo(json_clean.to_string())); json_acc.clear(); }
} else { let _ = tx_events_inner.send(InternalEvent::CommandResponse(trimmed.to_string())); }
line.clear();
}
});
while let Ok(cmd) = rx_cmd.recv() {
{ let config = shared_config.lock().unwrap(); if config.version != current_version { *stop_flag.lock().unwrap() = true; break; } }
if stream.write_all(format!("{}\n", cmd).as_bytes()).is_err() { break; }
}
let _ = tx_events.send(InternalEvent::Disconnected);
}
thread::sleep(std::time::Duration::from_secs(2));
}
}
fn tcp_log_worker(shared_config: Arc<Mutex<ConnectionConfig>>, tx_events: Sender<InternalEvent>) {
let mut current_version = 0;
let mut current_addr = String::new();
loop {
{ let config = shared_config.lock().unwrap(); if config.version != current_version { current_version = config.version; current_addr = format!("{}:{}", config.ip, config.log_port); } }
if current_addr.is_empty() || current_addr.starts_with(":") { thread::sleep(std::time::Duration::from_secs(1)); continue; }
if let Ok(stream) = TcpStream::connect(&current_addr) {
let mut reader = BufReader::new(stream);
let mut line = String::new();
while reader.read_line(&mut line).is_ok() {
if shared_config.lock().unwrap().version != current_version { break; }
let trimmed = line.trim();
if trimmed.starts_with("LOG ") {
let parts: Vec<&str> = trimmed[4..].splitn(2, ' ').collect();
if parts.len() == 2 { let _ = tx_events.send(InternalEvent::Log(LogEntry { time: Local::now().format("%H:%M:%S%.3f").to_string(), level: parts[0].to_string(), message: parts[1].to_string() })); }
}
line.clear();
}
}
thread::sleep(std::time::Duration::from_secs(2));
}
}
fn udp_worker(shared_config: Arc<Mutex<ConnectionConfig>>, id_to_meta: Arc<Mutex<HashMap<u32, SignalMetadata>>>, traced_data: Arc<Mutex<HashMap<String, TraceData>>>, tx_events: Sender<InternalEvent>) {
let mut current_version = 0;
let mut socket: Option<UdpSocket> = None;
let mut last_seq: Option<u32> = None;
loop {
let (ver, port) = { let config = shared_config.lock().unwrap(); (config.version, config.udp_port.clone()) };
if ver != current_version || socket.is_none() {
current_version = ver;
if port.is_empty() { socket = None; continue; }
let port_num: u16 = port.parse().unwrap_or(8081);
let s = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).ok();
let mut bound = false;
if let Some(sock) = s {
let _ = sock.set_reuse_address(true);
#[cfg(all(unix, not(target_os = "solaris"), not(target_os = "illumos")))]
let _ = sock.set_reuse_port(true);
let _ = sock.set_recv_buffer_size(10 * 1024 * 1024);
let addr = format!("0.0.0.0:{}", port_num).parse::<std::net::SocketAddr>().unwrap();
if sock.bind(&addr.into()).is_ok() { socket = Some(sock.into()); bound = true; }
}
if !bound { thread::sleep(std::time::Duration::from_secs(5)); continue; }
let _ = socket.as_ref().unwrap().set_read_timeout(Some(std::time::Duration::from_millis(500)));
last_seq = None;
}
let s = if let Some(sock) = socket.as_ref() { sock } else { thread::sleep(std::time::Duration::from_secs(1)); continue; };
let mut buf = [0u8; 4096];
let mut total_packets = 0u64;
loop {
if shared_config.lock().unwrap().version != current_version { break; }
if let Ok(n) = s.recv(&mut buf) {
total_packets += 1;
if (total_packets % 500) == 0 { let _ = tx_events.send(InternalEvent::UdpStats(total_packets)); }
if n < 20 { continue; }
let mut magic_buf = [0u8; 4]; magic_buf.copy_from_slice(&buf[0..4]);
if u32::from_le_bytes(magic_buf) != 0xDA7A57AD { continue; }
let mut seq_buf = [0u8; 4]; seq_buf.copy_from_slice(&buf[4..8]);
let seq = u32::from_le_bytes(seq_buf);
if let Some(last) = last_seq { if seq != last + 1 && seq > last { let _ = tx_events.send(InternalEvent::UdpDropped(seq - last - 1)); } }
last_seq = Some(seq);
let count = u32::from_le_bytes(buf[16..20].try_into().unwrap());
let now = APP_START_TIME.elapsed().as_secs_f64();
let mut offset = 20;
let mut local_updates: HashMap<String, Vec<[f64; 2]>> = HashMap::new();
let mut last_values: HashMap<String, f64> = HashMap::new();
let metas = id_to_meta.lock().unwrap();
for _ in 0..count {
if offset + 8 > n { break; }
let id = u32::from_le_bytes(buf[offset..offset+4].try_into().unwrap());
let size = u32::from_le_bytes(buf[offset+4..offset+8].try_into().unwrap());
offset += 8;
if offset + size as usize > n { break; }
let data_slice = &buf[offset..offset + size as usize];
if let Some(meta) = metas.get(&id) {
let t = meta.sig_type.as_str();
let val = match size {
1 => { if t.contains('u') { data_slice[0] as f64 } else { (data_slice[0] as i8) as f64 } },
2 => { let b = data_slice[0..2].try_into().unwrap(); if t.contains('u') { u16::from_le_bytes(b) as f64 } else { i16::from_le_bytes(b) as f64 } },
4 => { let b = data_slice[0..4].try_into().unwrap(); if t.contains("float") { f32::from_le_bytes(b) as f64 } else if t.contains('u') { u32::from_le_bytes(b) as f64 } else { i32::from_le_bytes(b) as f64 } },
8 => { let b = data_slice[0..8].try_into().unwrap(); if t.contains("float") { f64::from_le_bytes(b) } else if t.contains('u') { u64::from_le_bytes(b) as f64 } else { i64::from_le_bytes(b) as f64 } },
_ => 0.0,
};
for name in &meta.names { local_updates.entry(name.clone()).or_default().push([now, val]); last_values.insert(name.clone(), val); }
}
offset += size as usize;
}
drop(metas);
if !local_updates.is_empty() {
let mut data_map = traced_data.lock().unwrap();
for (name, new_points) in local_updates {
if let Some(entry) = data_map.get_mut(&name) {
for point in new_points { entry.values.push_back(point); }
if let Some(lv) = last_values.get(&name) { entry.last_value = *lv; }
while entry.values.len() > 100000 { entry.values.pop_front(); }
}
}
}
}
}
}
}
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::Discovery(signals) => { let mut metas = self.id_to_meta.lock().unwrap(); metas.clear(); for s in &signals { let meta = metas.entry(s.id).or_insert_with(|| SignalMetadata { names: Vec::new(), sig_type: s.sig_type.clone() }); if !meta.names.contains(&s.name) { meta.names.push(s.name.clone()); } } }
InternalEvent::Tree(tree) => { self.app_tree = Some(tree); }
InternalEvent::NodeInfo(info) => { self.node_info = info; }
InternalEvent::TraceRequested(name) => { let mut data_map = self.traced_signals.lock().unwrap(); data_map.entry(name).or_insert_with(|| TraceData { values: VecDeque::with_capacity(10000), last_value: 0.0 }); }
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::UdpStats(count) => { self.udp_packets = count; }
InternalEvent::UdpDropped(dropped) => { self.udp_dropped += dropped as u64; }
InternalEvent::Connected => { self.connected = true; let _ = self.tx_cmd.send("TREE".to_string()); let _ = self.tx_cmd.send("DISCOVER".to_string()); }
InternalEvent::Disconnected => { self.connected = false; }
InternalEvent::InternalLog(msg) => { self.logs.push_back(LogEntry { time: Local::now().format("%H:%M:%S").to_string(), level: "GUI_ERROR".to_string(), message: msg }); }
InternalEvent::CommandResponse(resp) => { self.logs.push_back(LogEntry { time: Local::now().format("%H:%M:%S").to_string(), level: "CMD_RESP".to_string(), message: resp }); }
}
}
if self.scope.enabled { self.apply_trigger_logic(); }
if let Some(dragged_name) = ctx.data_mut(|d| d.get_temp::<String>(egui::Id::new("drag_signal"))) {
egui::Area::new(egui::Id::new("drag_ghost")).fixed_pos(ctx.input(|i| i.pointer.hover_pos().unwrap_or(egui::Pos2::ZERO))).order(egui::Order::Tooltip).show(ctx, |ui| { ui.group(|ui| { ui.label(format!("📈 {}", dragged_name)); }); });
}
if let Some(dialog) = &mut self.forcing_dialog {
let mut close = false;
egui::Window::new("Force Signal").show(ctx, |ui| { ui.label(&dialog.signal_path); ui.text_edit_singleline(&mut dialog.value); ui.horizontal(|ui| { if ui.button("Apply").clicked() { let _ = self.tx_cmd.send(format!("FORCE {} {}", dialog.signal_path, dialog.value)); self.forced_signals.insert(dialog.signal_path.clone(), dialog.value.clone()); close = true; } if ui.button("Cancel").clicked() { close = true; } }); });
if close { self.forcing_dialog = None; }
}
if let Some((p_idx, s_idx)) = self.style_editor {
let mut close = false;
egui::Window::new("Signal Style").show(ctx, |ui| {
if let Some(plot) = self.plots.get_mut(p_idx) {
if let Some(sig) = plot.signals.get_mut(s_idx) {
ui.horizontal(|ui| { ui.label("Label:"); ui.text_edit_singleline(&mut sig.label); });
ui.horizontal(|ui| { ui.label("Unit:"); ui.text_edit_singleline(&mut sig.unit); });
ui.horizontal(|ui| { ui.label("Color:"); let mut color = sig.color.to_array(); if ui.color_edit_button_srgba_unmultiplied(&mut color).changed() { sig.color = egui::Color32::from_rgba_unmultiplied(color[0], color[1], color[2], color[3]); } });
ui.horizontal(|ui| { ui.label("Gain:"); ui.add(egui::DragValue::new(&mut sig.gain).speed(0.1)); });
ui.horizontal(|ui| { ui.label("Offset:"); ui.add(egui::DragValue::new(&mut sig.offset).speed(1.0)); });
if ui.button("Close").clicked() { close = true; }
}
}
});
if close { self.style_editor = None; }
}
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, "📜 Logs");
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();
ui.checkbox(&mut self.scope.enabled, "🔭 Scope");
if self.scope.enabled {
egui::ComboBox::from_id_salt("window_size").selected_text(format!("{}ms", self.scope.window_ms)).show_ui(ui, |ui| {
for ms in [10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 2000.0, 5000.0, 10000.0] { ui.selectable_value(&mut self.scope.window_ms, ms, format!("{}ms", ms)); }
});
ui.selectable_value(&mut self.scope.mode, AcquisitionMode::FreeRun, "Free");
ui.selectable_value(&mut self.scope.mode, AcquisitionMode::Triggered, "Trig");
if self.scope.mode == AcquisitionMode::FreeRun {
if ui.button(if self.scope.paused { "▶ Resume" } else { "⏸ Pause" }).clicked() { self.scope.paused = !self.scope.paused; }
} else {
if ui.button(if self.scope.is_armed { "🔴 Armed" } else { "⚪ Single" }).clicked() { self.scope.is_armed = true; self.scope.trigger_active = false; }
ui.menu_button("⚙ Trigger", |ui| {
egui::Grid::new("trig_grid").num_columns(2).show(ui, |ui| {
ui.label("Source:"); ui.text_edit_singleline(&mut self.scope.trigger_source); ui.end_row();
ui.label("Edge:"); egui::ComboBox::from_id_salt("edge").selected_text(format!("{:?}", self.scope.trigger_edge)).show_ui(ui, |ui| {
ui.selectable_value(&mut self.scope.trigger_edge, TriggerEdge::Rising, "Rising");
ui.selectable_value(&mut self.scope.trigger_edge, TriggerEdge::Falling, "Falling");
ui.selectable_value(&mut self.scope.trigger_edge, TriggerEdge::Both, "Both");
}); ui.end_row();
ui.label("Threshold:"); ui.add(egui::DragValue::new(&mut self.scope.trigger_threshold).speed(0.1)); ui.end_row();
ui.label("Pre-trig %:"); ui.add(egui::Slider::new(&mut self.scope.pre_trigger_percent, 0.0..=100.0)); ui.end_row();
ui.label("Mode:"); ui.selectable_value(&mut self.scope.trigger_type, TriggerType::Single, "Single"); ui.selectable_value(&mut self.scope.trigger_type, TriggerType::Continuous, "Cont"); ui.end_row();
});
});
}
}
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.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(); }
if ui.button("❌ Off").clicked() { self.config.version += 1; let mut cfg = self.config.clone(); cfg.ip = "".to_string(); *self.shared_config.lock().unwrap() = cfg; ui.close_menu(); }
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.label(format!("UDP: OK[{}] DROP[{}]", self.udp_packets, self.udp_dropped)); });
});
});
if self.show_left_panel { egui::SidePanel::left("left").resizable(true).width_range(200.0..=500.0).show(ctx, |ui| { egui::ScrollArea::vertical().show(ui, |ui| { if let Some(tree) = self.app_tree.clone() { self.render_tree(ui, &tree, "".to_string()); } }); }); }
if self.show_right_panel {
egui::SidePanel::right("right").resizable(true).width_range(250.0..=400.0).show(ctx, |ui| {
ui.heading("Traced Signals");
let mut names: Vec<_> = { let data_map = self.traced_signals.lock().unwrap(); data_map.keys().cloned().collect() };
names.sort();
egui::ScrollArea::vertical().id_salt("traced_scroll").show(ui, |ui| {
for key in names {
let last_val = { self.traced_signals.lock().unwrap().get(&key).map(|d| d.last_value).unwrap_or(0.0) };
ui.horizontal(|ui| {
let response = ui.add(egui::Label::new(format!("{}: {:.2}", key, last_val)).sense(egui::Sense::drag()));
if response.drag_started() { ctx.data_mut(|d| d.insert_temp(egui::Id::new("drag_signal"), key.clone())); }
if ui.button("").clicked() { let _ = self.tx_cmd.send(format!("TRACE {} 0", key)); let _ = self.internal_tx.send(InternalEvent::ClearTrace(key)); }
});
}
});
ui.separator();
ui.heading("Forced Signals");
for (path, val) in &self.forced_signals { ui.horizontal(|ui| { ui.label(format!("{}: {}", path, val)); if ui.button("").clicked() { let _ = self.tx_cmd.send(format!("UNFORCE {}", path)); } }); }
});
}
if self.show_bottom_panel {
egui::TopBottomPanel::bottom("log_panel").resizable(true).default_height(150.0).show(ctx, |ui| {
ui.horizontal(|ui| {
ui.heading("Logs"); ui.separator();
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, "Warn"); ui.checkbox(&mut self.log_filters.show_error, "Error");
ui.separator();
ui.label("Filter:"); ui.text_edit_singleline(&mut self.log_filters.content_regex);
if ui.button("🗑 Clear").clicked() { self.logs.clear(); }
});
ui.separator();
let regex = if !self.log_filters.content_regex.is_empty() { Regex::new(&self.log_filters.content_regex).ok() } else { None };
egui::ScrollArea::vertical().stick_to_bottom(true).auto_shrink([false, false]).show(ui, |ui| {
for log in &self.logs {
let show = match log.level.as_str() { "Debug" => self.log_filters.show_debug, "Information" => self.log_filters.show_info, "Warning" => self.log_filters.show_warning, "FatalError" | "OSError" | "ParametersError" => 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; } }
let color = match log.level.as_str() { "FatalError" | "OSError" | "ParametersError" => egui::Color32::from_rgb(255, 100, 100), "Warning" => egui::Color32::from_rgb(255, 255, 100), "Information" => egui::Color32::from_rgb(100, 255, 100), "Debug" => egui::Color32::from_rgb(100, 100, 255), "GUI_ERROR" => egui::Color32::from_rgb(255, 50, 255), "CMD_RESP" => egui::Color32::from_rgb(255, 255, 255), _ => egui::Color32::WHITE };
ui.horizontal_wrapped(|ui| { ui.label(egui::RichText::new(&log.time).color(egui::Color32::GRAY).monospace()); ui.label(egui::RichText::new(format!("[{}]", log.level)).color(color).strong()); ui.add(egui::Label::new(&log.message).wrap()); });
}
});
});
}
egui::CentralPanel::default().show(ctx, |ui| {
let n_plots = self.plots.len();
if n_plots > 0 {
let plot_height = ui.available_height() / n_plots as f32;
let mut to_remove = None;
let mut current_range = 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());
ui.selectable_value(&mut plot_inst.plot_type, PlotType::Normal, "Series");
ui.selectable_value(&mut plot_inst.plot_type, PlotType::LogicAnalyzer, "Logic");
if ui.button("🗑").clicked() { to_remove = Some(p_idx); }
});
let mut plot = Plot::new(&plot_inst.id).height(plot_height - 40.0).show_axes([true, true]);
// SCOPE SYNC LOGIC
if self.scope.enabled {
let window_s = self.scope.window_ms / 1000.0;
let center_t = if self.scope.mode == AcquisitionMode::Triggered {
if self.scope.trigger_active { self.scope.last_trigger_time } else { APP_START_TIME.elapsed().as_secs_f64() }
} else {
APP_START_TIME.elapsed().as_secs_f64()
};
let pre_trigger_s = (self.scope.pre_trigger_percent / 100.0) * window_s;
let x_min = center_t - pre_trigger_s;
let x_max = x_min + window_s;
plot = plot.include_x(x_min).include_x(x_max);
if !self.scope.paused {
plot = plot.auto_bounds(egui::Vec2b::new(true, true));
}
} else {
if let Some(range) = self.shared_x_range { if !plot_inst.auto_bounds { plot = plot.include_x(range[0]).include_x(range[1]); } }
if plot_inst.auto_bounds { plot = plot.auto_bounds(egui::Vec2b::new(true, true)); }
}
let plot_resp = plot.show(ui, |plot_ui| {
if !self.scope.enabled && !plot_inst.auto_bounds {
if let Some(range) = self.shared_x_range {
let bounds = plot_ui.plot_bounds();
plot_ui.set_plot_bounds(PlotBounds::from_min_max([range[0], bounds.min()[1]], [range[1], bounds.max()[1]]));
}
}
// Trigger Line
if self.scope.enabled && self.scope.mode == AcquisitionMode::Triggered && self.scope.trigger_active {
plot_ui.vline(VLine::new(self.scope.last_trigger_time).color(egui::Color32::YELLOW).style(LineStyle::Dashed { length: 5.0 }));
}
let data_map = self.traced_signals.lock().unwrap();
for (s_idx, sig_cfg) in plot_inst.signals.iter().enumerate() {
if let Some(data) = data_map.get(&sig_cfg.source_name) {
let mut points_vec = Vec::new();
for [t, v] in &data.values {
let mut final_v = *v * sig_cfg.gain + sig_cfg.offset;
if plot_inst.plot_type == PlotType::LogicAnalyzer { final_v = (s_idx as f64 * 1.5) + (if final_v > 0.5 { 1.0 } else { 0.0 }); }
points_vec.push([*t, final_v]);
}
plot_ui.line(Line::new(PlotPoints::from(points_vec)).name(&sig_cfg.label).color(sig_cfg.color));
}
}
if p_idx == 0 || current_range.is_none() { let b = plot_ui.plot_bounds(); current_range = Some([b.min()[0], b.max()[0]]); }
});
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: 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 plot_resp.response.dragged() || ctx.input(|i| i.pointer.any_click() || i.smooth_scroll_delta.y != 0.0) {
if plot_resp.response.hovered() {
plot_inst.auto_bounds = false;
let b = plot_resp.transform.bounds();
self.shared_x_range = Some([b.min()[0], b.max()[0]]);
}
}
plot_resp.response.context_menu(|ui| {
if ui.button("🔍 Fit View").clicked() { plot_inst.auto_bounds = true; self.shared_x_range = None; ui.close_menu(); }
ui.separator();
let mut sig_to_remove = None;
for (s_idx, sig) in plot_inst.signals.iter().enumerate() {
ui.horizontal(|ui| { ui.label(&sig.label); if ui.button("🎨").clicked() { self.style_editor = Some((p_idx, s_idx)); ui.close_menu(); } if ui.button("").clicked() { sig_to_remove = Some(s_idx); ui.close_menu(); } });
}
if let Some(idx) = sig_to_remove { plot_inst.signals.remove(idx); }
});
});
}
if let Some(idx) = to_remove { self.plots.remove(idx); }
if !self.scope.enabled { if let Some(range) = current_range { if self.shared_x_range.is_none() { self.shared_x_range = Some(range); } } }
} else { ui.centered_and_justified(|ui| { ui.label("Add a plot panel to begin analysis"); }); }
});
ctx.request_repaint_after(std::time::Duration::from_millis(16));
}
}
fn main() -> Result<(), eframe::Error> {
let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default().with_inner_size([1280.0, 800.0]), ..Default::default() };
eframe::run_native("MARTe2 Debug Explorer", options, Box::new(|cc| Ok(Box::new(MarteDebugApp::new(cc)))))
}