diff --git a/SPECS.md b/SPECS.md index 9d0426d..e73ea10 100644 --- a/SPECS.md +++ b/SPECS.md @@ -9,8 +9,25 @@ Implement a "Zero-Code-Change" observability layer for the MARTe2 real-time fram - **FR-02 (Telemetry):** Stream high-frequency signal data (verified up to 100Hz) to a remote client. - **FR-03 (Forcing):** Allow manual override of signal values in memory during execution. - **FR-04 (Logs):** Stream global framework logs to a dedicated terminal via a standalone `TcpLogger` service. -- **FR-05 (Execution Control):** Pause and resume the real-time execution threads via scheduler injection. -- **FR-06 (UI):** Provide a native, immediate-mode GUI for visualization (Oscilloscope). +- **FR-05 (Log Filtering):** The client must support filtering logs by type (Debug, Information, Warning, FatalError) and by content using regular expressions. +- **FR-06 (Execution & UI):** + - Provide a native GUI for visualization. + - Support Pause/Resume of real-time execution threads via scheduler injection. +- **FR-07 (Session Management):** + - The top panel must provide a "Disconnect" button to close active network streams. + - Support runtime re-configuration and "Apply & Reconnect" logic. +- **FR-08 (Decoupled Tracing):** + Clicking `trace` activates telemetry; data is buffered and shown as a "Last Value" in the sidebar, but not plotted until manually assigned. +- **FR-08 (Advanced Plotting):** + - Support multiple plot panels with perfectly synchronized time (X) axes. + - Drag-and-drop signals from the traced list into specific plots. + - Automatic distinct color assignment for each signal added to a plot. + - Plot modes: Standard (Time Series) and Logic Analyzer (Stacked rows). + - Signal transformations: Gain, offset, units, and custom labels. + - Visual styling: Deep customization of colors, line styles (Solid, Dashed, etc.), and marker shapes (Circle, Square, etc.). +- **FR-09 (Navigation):** + - Context menus for resetting zoom (X, Y, or both). + - "Fit to View" functionality that automatically scales both axes to encompass all available buffered data points. ### 2.2 Technical Constraints (TC) - **TC-01:** No modifications allowed to the MARTe2 core library or component source code. diff --git a/Source/DebugService.cpp b/Source/DebugService.cpp index 512defc..e77a541 100644 --- a/Source/DebugService.cpp +++ b/Source/DebugService.cpp @@ -1,6 +1,7 @@ #include "DebugService.h" -#include "AdvancedErrorManagement.h" +#include "StandardParser.h" #include "StreamString.h" +#include "BasicSocket.h" #include "DebugBrokerWrapper.h" #include "ObjectRegistryDatabase.h" #include "ClassRegistryItem.h" @@ -11,6 +12,15 @@ #include "GAM.h" // Explicitly include target brokers for templating +#include "MemoryMapInputBroker.h" +#include "MemoryMapOutputBroker.h" +#include "MemoryMapSynchronisedInputBroker.h" +#include "MemoryMapSynchronisedOutputBroker.h" +#include "MemoryMapInterpolatedInputBroker.h" +#include "MemoryMapMultiBufferInputBroker.h" +#include "MemoryMapMultiBufferOutputBroker.h" +#include "MemoryMapSynchronisedMultiBufferInputBroker.h" +#include "MemoryMapSynchronisedMultiBufferOutputBroker.h" namespace MARTe { @@ -101,6 +111,7 @@ bool DebugService::Initialise(StructuredDataI & data) { } if (isServer) { + // 8MB Buffer for lossless tracing at high frequency if (!traceBuffer.Init(8 * 1024 * 1024)) return false; PatchRegistry(); @@ -167,7 +178,6 @@ void DebugService::PatchRegistry() { PatchItemInternal("MemoryMapSynchronisedMultiBufferInputBroker", &b8); static DebugMemoryMapSynchronisedMultiBufferOutputBrokerBuilder b9; PatchItemInternal("MemoryMapSynchronisedMultiBufferOutputBroker", &b9); - } void DebugService::ProcessSignal(DebugSignalInfo* s, uint32 size) { @@ -337,7 +347,7 @@ ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo & info) { } InternetHost dest(streamPort, streamIP.Buffer()); - udpSocket.SetDestination(dest); + (void)udpSocket.SetDestination(dest); uint8 packetBuffer[4096]; uint32 packetOffset = 0; @@ -349,6 +359,7 @@ ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo & info) { uint8 sampleData[1024]; bool hasData = false; + // TIGHT LOOP: Drain the buffer as fast as possible without sleeping while ((info.GetStage() == ExecutionInfo::MainStage) && traceBuffer.Pop(id, sampleData, size, 1024)) { hasData = true; if (packetOffset == 0) { @@ -357,36 +368,44 @@ ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo & info) { header.seq = sequenceNumber++; header.timestamp = HighResolutionTimer::Counter(); header.count = 0; - MemoryOperationsHelper::Copy(packetBuffer, &header, sizeof(TraceHeader)); + std::memcpy(packetBuffer, &header, sizeof(TraceHeader)); packetOffset = sizeof(TraceHeader); } + // Packet Packing: Header + [ID:4][Size:4][Data:N] + // If this sample doesn't fit, flush the current packet first if (packetOffset + 8 + size > 1400) { uint32 toWrite = packetOffset; - udpSocket.Write((char8*)packetBuffer, toWrite); - packetOffset = 0; + (void)udpSocket.Write((char8*)packetBuffer, toWrite); + + // Re-init header for the next packet TraceHeader header; header.magic = 0xDA7A57AD; header.seq = sequenceNumber++; header.timestamp = HighResolutionTimer::Counter(); header.count = 0; - MemoryOperationsHelper::Copy(packetBuffer, &header, sizeof(TraceHeader)); + std::memcpy(packetBuffer, &header, sizeof(TraceHeader)); packetOffset = sizeof(TraceHeader); } - MemoryOperationsHelper::Copy(&packetBuffer[packetOffset], &id, 4); - MemoryOperationsHelper::Copy(&packetBuffer[packetOffset + 4], &size, 4); - MemoryOperationsHelper::Copy(&packetBuffer[packetOffset + 8], sampleData, size); + std::memcpy(&packetBuffer[packetOffset], &id, 4); + std::memcpy(&packetBuffer[packetOffset + 4], &size, 4); + std::memcpy(&packetBuffer[packetOffset + 8], sampleData, size); packetOffset += (8 + size); + + // Update sample count in the current packet header TraceHeader *h = (TraceHeader*)packetBuffer; h->count++; } + // Flush any remaining data if (packetOffset > 0) { uint32 toWrite = packetOffset; - udpSocket.Write((char8*)packetBuffer, toWrite); + (void)udpSocket.Write((char8*)packetBuffer, toWrite); packetOffset = 0; } + + // Only sleep if the buffer was completely empty if (!hasData) Sleep::MSec(1); } return ErrorManagement::NoError; @@ -415,7 +434,7 @@ void DebugService::HandleCommand(StreamString cmd, BasicTCPSocket *client) { uint32 count = ForceSignal(name.Buffer(), val.Buffer()); if (client) { StreamString resp; resp.Printf("OK FORCE %u\n", count); - uint32 s = resp.Size(); client->Write(resp.Buffer(), s); + uint32 s = resp.Size(); (void)client->Write(resp.Buffer(), s); } } } @@ -425,7 +444,7 @@ void DebugService::HandleCommand(StreamString cmd, BasicTCPSocket *client) { uint32 count = UnforceSignal(name.Buffer()); if (client) { StreamString resp; resp.Printf("OK UNFORCE %u\n", count); - uint32 s = resp.Size(); client->Write(resp.Buffer(), s); + uint32 s = resp.Size(); (void)client->Write(resp.Buffer(), s); } } } @@ -437,31 +456,31 @@ void DebugService::HandleCommand(StreamString cmd, BasicTCPSocket *client) { if (cmd.GetToken(decim, delims, term)) { AnyType decimVal(UnsignedInteger32Bit, 0u, &d); AnyType decimStr(CharString, 0u, decim.Buffer()); - TypeConvert(decimVal, decimStr); + (void)TypeConvert(decimVal, decimStr); } uint32 count = TraceSignal(name.Buffer(), enable, d); if (client) { StreamString resp; resp.Printf("OK TRACE %u\n", count); - uint32 s = resp.Size(); client->Write(resp.Buffer(), s); + uint32 s = resp.Size(); (void)client->Write(resp.Buffer(), s); } } } else if (token == "DISCOVER") Discover(client); else if (token == "PAUSE") { SetPaused(true); - if (client) { uint32 s = 3; client->Write("OK\n", s); } + if (client) { uint32 s = 3; (void)client->Write("OK\n", s); } } else if (token == "RESUME") { SetPaused(false); - if (client) { uint32 s = 3; client->Write("OK\n", s); } + if (client) { uint32 s = 3; (void)client->Write("OK\n", s); } } else if (token == "TREE") { StreamString json; json = "{\"Name\": \"Root\", \"Class\": \"ObjectRegistryDatabase\", \"Children\": [\n"; - ExportTree(ObjectRegistryDatabase::Instance(), json); + (void)ExportTree(ObjectRegistryDatabase::Instance(), json); json += "\n]}\nOK TREE\n"; uint32 s = json.Size(); - client->Write(json.Buffer(), s); + (void)client->Write(json.Buffer(), s); } else if (token == "INFO") { StreamString path; @@ -475,7 +494,7 @@ void DebugService::HandleCommand(StreamString cmd, BasicTCPSocket *client) { else if (client) { const char* msg = "ERROR: Unknown command\n"; uint32 s = StringHelper::Length(msg); - client->Write(msg, s); + (void)client->Write(msg, s); } } } @@ -527,7 +546,7 @@ void DebugService::InfoNode(const char8* path, BasicTCPSocket *client) { json += "}\nOK INFO\n"; uint32 s = json.Size(); - client->Write(json.Buffer(), s); + (void)client->Write(json.Buffer(), s); } uint32 DebugService::ExportTree(ReferenceContainer *container, StreamString &json) { @@ -661,7 +680,7 @@ void DebugService::Discover(BasicTCPSocket *client) { if (client) { StreamString header = "{\n \"Signals\": [\n"; uint32 s = header.Size(); - client->Write(header.Buffer(), s); + (void)client->Write(header.Buffer(), s); mutex.FastLock(); for (uint32 i = 0; i < numberOfAliases; i++) { StreamString line; @@ -672,12 +691,12 @@ void DebugService::Discover(BasicTCPSocket *client) { if (i < numberOfAliases - 1) line += ","; line += "\n"; s = line.Size(); - client->Write(line.Buffer(), s); + (void)client->Write(line.Buffer(), s); } mutex.FastUnLock(); StreamString footer = " ]\n}\nOK DISCOVER\n"; s = footer.Size(); - client->Write(footer.Buffer(), s); + (void)client->Write(footer.Buffer(), s); } } @@ -694,7 +713,7 @@ void DebugService::ListNodes(const char8* path, BasicTCPSocket *client) { StreamString header; header.Printf("Nodes under %s:\n", path ? path : "/"); uint32 s = header.Size(); - client->Write(header.Buffer(), s); + (void)client->Write(header.Buffer(), s); ReferenceContainer *container = dynamic_cast(ref.operator->()); if (container) { @@ -705,7 +724,7 @@ void DebugService::ListNodes(const char8* path, BasicTCPSocket *client) { StreamString line; line.Printf(" %s [%s]\n", child->GetName(), child->GetClassProperties()->GetName()); s = line.Size(); - client->Write(line.Buffer(), s); + (void)client->Write(line.Buffer(), s); } } } @@ -713,15 +732,15 @@ void DebugService::ListNodes(const char8* path, BasicTCPSocket *client) { DataSourceI *ds = dynamic_cast(ref.operator->()); if (ds) { StreamString dsHeader = " Signals:\n"; - s = dsHeader.Size(); client->Write(dsHeader.Buffer(), s); + s = dsHeader.Size(); (void)client->Write(dsHeader.Buffer(), s); uint32 nSignals = ds->GetNumberOfSignals(); for (uint32 i=0; iGetSignalName(i, sname); + (void)ds->GetSignalName(i, sname); TypeDescriptor stype = ds->GetSignalType(i); const char8* stypeName = TypeDescriptor::GetTypeNameFromTypeDescriptor(stype); line.Printf(" %s [%s]\n", sname.Buffer(), stypeName ? stypeName : "Unknown"); - s = line.Size(); client->Write(line.Buffer(), s); + s = line.Size(); (void)client->Write(line.Buffer(), s); } } @@ -731,31 +750,31 @@ void DebugService::ListNodes(const char8* path, BasicTCPSocket *client) { uint32 nOut = gam->GetNumberOfOutputSignals(); StreamString gamHeader; gamHeader.Printf(" Input Signals (%d):\n", nIn); - s = gamHeader.Size(); client->Write(gamHeader.Buffer(), s); + s = gamHeader.Size(); (void)client->Write(gamHeader.Buffer(), s); for (uint32 i=0; iGetSignalName(InputSignals, i, sname); + (void)gam->GetSignalName(InputSignals, i, sname); line.Printf(" %s\n", sname.Buffer()); - s = line.Size(); client->Write(line.Buffer(), s); + s = line.Size(); (void)client->Write(line.Buffer(), s); } gamHeader.SetSize(0); gamHeader.Printf(" Output Signals (%d):\n", nOut); - s = gamHeader.Size(); client->Write(gamHeader.Buffer(), s); + s = gamHeader.Size(); (void)client->Write(gamHeader.Buffer(), s); for (uint32 i=0; iGetSignalName(OutputSignals, i, sname); + (void)gam->GetSignalName(OutputSignals, i, sname); line.Printf(" %s\n", sname.Buffer()); - s = line.Size(); client->Write(line.Buffer(), s); + s = line.Size(); (void)client->Write(line.Buffer(), s); } } const char* okMsg = "OK LS\n"; s = StringHelper::Length(okMsg); - client->Write(okMsg, s); + (void)client->Write(okMsg, s); } else { const char* msg = "ERROR: Path not found\n"; uint32 s = StringHelper::Length(msg); - client->Write(msg, s); + (void)client->Write(msg, s); } } diff --git a/Tools/gui_client/Cargo.lock b/Tools/gui_client/Cargo.lock index 08f2483..9c65660 100644 --- a/Tools/gui_client/Cargo.lock +++ b/Tools/gui_client/Cargo.lock @@ -1796,6 +1796,7 @@ dependencies = [ "crossbeam-channel", "eframe", "egui_plot", + "once_cell", "regex", "serde", "serde_json", diff --git a/Tools/gui_client/Cargo.toml b/Tools/gui_client/Cargo.toml index 1ff5c43..882622f 100644 --- a/Tools/gui_client/Cargo.toml +++ b/Tools/gui_client/Cargo.toml @@ -12,3 +12,4 @@ chrono = "0.4" crossbeam-channel = "0.5" regex = "1.10" socket2 = { version = "0.5", features = ["all"] } +once_cell = "1.21.3" diff --git a/Tools/gui_client/src/main.rs b/Tools/gui_client/src/main.rs index 7de455b..01a062d 100644 --- a/Tools/gui_client/src/main.rs +++ b/Tools/gui_client/src/main.rs @@ -1,5 +1,5 @@ use eframe::egui; -use egui_plot::{Line, Plot, PlotPoints}; +use egui_plot::{Line, Plot, PlotPoints, MarkerShape, LineStyle, PlotBounds}; use std::collections::{HashMap, VecDeque}; use std::net::{TcpStream, UdpSocket}; use std::io::{Write, BufReader, BufRead}; @@ -8,8 +8,11 @@ use std::thread; use serde::{Deserialize, Serialize}; use chrono::Local; use crossbeam_channel::{unbounded, Receiver, Sender}; -use regex::Regex; use socket2::{Socket, Domain, Type, Protocol}; +use regex::Regex; +use once_cell::sync::Lazy; + +static APP_START_TIME: Lazy = Lazy::new(std::time::Instant::now); // --- Models --- @@ -52,6 +55,7 @@ struct LogEntry { struct TraceData { values: VecDeque<[f64; 2]>, + last_value: f64, } struct SignalMetadata { @@ -68,6 +72,48 @@ struct ConnectionConfig { version: u64, } +#[derive(Clone, Copy, PartialEq, Debug)] +enum PlotType { + Normal, + LogicAnalyzer, +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum MarkerType { + None, + Circle, + Square, +} + +impl MarkerType { + fn to_shape(&self) -> Option { + 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, + auto_bounds: bool, +} + enum InternalEvent { Log(LogEntry), Discovery(Vec), @@ -87,53 +133,43 @@ enum InternalEvent { struct ForcingDialog { signal_path: String, - sig_type: String, - dims: u8, - elems: u32, value: String, } struct LogFilters { - content_regex: String, show_debug: bool, show_info: bool, show_warning: bool, show_error: bool, paused: bool, + content_regex: String, } struct MarteDebugApp { connected: bool, + is_breaking: bool, config: ConnectionConfig, shared_config: Arc>, - - signals: Vec, app_tree: Option, - - traced_signals: Arc>>, id_to_meta: Arc>>, - + traced_signals: Arc>>, + plots: Vec, forced_signals: HashMap, - is_paused: bool, - logs: VecDeque, 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, - + style_editor: Option<(usize, usize)>, tx_cmd: Sender, rx_events: Receiver, internal_tx: Sender, + shared_x_range: Option<[f64; 2]>, } impl MarteDebugApp { @@ -141,119 +177,62 @@ impl MarteDebugApp { 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 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); - }); - + 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); - }); - + 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); - }); + thread::spawn(move || { udp_worker(shared_config_udp, id_to_meta_clone, traced_signals_clone, tx_events_udp); }); Self { - connected: false, - config, - shared_config, - signals: Vec::new(), - app_tree: None, - id_to_meta, - traced_signals, - forced_signals: HashMap::new(), - is_paused: false, - logs: VecDeque::with_capacity(2000), - log_filters: LogFilters { - content_regex: "".to_string(), - show_debug: true, - show_info: true, - show_warning: true, - show_error: true, - paused: false, - }, - 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, - tx_cmd, - rx_events, - internal_tx, + 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, } } - 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() }; + 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(¤t_path); - + 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()); - } + 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 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(), - sig_type: item.sig_type.clone().unwrap_or_else(|| "Unknown".to_string()), - dims: item.dimensions.unwrap_or(0), - elems: item.elements.unwrap_or(1), - value: "".to_string(), - }); - } + 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() }); } } }); } @@ -263,82 +242,37 @@ impl MarteDebugApp { fn tcp_command_worker(shared_config: Arc>, rx_cmd: Receiver, tx_events: Sender) { 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); - } - } - + { 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(¤t_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 && 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(); - match serde_json::from_str::(json_clean) { - Ok(resp) => { let _ = tx_events_inner.send(InternalEvent::Discovery(resp.signals)); } - Err(e) => { let _ = tx_events_inner.send(InternalEvent::InternalLog(format!("JSON Parse Error (Discover): {}", e))); } - } - json_acc.clear(); - } else if trimmed == "OK TREE" { - in_json = false; - let json_clean = json_acc.trim_end_matches("OK TREE").trim(); - match serde_json::from_str::(json_clean) { - Ok(resp) => { let _ = tx_events_inner.send(InternalEvent::Tree(resp)); } - Err(e) => { let _ = tx_events_inner.send(InternalEvent::InternalLog(format!("JSON Parse Error (Tree): {}", e))); } - } - 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())); - } + 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::(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::(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 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); } @@ -349,35 +283,18 @@ fn tcp_command_worker(shared_config: Arc>, rx_cmd: Recei fn tcp_log_worker(shared_config: Arc>, tx_events: Sender) { 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); - } - } - + { 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(¤t_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; - } - } + 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(), - })); - } + 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(); } @@ -390,15 +307,11 @@ fn udp_worker(shared_config: Arc>, id_to_meta: Arc = None; let mut last_seq: Option = None; - loop { - let (ver, port) = { - let config = shared_config.lock().unwrap(); - (config.version, config.udp_port.clone()) - }; - + 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; @@ -406,121 +319,63 @@ fn udp_worker(shared_config: Arc>, id_to_meta: Arc().unwrap(); - if sock.bind(&addr.into()).is_ok() { - socket = Some(sock.into()); - bound = true; - } - } - - if !bound { - let _ = tx_events.send(InternalEvent::InternalLog(format!("UDP Bind Error on port {}", port))); - thread::sleep(std::time::Duration::from_secs(5)); - continue; + 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 = socket.as_ref().unwrap(); + 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 start_time = std::time::Instant::now(); let mut total_packets = 0u64; - loop { - if shared_config.lock().unwrap().version != current_version { - break; - } - + 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 (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; } - - // Sequence check 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 { - let dropped = if seq > last { seq - last - 1 } else { 0 }; - if dropped > 0 { - let _ = tx_events.send(InternalEvent::UdpDropped(dropped)); - } - } - } + 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 mut count_buf = [0u8; 4]; count_buf.copy_from_slice(&buf[16..20]); - let count = u32::from_le_bytes(count_buf); - - let now = start_time.elapsed().as_secs_f64(); + 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> = HashMap::new(); + let mut last_values: HashMap = HashMap::new(); let metas = id_to_meta.lock().unwrap(); - for _ in 0..count { if offset + 8 > n { break; } - let mut id_buf = [0u8; 4]; id_buf.copy_from_slice(&buf[offset..offset+4]); - let id = u32::from_le_bytes(id_buf); - let mut size_buf = [0u8; 4]; size_buf.copy_from_slice(&buf[offset+4..offset+8]); - let size = u32::from_le_bytes(size_buf); + 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 mut b = [0u8; 2]; b.copy_from_slice(data_slice); - if t.contains('u') { u16::from_le_bytes(b) as f64 } else { i16::from_le_bytes(b) as f64 } - }, - 4 => { - let mut b = [0u8; 4]; b.copy_from_slice(data_slice); - 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 mut b = [0u8; 8]; b.copy_from_slice(data_slice); - 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 } - }, + 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]); - } + 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); - } - while entry.values.len() > 10000 { - entry.values.pop_front(); - } + 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() > 10000 { entry.values.pop_front(); } } } } @@ -533,319 +388,175 @@ 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").to_string(), - level: "GUI_ERROR".to_string(), - message: msg, - }); - } - 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()); } - } - self.signals = signals; - } - 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) }); - } - InternalEvent::ClearTrace(name) => { - let mut data_map = self.traced_signals.lock().unwrap(); - data_map.remove(&name); - } - InternalEvent::CommandResponse(resp) => { - self.logs.push_back(LogEntry { - time: Local::now().format("%H:%M:%S").to_string(), - level: "CMD_RESP".to_string(), - message: resp, - }); - } - 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::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 let Some(dragged_name) = ctx.data_mut(|d| d.get_temp::(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").collapsible(false).resizable(false).show(ctx, |ui| { - ui.label(format!("Signal: {}", dialog.signal_path)); - ui.label(format!("Type: {} (Dims: {}, Elems: {})", dialog.sig_type, dialog.dims, dialog.elems)); - ui.horizontal(|ui| { - ui.label("Value:"); - ui.text_edit_singleline(&mut dialog.value); - }); - ui.horizontal(|ui| { - if ui.button("Apply Force").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; } - }); - }); + 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; } } - egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { + 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, "⚙ Debug"); + ui.toggle_value(&mut self.show_right_panel, "📊 Signals"); ui.toggle_value(&mut self.show_bottom_panel, "📜 Logs"); ui.separator(); - ui.heading("MARTe2 Debug Explorer"); + 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", egui::Color32::GREEN) } else { ("⏸ Pause", 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("🔌 Connection", |ui| { - egui::Grid::new("conn_grid") - .num_columns(2) - .spacing([40.0, 4.0]) - .show(ui, |ui| { - ui.label("Server IP:"); - ui.text_edit_singleline(&mut self.config.ip); - ui.end_row(); - ui.label("Control Port (TCP):"); - ui.text_edit_singleline(&mut self.config.tcp_port); - ui.end_row(); - ui.label("Telemetry Port (UDP):"); - ui.text_edit_singleline(&mut self.config.udp_port); - ui.end_row(); - ui.label("Log Port (TCP):"); - ui.text_edit_singleline(&mut self.config.log_port); - ui.end_row(); - }); + 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(); + ui.label("Telemetry:"); ui.text_edit_singleline(&mut self.config.udp_port); ui.end_row(); + ui.label("Logs:"); ui.text_edit_singleline(&mut self.config.log_port); ui.end_row(); + }); ui.separator(); - if ui.button("🔄 Apply & Reconnect").clicked() { - self.config.version += 1; - let mut shared = self.shared_config.lock().unwrap(); - *shared = self.config.clone(); - ui.close_menu(); - } + if ui.button("🔄 Apply & Reconnect").clicked() { self.config.version += 1; *self.shared_config.lock().unwrap() = self.config.clone(); ui.close_menu(); } + if ui.button("❌ Disconnect").clicked() { self.config.version += 1; let mut cfg = self.config.clone(); cfg.ip = "".to_string(); *self.shared_config.lock().unwrap() = cfg; ui.close_menu(); } }); - let status_color = if self.connected { egui::Color32::GREEN } else { egui::Color32::RED }; ui.label(egui::RichText::new(if self.connected { "● Online" } else { "○ Offline" }).color(status_color)); - - ui.separator(); - if ui.button("🔄 Refresh").clicked() { - let _ = self.tx_cmd.send("TREE".to_string()); - let _ = self.tx_cmd.send("DISCOVER".to_string()); - } - ui.separator(); - if self.is_paused { - if ui.button("▶ Resume").clicked() { - let _ = self.tx_cmd.send("RESUME".to_string()); - self.is_paused = false; - } - } else { - if ui.button("⏸ Pause").clicked() { - let _ = self.tx_cmd.send("PAUSE".to_string()); - self.is_paused = true; - } - } - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.label(format!("UDP: OK [{}] / DROPPED [{}]", self.udp_packets, self.udp_dropped)); - }); + 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_bottom_panel { - egui::TopBottomPanel::bottom("log_panel").resizable(true).default_height(200.0).show(ctx, |ui| { - ui.horizontal(|ui| { - ui.heading("System Logs"); - ui.separator(); - ui.label("Filter:"); - ui.text_edit_singleline(&mut self.log_filters.content_regex); - 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.toggle_value(&mut self.log_filters.paused, "⏸ Pause Logs"); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - 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) = ®ex { 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.scope(|ui| { - ui.horizontal(|ui| { - ui.label(egui::RichText::new(&log.time).monospace().color(egui::Color32::GRAY)); - ui.label(egui::RichText::new(format!("[{}]", log.level)).color(color).strong()); - ui.add(egui::Label::new(&log.message).wrap()); - ui.allocate_space(egui::vec2(ui.available_width(), 0.0)); - }); - }); - } - }); - }); - } - - if self.show_left_panel { - egui::SidePanel::left("signals_panel").resizable(true).width_range(300.0..=600.0).show(ctx, |ui| { - ui.heading("Application Tree"); - ui.separator(); - egui::ScrollArea::vertical().id_salt("tree_scroll").show(ui, |ui| { - if let Some(tree) = self.app_tree.clone() { - self.render_tree(ui, &tree, "".to_string()); - } else { - ui.label("Connecting to server..."); - } - }); - - ui.separator(); - ui.heading("Node Information"); - egui::ScrollArea::vertical().id_salt("info_scroll").show(ui, |ui| { - if self.node_info.is_empty() { - ui.label("Click 'Info' on a node to view details"); - } else { - ui.add(egui::Label::new(egui::RichText::new(&self.node_info).monospace()).wrap()); - } - }); - }); - } + 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("debug_panel").resizable(true).width_range(200.0..=400.0).show(ctx, |ui| { - ui.heading("Active Controls"); - ui.separator(); - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Forced Signals").strong()); - if ui.button("🗑").clicked() { - for path in self.forced_signals.keys().cloned().collect::>() { - let _ = self.tx_cmd.send(format!("UNFORCE {}", path)); - } - self.forced_signals.clear(); - } - }); - egui::ScrollArea::vertical().id_salt("forced_scroll").show(ui, |ui| { - let mut to_update = None; - let mut to_remove = None; - for (path, val) in &self.forced_signals { - ui.horizontal(|ui| { - if ui.selectable_label(false, format!("{}: {}", path, val)).clicked() { - to_update = Some(path.clone()); - } - if ui.button("❌").clicked() { - let _ = self.tx_cmd.send(format!("UNFORCE {}", path)); - to_remove = Some(path.clone()); - } - }); - } - if let Some(p) = to_remove { self.forced_signals.remove(&p); } - if let Some(p) = to_update { - self.forcing_dialog = Some(ForcingDialog { - signal_path: p.clone(), sig_type: "Unknown".to_string(), dims: 0, elems: 1, - value: self.forced_signals.get(&p).unwrap().clone(), - }); - } - }); - - ui.separator(); - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Traced Signals").strong()); - if ui.button("🗑").clicked() { - let names: Vec<_> = { - let data_map = self.traced_signals.lock().unwrap(); - data_map.keys().cloned().collect() - }; - for key in names { - let _ = self.tx_cmd.send(format!("TRACE {} 0", key)); - let _ = self.internal_tx.send(InternalEvent::ClearTrace(key)); - } - } - }); + 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| { - let mut names: Vec<_> = { - let data_map = self.traced_signals.lock().unwrap(); - data_map.keys().cloned().collect() - }; - names.sort(); 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| { - ui.label(&key); - if ui.button("❌").clicked() { - let _ = self.tx_cmd.send(format!("TRACE {} 0", key)); - let _ = self.internal_tx.send(InternalEvent::ClearTrace(key)); - } + 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("System 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); + ui.separator(); + ui.toggle_value(&mut self.log_filters.paused, "⏸ Pause"); + 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) = ®ex { 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| { - ui.horizontal(|ui| { - ui.heading("Oscilloscope"); - if ui.button("🔄 Reset View").clicked() { + 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]); + 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 !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]])); } } + 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::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.push([*t, final_v]); + } + plot_ui.line(Line::new(PlotPoints::from(points)).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::(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 }); + self.logs.push_back(LogEntry { time: Local::now().format("%H:%M:%S").to_string(), level: "GUI".to_string(), message: format!("Dropped {} into plot", dropped) }); + ctx.data_mut(|d| d.remove_temp::(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); } + }); + }); } - }); - let plot = Plot::new("traces_plot") - .legend(egui_plot::Legend::default()) - .auto_bounds(egui::Vec2b::new(true, true)) - .y_axis_min_width(4.0); - - plot.show(ui, |plot_ui| { - let data_map = self.traced_signals.lock().unwrap(); - for (name, data) in data_map.iter() { - let points: PlotPoints = data.values.iter().cloned().collect(); - plot_ui.line(Line::new(points).name(name)); - } - }); + if let Some(idx) = to_remove { self.plots.remove(idx); } + 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)); @@ -853,13 +564,6 @@ impl eframe::App for MarteDebugApp { } 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)))), - ) + 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))))) }