Files
marte-debug/Tools/gui_client/src/main.rs
T

1818 lines
77 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 arrow::array::Float64Array;
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use chrono::Local;
use crossbeam_channel::{unbounded, Receiver, Sender};
use eframe::egui;
use egui_plot::{Line, LineStyle, MarkerShape, Plot, PlotBounds, PlotPoints, VLine};
use once_cell::sync::Lazy;
use parquet::arrow::arrow_writer::ArrowWriter;
use parquet::file::properties::WriterProperties;
use regex::Regex;
use rfd::FileDialog;
use serde::{Deserialize, Serialize};
use socket2::{Domain, Protocol, Socket, Type};
use std::collections::{HashMap, VecDeque};
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::net::{TcpStream, UdpSocket};
use std::sync::{Arc, Mutex};
use std::thread;
static BASE_TELEM_TS: Lazy<Mutex<Option<u64>>> = Lazy::new(|| Mutex::new(None));
// --- Models ---
#[derive(Serialize, Deserialize, Clone, Debug)]
struct Signal {
name: String,
id: u32,
#[serde(rename = "type")]
sig_type: String,
#[serde(default)]
dimensions: u8,
#[serde(default = "default_elements")]
elements: u32,
}
fn default_elements() -> u32 { 1 }
#[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>,
#[serde(rename = "IsTraceable")]
is_traceable: Option<bool>,
#[serde(rename = "IsForcable")]
is_forcable: Option<bool>,
}
#[derive(Clone)]
struct LogEntry {
time: String,
level: String,
message: String,
}
struct TraceData {
values: VecDeque<[f64; 2]>,
last_value: f64,
recording_tx: Option<Sender<[f64; 2]>>,
recording_path: Option<String>,
is_monitored: bool,
}
struct SignalMetadata {
names: Vec<String>,
sig_type: String,
dimensions: u8,
elements: u32,
}
#[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, bool), // Name, IsMonitored
ClearTrace(String),
UdpStats(u64),
UdpDropped(u32),
RecordPathChosen(String, String), // SignalName, FilePath
RecordingError(String, String), // SignalName, ErrorMessage
TelemMatched(u32), // Signal ID
ServiceConfig { udp_port: String, log_port: String },
}
// --- App State ---
struct ForcingDialog {
signal_path: String,
value: String,
}
struct MonitorDialog {
signal_path: String,
period_ms: 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_type: TriggerType,
trigger_source: String,
trigger_edge: TriggerEdge,
trigger_threshold: f64,
pre_trigger_percent: f64,
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>,
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,
show_left_panel: bool,
show_right_panel: bool,
show_bottom_panel: bool,
selected_node: String,
node_info: String,
udp_packets: u64,
udp_dropped: u64,
telem_match_count: HashMap<u32, u64>,
forcing_dialog: Option<ForcingDialog>,
monitoring_dialog: Option<MonitorDialog>,
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,
}
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,
telem_match_count: HashMap::new(),
forcing_dialog: None,
monitoring_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 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 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") {
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(),
});
// Note: internal monitoring logic will handle individual elements via naming convention
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(),
});
}
}
}
});
}
}
}
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.contains("OK DISCOVER") {
in_json = false;
let json_clean =
json_acc.split("OK DISCOVER").next().unwrap_or("").trim();
match serde_json::from_str::<DiscoverResponse>(json_clean) {
Ok(resp) => {
let _ = tx_events_inner
.send(InternalEvent::Discovery(resp.signals));
}
Err(e) => {
let _ =
tx_events_inner.send(InternalEvent::InternalLog(format!(
"Discovery JSON Error: {} | Payload: {}",
e, json_clean
)));
}
}
json_acc.clear();
} else if trimmed.contains("OK TREE") {
in_json = false;
let json_clean = json_acc.split("OK TREE").next().unwrap_or("").trim();
match serde_json::from_str::<TreeItem>(json_clean) {
Ok(resp) => {
let _ = tx_events_inner.send(InternalEvent::Tree(resp));
}
Err(e) => {
let _ = tx_events_inner.send(InternalEvent::InternalLog(
format!("Tree JSON Error: {}", e),
));
}
}
json_acc.clear();
} else if trimmed.contains("OK INFO") {
in_json = false;
let json_clean = json_acc.split("OK INFO").next().unwrap_or("").trim();
let _ = tx_events_inner
.send(InternalEvent::NodeInfo(json_clean.to_string()));
json_acc.clear();
}
} else {
if trimmed.starts_with("OK SERVICE_INFO") {
// OK SERVICE_INFO TCP_CTRL:8110 UDP_STREAM:8111 TCP_LOG:8082 STATE:RUNNING
let parts: Vec<&str> = trimmed.split_whitespace().collect();
let mut udp = String::new();
let mut log = String::new();
for p in parts {
if p.starts_with("UDP_STREAM:") {
udp = p.split(':').nth(1).unwrap_or("").to_string();
}
if p.starts_with("TCP_LOG:") {
log = p.split(':').nth(1).unwrap_or("").to_string();
}
}
if !udp.is_empty() || !log.is_empty() {
let _ = tx_events_inner.send(InternalEvent::ServiceConfig {
udp_port: udp,
log_port: log,
});
}
}
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;
let _ = tx_events.send(InternalEvent::Disconnected);
break;
}
}
if stream.write_all(format!("{}\n", cmd).as_bytes()).is_err() {
let _ = tx_events.send(InternalEvent::Disconnected);
break;
}
// Small delay to allow server to process and send response
thread::sleep(std::time::Duration::from_millis(100));
}
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 recording_worker(
rx: Receiver<[f64; 2]>,
path: String,
signal_name: String,
tx_events: Sender<InternalEvent>,
) {
let file = match File::create(&path) {
Ok(f) => f,
Err(e) => {
let _ = tx_events.send(InternalEvent::RecordingError(
signal_name,
format!("File Error: {}", e),
));
return;
}
};
let schema = Arc::new(Schema::new(vec![
Field::new("timestamp", DataType::Float64, false),
Field::new("value", DataType::Float64, false),
]));
let mut writer = match ArrowWriter::try_new(
file,
schema.clone(),
Some(WriterProperties::builder().build()),
) {
Ok(w) => w,
Err(e) => {
let _ = tx_events.send(InternalEvent::RecordingError(
signal_name,
format!("Parquet Error: {}", e),
));
return;
}
};
let (mut t_acc, mut v_acc) = (Vec::with_capacity(1000), Vec::with_capacity(1000));
while let Ok([t, v]) = rx.recv() {
t_acc.push(t);
v_acc.push(v);
if t_acc.len() >= 1000 {
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Float64Array::from(t_acc.clone())),
Arc::new(Float64Array::from(v_acc.clone())),
],
)
.unwrap();
let _ = writer.write(&batch);
t_acc.clear();
v_acc.clear();
}
}
if !t_acc.is_empty() {
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Float64Array::from(t_acc)),
Arc::new(Float64Array::from(v_acc)),
],
)
.unwrap();
let _ = writer.write(&batch);
}
let _ = writer.close();
}
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;
let mut last_warning_time = std::time::Instant::now();
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;
{
let mut base = BASE_TELEM_TS.lock().unwrap();
*base = None;
}
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;
}
if u32::from_le_bytes(buf[0..4].try_into().unwrap()) != 0xDA7A57AD {
continue;
}
let seq = u32::from_le_bytes(buf[4..8].try_into().unwrap());
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 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();
if metas.is_empty() && count > 0 && last_warning_time.elapsed().as_secs() > 5 {
let _ = tx_events.send(InternalEvent::InternalLog(
"UDP received but Metadata empty. Still discovering?".to_string(),
));
last_warning_time = std::time::Instant::now();
}
for _ in 0..count {
if offset + 16 > n {
break;
}
let id = u32::from_le_bytes(buf[offset..offset + 4].try_into().unwrap());
let ts_raw =
u64::from_le_bytes(buf[offset + 4..offset + 12].try_into().unwrap());
let size =
u32::from_le_bytes(buf[offset + 12..offset + 16].try_into().unwrap());
offset += 16;
if offset + size as usize > n {
break;
}
let data_slice = &buf[offset..offset + size as usize];
let mut base_ts_guard = BASE_TELEM_TS.lock().unwrap();
if base_ts_guard.is_none() {
*base_ts_guard = Some(ts_raw);
}
let base = base_ts_guard.unwrap();
let ts_s = if ts_raw >= base {
(ts_raw - base) as f64 / 1000000.0
} else {
0.0 // Avoid huge jitter wrap-around
};
drop(base_ts_guard);
if let Some(meta) = metas.get(&id) {
let _ = tx_events.send(InternalEvent::TelemMatched(id));
let t = meta.sig_type.as_str();
let type_size = if meta.elements > 0 { size / meta.elements } else { size };
for i in 0..meta.elements {
let elem_offset = (i * type_size) as usize;
if elem_offset + type_size as usize > data_slice.len() { break; }
let elem_data = &data_slice[elem_offset..elem_offset + type_size as usize];
let val = match type_size {
1 => {
if t.contains('u') {
elem_data[0] as f64
} else {
(elem_data[0] as i8) as f64
}
}
2 => {
let b = elem_data[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 = elem_data[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 = elem_data[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 {
let target_name = if meta.elements > 1 {
format!("{}[{}]", name, i)
} else {
name.clone()
};
local_updates
.entry(target_name.clone())
.or_default()
.push([ts_s, val]);
last_values.insert(target_name, 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(tx) = &entry.recording_tx {
let _ = tx.send(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(),
dimensions: s.dimensions,
elements: s.elements,
});
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;
self.logs.push_back(LogEntry {
time: Local::now().format("%H:%M:%S").to_string(),
level: "GUI_INFO".to_string(),
message: format!("Trace requested for: {}", name),
});
}
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;
// Wait for connection to stabilize before sending commands
std::thread::sleep(std::time::Duration::from_millis(200));
let _ = self.tx_cmd.send("SERVICE_INFO".to_string());
std::thread::sleep(std::time::Duration::from_millis(100));
let _ = self.tx_cmd.send("TREE".to_string());
// Wait for TREE response before sending next command
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = self.tx_cmd.send("DISCOVER".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();
self.logs.push_back(LogEntry {
time: Local::now().format("%H:%M:%S").to_string(),
level: "GUI_INFO".to_string(),
message: format!("Config updated from server: UDP={}, LOG={}", self.config.udp_port, self.config.log_port),
});
}
}
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,
});
}
InternalEvent::TelemMatched(id) => {
*self.telem_match_count.entry(id).or_insert(0) += 1;
}
InternalEvent::RecordPathChosen(name, path) => {
let mut data_map = self.traced_signals.lock().unwrap();
if let Some(entry) = data_map.get_mut(&name) {
let (tx, rx) = unbounded();
entry.recording_tx = Some(tx);
entry.recording_path = Some(path.clone());
let tx_err = self.internal_tx.clone();
thread::spawn(move || {
recording_worker(rx, path, name, tx_err);
});
}
}
InternalEvent::RecordingError(name, err) => {
self.logs.push_back(LogEntry {
time: Local::now().format("%H:%M:%S").to_string(),
level: "REC_ERROR".to_string(),
message: format!("{}: {}", name, err),
});
}
}
}
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(dialog) = &mut self.monitoring_dialog {
let mut close = false;
egui::Window::new("Monitor Signal").show(ctx, |ui| {
ui.label(&dialog.signal_path);
ui.horizontal(|ui| {
ui.label("Period (ms):");
ui.text_edit_singleline(&mut dialog.period_ms);
});
ui.horizontal(|ui| {
if ui.button("Apply").clicked() {
let period = dialog.period_ms.parse::<u32>().unwrap_or(100);
let _ = self
.tx_cmd
.send(format!("MONITOR SIGNAL {} {}", dialog.signal_path, period));
let _ = self.tx_cmd.send("DISCOVER".to_string());
// Check if it's an array signal to add all elements to view
let mut elements = 1;
if let Some(tree) = &self.app_tree {
// Helper to find item in tree
fn find_item<'a>(item: &'a TreeItem, target: &str, current: &str) -> Option<&'a TreeItem> {
let path = if current.is_empty() { item.name.clone() } else { format!("{}.{}", current, item.name) };
if path == target || (current.is_empty() && item.name == "Root" && target.is_empty()) { return Some(item); }
if let Some(children) = &item.children {
for child in children {
if let Some(found) = find_item(child, target, &path) { return Some(found); }
}
}
None
}
if let Some(found) = find_item(tree, &dialog.signal_path, "") {
elements = found.elements.unwrap_or(1);
}
}
if elements > 1 {
for i in 0..elements {
let elem_path = format!("{}[{}]", dialog.signal_path, i);
let _ = self.internal_tx.send(InternalEvent::TraceRequested(elem_path, true));
}
} else {
let _ = self
.internal_tx
.send(InternalEvent::TraceRequested(dialog.signal_path.clone(), true));
}
close = true;
}
if ui.button("Cancel").clicked() {
close = true;
}
});
});
if close {
self.monitoring_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();
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");
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("⚙ Trig", |ui| {
egui::Grid::new("trig").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("Thresh:");
ui.add(
egui::DragValue::new(&mut self.scope.trigger_threshold)
.speed(0.1),
);
ui.end_row();
ui.label("Pre %:");
ui.add(egui::Slider::new(
&mut self.scope.pre_trigger_percent,
0.0..=100.0,
));
ui.end_row();
ui.label("Type:");
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();
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();
ui.label("Telemetry (Auto):");
ui.label(&self.config.udp_port);
ui.end_row();
ui.label("Logs (Auto):");
ui.label(&self.config.log_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("📡 Re-Discover").clicked() {
let _ = self.tx_cmd.send("DISCOVER".to_string());
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 mut data_map = self.traced_signals.lock().unwrap();
if let Some(entry) = data_map.get_mut(&key) {
let last_val = entry.last_value;
let is_recording = entry.recording_tx.is_some();
ui.horizontal(|ui| {
if is_recording {
ui.label(
egui::RichText::new("●").color(egui::Color32::RED),
);
}
let response = ui.add(
egui::Label::new(format!("{}: {:.2}", key, last_val))
.sense(
egui::Sense::drag().union(egui::Sense::click()),
),
);
if response.drag_started() {
ctx.data_mut(|d| {
d.insert_temp(
egui::Id::new("drag_signal"),
key.clone(),
)
});
}
response.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()));
}
});
}
}
});
ui.separator();
ui.heading("Forced Signals");
let mut to_delete = Vec::new();
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));
to_delete.push(path.to_owned());
}
});
}
for key in to_delete.iter() {
self.forced_signals.remove(key);
}
});
}
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" | "GUI_INFO" | "GUI_WARN" | "CMD_RESP" => {
self.log_filters.show_info
}
"Warning" => self.log_filters.show_warning,
"FatalError" | "OSError" | "ParametersError" | "GUI_ERROR"
| "REC_ERROR" => 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" | "GUI_ERROR"
| "REC_ERROR" => egui::Color32::from_rgb(255, 100, 100),
"Warning" | "GUI_WARN" => {
egui::Color32::from_rgb(255, 255, 100)
}
"Information" | "GUI_INFO" => {
egui::Color32::from_rgb(100, 255, 100)
}
"Debug" => egui::Color32::from_rgb(100, 100, 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]);
plot = plot.x_axis_formatter(|mark, _range| {
let val = mark.value;
let hours = (val / 3600.0) as u32;
let mins = ((val % 3600.0) / 60.0) as u32;
let secs = val % 60.0;
format!("{:02}:{:02}:{:05.2}", hours, mins, secs)
});
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];
}
}
}
}
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 {
latest_t
}
} else {
latest_t
};
let x_min =
center_t - (self.scope.pre_trigger_percent / 100.0) * window_s;
plot = plot.include_x(x_min).include_x(x_min + window_s);
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]],
));
}
}
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 }),
);
}
for (s_idx, sig_cfg) in plot_inst.signals.iter().enumerate() {
if let Some(data) = data_map.get(&sig_cfg.source_name) {
let points_iter =
data.values.iter().rev().take(5000).rev().map(|[t, v]| {
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 });
}
[*t, final_v]
});
plot_ui.line(
Line::new(PlotPoints::from_iter(points_iter))
.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]]);
}
});
drop(data_map);
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.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("🎨 Style").clicked() {
self.style_editor = Some((p_idx, s_idx));
ui.close_menu();
}
if ui.button("❌ Remove").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)))),
)
}