Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,40 @@ Get-FileHash .\SerialGUI_rs-x86_64-pc-windows-msvc.exe -Algorithm SHA256
2. Select the serial port and configure the parameters according to your needs.
3. Start monitoring the serial communication.

## Graph Mode

SerialGUI-Rs now includes a graph visualization feature for displaying numeric data in real-time.

### How to Use Graph Mode

1. Enable the chart panel by checking "Show chart panel" in the top menu bar
2. Send numeric data in a comma-separated format through your serial device
3. Watch as your data is automatically plotted on the chart

### Supported Data Format

The graph mode accepts data in the following format:
```
value1,value2,value3,value4,...
```

**Examples:**
- Simple integers: `1,2,3,4,5,6`
- Decimal values: `1.5,2.7,3.2,4.8`
- Mixed data: `10,25.5,30,15.2`
- Negative numbers: `-1,2,-3.5,4`

### Features

- **Real-time plotting**: Data points appear immediately as they arrive
- **Auto-scaling**: The Y-axis automatically adjusts to fit your data range
- **Streaming support**: Works with continuous data streams
- **Fragmentation handling**: Correctly processes data split across multiple packets
- **Memory efficient**: Limits data history to 1000 samples to prevent memory issues
- **Grid lines**: Visual reference lines help interpret the data values
- **Axis labels**: Clear X and Y axis labels showing data values and sample indices


## Contribution

Thank you for considering contributing to **SerialGUI-Rs**! Here are some basic rules for contributing to the project, following the guidelines of GNU projects:
Expand Down
Binary file modified assets/Screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 21 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::communicationtrait::{CommunicationEvent, CommunicationManager};
use crate::generalsettings::AppSettings;
use crate::gui::{ConnectionPanel, FileLogPanel, MenuBar, RxPanel, SendPanel};
use crate::gui::{ChartPanel, ConnectionPanel, FileLogPanel, MenuBar, RxPanel, SendPanel};
use crate::serial_impl::SerialCommunication;
use std::sync::{mpsc, Arc, Mutex};

