Skip to content

Commit f2f9385

Browse files
committed
Make it remember previous settings and log everything to a csv file
1 parent 6858254 commit f2f9385

6 files changed

Lines changed: 499 additions & 8 deletions

File tree

Cargo.lock

Lines changed: 121 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ CanOpenDataViewer/
2525
* **Configurable SDO Polling:** Select any SDO from a device's Object Dictionary, set a custom polling rate for each, and see the values plotted or logged in real-time.
2626
* **Node Health Monitoring:** Automatic health checks verify that the CANopen node is alive by periodically reading the mandatory Device Type object (0x1000:00). Detects node disconnection within 4-6 seconds and updates the UI accordingly.
2727
* **Connection Status & Error Reporting:** Clear visual indicators show whether the node is connected (green) or disconnected (red). All connection failures and SDO read errors are displayed in dismissible error banners with detailed messages.
28+
* **Configuration Persistence:** Automatically saves and restores your last used settings (CAN interface, Node ID, EDS file path, logging preferences). No need to re-enter configuration on every startup.
29+
* **Automatic File Logging:** Optionally log all SDO data, connection events, and errors to CSV files with timestamps. Logs are saved to `~/.local/share/canopen-viewer/logs/` by default. Enable/disable logging via the checkbox in the top panel, and open the log folder with one click.
2830
* **Selective TPDO Monitoring:** The UI automatically lists all available Transmit-PDOs from a device profile. Simply check the ones you want to monitor.
2931
* **Intelligent Data Handling:**
3032
* **Numeric** PDO data is automatically sent to the real-time plot.
@@ -128,11 +130,11 @@ This ensures the user is always presented with information in its most useful fo
128130
```
129131

130132
Then in the GUI:
131-
- Select CAN interface: `vcan0`
132-
- Enter Node ID: `4`
133-
- Select EDS file: `examples/mock_node.eds` (for testing with mock node)
134-
- Click "Start"
133+
- **First time:** Select CAN interface (`vcan0`), enter Node ID (`4`), and select EDS file (`examples/mock_node.eds`)
134+
- **Subsequent times:** Your last configuration will be automatically loaded - just click through the steps
135+
- Click "Start" - your settings will be saved automatically
135136
- **You should see:** Green "● Connected" status in the top panel
137+
- **Optional:** Enable logging with the "Enable Logging" checkbox (top-right) to record all events to CSV
136138
- Subscribe to SDOs via the "Subscribe to SDO" button
137139
- **Try it:** Stop the mock node (Ctrl+C in Terminal 1) and watch the status change to red "● Disconnected" within 4-6 seconds
138140

@@ -193,6 +195,26 @@ This ensures the user is always presented with information in its most useful fo
193195
1. Check CPU usage - the application should use minimal CPU when idle
194196
2. Report the issue with steps to reproduce at: https://github.com/erdemsimsek/CanOpenDataViewer/issues
195197
198+
### Logging Issues
199+
200+
**Problem:** Logging checkbox doesn't work or logs aren't being created.
201+
202+
**Solution:**
203+
1. Check that the log directory is writable: `~/.local/share/canopen-viewer/logs/`
204+
2. If the directory doesn't exist, the application will try to create it automatically
205+
3. Click "Open Log Folder" button to view the log directory in your file manager
206+
4. Log files are named: `canopen_log_YYYYMMDD_HHMMSS.csv`
207+
208+
**Log File Format:**
209+
- CSV format with headers: `Timestamp, Event Type, Address, Value, Message`
210+
- Event types: `SDO_DATA`, `SDO_ERROR`, `CONNECTION_FAILED`, `CONNECTION_STATUS`
211+
- Open with any spreadsheet application (Excel, LibreOffice Calc, etc.)
212+
213+
**Configuration File Location:**
214+
- Configuration is saved to: `~/.config/canopen-viewer/config.toml`
215+
- You can manually edit this file if needed
216+
- Fields: `can_interface`, `node_id`, `eds_file_path`, `enable_logging`, `log_directory`
217+
196218
## Roadmap
197219

198220
* []

canopen-viewer/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,12 @@ egui_plot = "0.31.0"
1717
image = "0.25.1"
1818
chrono = "0.4.41"
1919

20+
# Configuration and logging
21+
serde = { version = "1.0", features = ["derive"] }
22+
toml = "0.8"
23+
csv = "1.3"
24+
directories = "5.0"
25+
open = "5.0"
26+
2027
# This will use the shared CANopen protocol code
2128
canopen-common = { path = "../canopen-common" }

canopen-viewer/src/config.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
use std::path::PathBuf;
2+
use serde::{Deserialize, Serialize};
3+
use std::fs;
4+
5+
#[derive(Debug, Clone, Serialize, Deserialize)]
6+
pub struct AppConfig {
7+
pub can_interface: String,
8+
pub node_id: u8,
9+
pub eds_file_path: Option<String>,
10+
pub enable_logging: bool,
11+
pub log_directory: Option<String>,
12+
}
13+
14+
impl Default for AppConfig {
15+
fn default() -> Self {
16+
Self {
17+
can_interface: String::new(),
18+
node_id: 1,
19+
eds_file_path: None,
20+
enable_logging: true,
21+
log_directory: None,
22+
}
23+
}
24+
}
25+
26+
impl AppConfig {
27+
/// Get the path to the config file
28+
pub fn config_file_path() -> Option<PathBuf> {
29+
directories::ProjectDirs::from("com", "canopen", "canopen-viewer")
30+
.map(|proj_dirs| {
31+
let config_dir = proj_dirs.config_dir();
32+
config_dir.join("config.toml")
33+
})
34+
}
35+
36+
/// Load configuration from file, returns default if file doesn't exist or on error
37+
pub fn load() -> Self {
38+
if let Some(config_path) = Self::config_file_path() {
39+
if config_path.exists() {
40+
match fs::read_to_string(&config_path) {
41+
Ok(contents) => {
42+
match toml::from_str(&contents) {
43+
Ok(config) => {
44+
println!("✓ Loaded configuration from {:?}", config_path);
45+
return config;
46+
}
47+
Err(e) => {
48+
eprintln!("Failed to parse config file: {}", e);
49+
}
50+
}
51+
}
52+
Err(e) => {
53+
eprintln!("Failed to read config file: {}", e);
54+
}
55+
}
56+
}
57+
}
58+
59+
println!("Using default configuration");
60+
Self::default()
61+
}
62+
63+
/// Save configuration to file
64+
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
65+
if let Some(config_path) = Self::config_file_path() {
66+
// Create config directory if it doesn't exist
67+
if let Some(parent) = config_path.parent() {
68+
fs::create_dir_all(parent)?;
69+
}
70+
71+
let toml_string = toml::to_string_pretty(self)?;
72+
fs::write(&config_path, toml_string)?;
73+
println!("✓ Saved configuration to {:?}", config_path);
74+
Ok(())
75+
} else {
76+
Err("Could not determine config file path".into())
77+
}
78+
}
79+
80+
/// Get the default log directory path
81+
pub fn default_log_directory() -> Option<PathBuf> {
82+
directories::ProjectDirs::from("com", "canopen", "canopen-viewer")
83+
.map(|proj_dirs| {
84+
let data_dir = proj_dirs.data_local_dir();
85+
data_dir.join("logs")
86+
})
87+
}
88+
89+
/// Get the log directory as PathBuf, using default if not set
90+
pub fn get_log_directory(&self) -> Option<PathBuf> {
91+
if let Some(ref dir) = self.log_directory {
92+
Some(PathBuf::from(dir))
93+
} else {
94+
Self::default_log_directory()
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)