Expand All @@ -16,6 +16,7 @@ pub struct TemplateApp {
#[serde(skip)]
connection_panel: ConnectionPanel,
rx_panel: RxPanel,
chart_panel: ChartPanel,
#[serde(skip)]
menu_bar: MenuBar,
send_panel: SendPanel,
Expand Down Expand Up @@ -45,6 +46,7 @@ impl Default for TemplateApp {
settings: settings.clone(),
connection_panel: ConnectionPanel::new(),
rx_panel: RxPanel::new(settings.max_log_string_length),
chart_panel: ChartPanel::new(settings.max_log_string_length),
menu_bar: MenuBar::new(),
send_panel: SendPanel::new(),
file_log_panel: FileLogPanel::new(default_filename),
Expand Down Expand Up @@ -128,6 +130,9 @@ impl TemplateApp {
};
self.write_log(&message);
self.file_log_panel.write_to_file(&data);
if self.settings.show_chart_panel {
self.chart_panel.process_rx(data);
}
ctx.request_repaint();
}
CommunicationEvent::ConnectionClosed => {
Expand Down Expand Up @@ -164,10 +169,22 @@ impl eframe::App for TemplateApp {

egui::CentralPanel::default().show(ctx, |ui| {
let available_size = ui.available_size();
let mut chart_area = available_size;
if self.settings.show_text_panel && self.settings.show_chart_panel {
chart_area.y *= 0.5;
}

if self.settings.show_chart_panel {
self.chart_panel
.show(ui, chart_area, self.settings.auto_scroll_log);
ui.separator();
}

self.rx_panel
.show(ui, available_size, self.settings.auto_scroll_log);
ui.separator();
if self.settings.show_text_panel {
self.rx_panel
.show(ui, chart_area, self.settings.auto_scroll_log);
ui.separator();
}

self.connection_panel
.show(ui, &mut self.serial_manager, &mut self.serial_events_rx);
Expand Down
4 changes: 4 additions & 0 deletions src/generalsettings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub struct AppSettings {
pub window_width: f32,
pub window_height: f32,
pub byte_mode: bool,
pub show_chart_panel: bool,
pub show_text_panel: bool,
}

impl Default for AppSettings {
Expand All @@ -27,6 +29,8 @@ impl Default for AppSettings {
window_width: 1050.0,
window_height: 500.0,
byte_mode: false,
show_chart_panel: false,
show_text_panel: true,
}
}
}
Expand Down
236 changes: 236 additions & 0 deletions src/gui/chart_panel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
use egui::{containers::Frame, emath, epaint, epaint::PathStroke, pos2, Color32, Pos2, Rect, Vec2};
use std::collections::VecDeque;

#[derive(Default)]
pub struct ChartPanel {
pub content: VecDeque<char>,
#[allow(dead_code)]
should_scroll_to_bottom: bool,
samples: VecDeque<f32>,
stream_buffer: String, // Buffer to accumulate partial data
}

impl ChartPanel {
pub fn new(_max_length: usize) -> Self {
Self {
content: VecDeque::new(),
should_scroll_to_bottom: false,
samples: VecDeque::new(),
stream_buffer: String::new(),
}
}

pub fn process_rx(&mut self, message: Vec<u8>) {
// Convert bytes to string and add to buffer
let message_str = String::from_utf8_lossy(&message);
self.stream_buffer.push_str(&message_str);

// Process complete values (those followed by commas)
while let Some(comma_pos) = self.stream_buffer.find(',') {
// Extract the complete value (everything up to the comma)
let complete_value = self.stream_buffer[..comma_pos].trim().to_string();

// Remove the processed part from buffer (including the comma)
self.stream_buffer = self.stream_buffer[comma_pos + 1..].to_string();

// Try to parse the complete value
if !complete_value.is_empty() {
if let Ok(sample) = complete_value.parse::<f32>() {
self.samples.push_back(sample);
}
}
}

// Prevent buffer from growing indefinitely
// Keep only the last incomplete value
if self.stream_buffer.len() > 50 {
// If buffer gets too long without a comma, it's probably garbage
// Keep only the last 20 characters in case there's a valid number at the end
let keep_from = self.stream_buffer.len().saturating_sub(20);
self.stream_buffer = self.stream_buffer[keep_from..].to_string();
}

// Limit the samples size to prevent memory growth
while self.samples.len() > 1000 {
self.samples.pop_front();
}
}

pub fn show(&mut self, ui: &mut egui::Ui, available_size: Vec2, _autoscroll: bool) {
// Reserve space for axes labels
let margin_left = 60.0;
let margin_bottom = 30.0;
let plot_size = Vec2::new(
available_size.x - margin_left,
(available_size.y * 0.85) - margin_bottom,
);

ui.horizontal(|ui| {
// Y-axis labels area
ui.allocate_ui_with_layout(
Vec2::new(margin_left - 10.0, plot_size.y),
egui::Layout::top_down(egui::Align::RIGHT),
|ui| {
if !self.samples.is_empty() {
let min_y = self.samples.iter().cloned().fold(f32::INFINITY, f32::min);
let max_y = self
.samples
.iter()
.cloned()
.fold(f32::NEG_INFINITY, f32::max);
let y_range = max_y - min_y;
let padding = if y_range > 0.0 { y_range * 0.1 } else { 1.0 };
let padded_min = min_y - padding;
let padded_max = max_y + padding;

// Draw Y-axis labels
for i in 0..=5 {
let y_val =
padded_min + (padded_max - padded_min) * (1.0 - i as f32 / 5.0);
let y_pos = (plot_size.y / 5.0) * i as f32;

ui.allocate_new_ui(
egui::UiBuilder::new().max_rect(Rect::from_min_size(
ui.min_rect().min + egui::vec2(0.0, y_pos - 8.0),
Vec2::new(margin_left - 15.0, 16.0),
)),
|ui| {
ui.with_layout(
egui::Layout::right_to_left(egui::Align::Center),
|ui| {
ui.label(format!("{y_val:.1}"));
},
);
},
);
}
}
},
);

// Plot area
Frame::canvas(ui.style()).show(ui, |ui| {
ui.ctx().request_repaint();

let (_id, rect) = ui.allocate_space(plot_size);

// Auto-scale based on actual data
let (min_val, max_val, x_range) = if self.samples.is_empty() {
(-1.0, 1.0, 0.0..=100.0)
} else {
let min_y = self.samples.iter().cloned().fold(f32::INFINITY, f32::min);
let max_y = self
.samples
.iter()
.cloned()
.fold(f32::NEG_INFINITY, f32::max);

let y_range = max_y - min_y;
let padding = if y_range > 0.0 { y_range * 0.1 } else { 1.0 };
let padded_min = min_y - padding;
let padded_max = max_y + padding;

let x_max = (self.samples.len() as f32).max(10.0);

(padded_min, padded_max, 0.0..=x_max)
};

let to_screen = emath::RectTransform::from_to(
Rect::from_x_y_ranges(x_range, max_val..=min_val), // Change this line
rect,
);

let mut shapes = vec![];

// Draw grid lines
if !self.samples.is_empty() {
// Horizontal grid lines
for i in 0..=5 {
let y = min_val + (max_val - min_val) * (i as f32 / 5.0);
let start = to_screen * pos2(0.0, y);
let end = to_screen * pos2(self.samples.len() as f32, y);
shapes.push(epaint::Shape::line_segment(
[start, end],
(0.5, Color32::GRAY.gamma_multiply(0.2)),
));
}

// Vertical grid lines
let num_v_lines = 8;
for i in 0..=num_v_lines {
let x = (self.samples.len() as f32) * (i as f32 / num_v_lines as f32);
let start = to_screen * pos2(x, min_val);
let end = to_screen * pos2(x, max_val);
shapes.push(epaint::Shape::line_segment(
[start, end],
(0.5, Color32::GRAY.gamma_multiply(0.2)),
));
}
}

// Draw data line
if !self.samples.is_empty() {
let points: Vec<Pos2> = self
.samples
.iter()
.enumerate()
.map(|(i, &value)| to_screen * pos2(i as f32, value))
.collect();

shapes.push(epaint::Shape::line(
points,
PathStroke::new(2.0, Color32::from_rgb(0, 150, 255)),
));
}

ui.painter().extend(shapes);
});
});

// X-axis labels
if !self.samples.is_empty() {
ui.horizontal(|ui| {
ui.add_space(margin_left);

let num_x_labels = 6;
let label_width = plot_size.x / num_x_labels as f32;

for i in 0..=num_x_labels {
let x_val = (self.samples.len() as f32) * (i as f32 / num_x_labels as f32);

ui.allocate_ui_with_layout(
Vec2::new(label_width, 20.0),
egui::Layout::centered_and_justified(egui::Direction::TopDown),
|ui| {
ui.label(format!("{x_val:.0}"));
},
);
}
});
}
}
}

impl serde::Serialize for ChartPanel {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.content.serialize(serializer)
}
}

impl<'de> serde::Deserialize<'de> for ChartPanel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let content = VecDeque::deserialize(deserializer)?;
Ok(Self {
content,
should_scroll_to_bottom: false,
samples: VecDeque::new(),
stream_buffer: String::new(),
})
}
}
10 changes: 10 additions & 0 deletions src/gui/menu_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ impl MenuBar {
clear_callback();
}
ui.checkbox(&mut settings.byte_mode, "Byte mode");

// Ensure at least one panel is always enabled
ui.add_enabled(
settings.show_text_panel,
egui::Checkbox::new(&mut settings.show_chart_panel, "Show chart panel"),
);
ui.add_enabled(
settings.show_chart_panel,
egui::Checkbox::new(&mut settings.show_text_panel, "Show text panel"),
);
});
});
});
Expand Down
2 changes: 2 additions & 0 deletions src/gui/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod chart_panel;
pub mod connection_panel;
pub mod file_log_panel;
pub mod menu_bar;
Expand All @@ -6,6 +7,7 @@ pub mod send_panel;
pub mod settings_panel;

// Re-export para facilitar el uso
pub use chart_panel::ChartPanel;
pub use connection_panel::ConnectionPanel;
pub use file_log_panel::FileLogPanel;
pub use menu_bar::MenuBar;
Expand Down
2 changes: 2 additions & 0 deletions src/gui/settings_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ impl SettingsPanel {
});

ui.checkbox(&mut settings.byte_mode, "Byte mode");
ui.checkbox(&mut settings.show_chart_panel, "Show chart panel");
ui.checkbox(&mut settings.show_text_panel, "Show text panel");
});
}
}
Loading