Skip to content

Commit 6858254

Browse files
committed
Improvement on error reporting and user feedback
1 parent d8f59d0 commit 6858254

3 files changed

Lines changed: 193 additions & 14 deletions

File tree

README.md

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ CanOpenDataViewer/
2323

2424
* **Real-time Plotting:** Visualize numeric TPDO and SDO data as it arrives from the CAN bus using a smooth, high-performance plot.
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.
26+
* **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.
27+
* **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.
2628
* **Selective TPDO Monitoring:** The UI automatically lists all available Transmit-PDOs from a device profile. Simply check the ones you want to monitor.
2729
* **Intelligent Data Handling:**
2830
* **Numeric** PDO data is automatically sent to the real-time plot.
@@ -66,8 +68,12 @@ This ensures the user is always presented with information in its most useful fo
6668
3. **Configure Monitoring:**
6769
* For **TPDOs**, the user simply checks a box next to each PDO they wish to monitor.
6870
* For **SDOs**, the user selects an SDO and enters a polling interval in milliseconds (e.g., `250ms`).
69-
4. **Start Session:** The user clicks "Connect". The CAN thread starts, begins polling the configured SDOs at their specified rates, and listens for all incoming TPDOs.
70-
5. **View Data:** As messages arrive, the application checks if they are on the user's monitoring list and handles them according to the design philosophy: plotting numeric data and logging everything else.
71+
4. **Start Session:** The user clicks "Start". The CAN thread establishes the connection and begins health monitoring.
72+
5. **Monitor Connection:** The application automatically checks node health every 2 seconds by reading the Device Type object (0x1000:00). The UI displays:
73+
* **Green "● Connected"** when the node responds to health checks
74+
* **Red "● Disconnected"** when the node stops responding (after 2 consecutive failures)
75+
* **Error banners** for connection failures or SDO read errors (click "✖" to dismiss)
76+
6. **View Data:** As messages arrive, the application checks if they are on the user's monitoring list and handles them according to the design philosophy: plotting numeric data and logging everything else.
7177

7278
## Technology Stack
7379

@@ -124,8 +130,68 @@ This ensures the user is always presented with information in its most useful fo
124130
Then in the GUI:
125131
- Select CAN interface: `vcan0`
126132
- Enter Node ID: `4`
127-
- Select EDS file (or skip for testing)
128-
- Click "Start" and subscribe to SDOs
133+
- Select EDS file: `examples/mock_node.eds` (for testing with mock node)
134+
- Click "Start"
135+
- **You should see:** Green "● Connected" status in the top panel
136+
- Subscribe to SDOs via the "Subscribe to SDO" button
137+
- **Try it:** Stop the mock node (Ctrl+C in Terminal 1) and watch the status change to red "● Disconnected" within 4-6 seconds
138+
139+
## Troubleshooting
140+
141+
### "Network is down (os error 100)" or Connection Failed
142+
143+
**Problem:** Error banner appears immediately after clicking "Start" with message about network being down.
144+
145+
**Solution:**
146+
1. Check if the CAN interface exists and is UP:
147+
```bash
148+
ip link show vcan0
149+
```
150+
151+
2. If the interface doesn't exist or is DOWN, set it up:
152+
```bash
153+
sudo modprobe vcan
154+
sudo ip link add dev vcan0 type vcan
155+
sudo ip link set up vcan0
156+
```
157+
158+
3. Verify the interface is UP:
159+
```bash
160+
ip link show vcan0
161+
# Should show: "vcan0: <NOARP,UP,LOWER_UP> ..."
162+
```
163+
164+
### Connection Shows "Disconnected" Even Though Mock Node is Running
165+
166+
**Problem:** The status indicator shows red "● Disconnected" even when the mock node is running.
167+
168+
**Possible causes:**
169+
1. **Wrong Node ID:** Ensure the Node ID in the viewer matches the mock node's `--node-id` parameter (default: 4)
170+
2. **Wrong Interface:** Ensure both applications are using the same CAN interface (e.g., `vcan0`)
171+
3. **Mock Node Crashed:** Check Terminal 1 for errors in the mock node output
172+
4. **Node Missing 0x1000:00:** The health check reads Device Type (0x1000:00), which must exist in the node's object dictionary
173+
174+
### SDO Read Errors
175+
176+
**Problem:** Error banner shows "SDO Read Error: 0xXXXX:YY - request failed: sdo request timeout"
177+
178+
**Possible causes:**
179+
1. **Object doesn't exist:** The SDO address may not be implemented in the node
180+
2. **Node stopped responding:** Check if the mock node is still running
181+
3. **Wrong data type:** The data type selected in the subscription doesn't match the object's actual type in the EDS file
182+
183+
**Solution:**
184+
- Verify the object exists in the EDS file and is marked as readable (`accesstype=ro` or `accesstype=rw`)
185+
- Check that the mock node is still running and hasn't crashed
186+
- Ensure the data type matches what's specified in the EDS file
187+
188+
### Application Freezes or Unresponsive
189+
190+
**Problem:** The UI becomes unresponsive or freezes.
191+
192+
**This shouldn't happen!** The application is designed with a non-blocking UI architecture. If you encounter this:
193+
1. Check CPU usage - the application should use minimal CPU when idle
194+
2. Report the issue with steps to reproduce at: https://github.com/erdemsimsek/CanOpenDataViewer/issues
129195
130196
## Roadmap
131197

canopen-viewer/src/communication.rs

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,18 @@ pub enum Command {
4949
#[derive(Debug)]
5050
pub enum Update {
5151
SdoList(BTreeMap<u16, SdoObject>),
52-
#[allow(dead_code)] // TODO: Will be used in Priority 1 fixes for connection status
52+
#[allow(dead_code)] // TODO: Will be used for successful connection notifications
5353
ConnectionSuccess(BTreeMap<u16, SdoObject>),
54-
#[allow(dead_code)] // TODO: Will be used in Priority 1 fixes for error reporting
5554
ConnectionFailed(String),
55+
ConnectionStatus(bool), // true = node alive, false = node not responding
5656
SdoData {
5757
address: SdoAddress,
5858
value: String,
5959
},
60+
SdoReadError {
61+
address: SdoAddress,
62+
error: String,
63+
},
6064
}
6165

6266
async fn sdo_polling_task(
@@ -86,11 +90,57 @@ async fn sdo_polling_task(
8690
value: value_string,
8791
});
8892
},
89-
_ => {}
93+
Err(err) => {
94+
let _ = update_tx.send(Update::SdoReadError {
95+
address: address.clone(),
96+
error: err.to_string(),
97+
});
98+
}
9099
};
91100
}
92101
}
93102

103+
/// Health check task that periodically reads Device Type (0x1000:00) to verify node is alive
104+
async fn health_check_task(
105+
update_tx: Sender<Update>,
106+
node_handle: CANopenNodeHandle,
107+
) {
108+
let mut interval = tokio::time::interval(Duration::from_secs(2));
109+
let mut consecutive_failures = 0;
110+
const MAX_FAILURES: u32 = 2; // Mark disconnected after 2 consecutive failures
111+
112+
loop {
113+
interval.tick().await;
114+
115+
// Read mandatory Device Type object (0x1000:00)
116+
let request = SdoRequest {
117+
node_id: node_handle.node_id(),
118+
index: 0x1000,
119+
subindex: 0x00,
120+
expected_type: SdoDataType::UInt32,
121+
};
122+
123+
match node_handle.sdo_read(request).await {
124+
Ok(_) => {
125+
// Node is alive
126+
consecutive_failures = 0;
127+
let _ = update_tx.send(Update::ConnectionStatus(true));
128+
},
129+
Err(err) => {
130+
// Node not responding
131+
consecutive_failures += 1;
132+
if consecutive_failures >= MAX_FAILURES {
133+
println!("Health check failed: {}", err);
134+
let _ = update_tx.send(Update::ConnectionStatus(false));
135+
let _ = update_tx.send(Update::ConnectionFailed(
136+
format!("Node not responding: {}", err)
137+
));
138+
}
139+
}
140+
}
141+
}
142+
}
143+
94144
// Make the main function for the thread public as well.
95145
pub fn communication_thread_main(
96146
command_rx: Receiver<Command>,
@@ -102,6 +152,7 @@ pub fn communication_thread_main(
102152

103153
let rt = tokio::runtime::Runtime::new().unwrap();
104154
let mut subscription_handles: HashMap<SdoAddress, JoinHandle<()>> = HashMap::new();
155+
let mut _health_check_handle: Option<JoinHandle<()>> = None;
105156
// Keep connection alive - it owns the background CAN reader task
106157
let mut _connection_handle: Option<CANopenConnection> = None;
107158
let mut node_handle: Option<CANopenNodeHandle> = None;
@@ -117,7 +168,17 @@ pub fn communication_thread_main(
117168
}){
118169
Ok((conn, handle)) => {
119170
_connection_handle = Some(conn);
120-
node_handle = Some(handle);
171+
node_handle = Some(handle.clone());
172+
173+
// Spawn health check task to monitor node
174+
let update_tx_clone = update_tx.clone();
175+
let health_handle = rt.spawn(health_check_task(
176+
update_tx_clone,
177+
handle,
178+
));
179+
_health_check_handle = Some(health_handle);
180+
181+
println!("Connection established, health check started");
121182
},
122183
Err(err) => {
123184
let _ = update_tx.send(Update::ConnectionFailed(err.to_string()));

canopen-viewer/src/main.rs

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ struct MyApp {
4646
update_rx: Option<Receiver<Update>>,
4747

4848
connection_status: bool,
49+
connection_requested: bool,
4950

5051
sdo_requested : bool,
5152
sdo_data : Option<BTreeMap<u16, SdoObject>>,
@@ -57,7 +58,10 @@ struct MyApp {
5758
modal_open_for: Option<SdoAddress>,
5859
modal_interval_str: String,
5960

60-
sdo_search_query: String
61+
sdo_search_query: String,
62+
63+
// Error reporting
64+
error_message: Option<String>,
6165
}
6266

6367

@@ -75,6 +79,7 @@ impl Default for MyApp {
7579
update_rx: None,
7680

7781
connection_status: false,
82+
connection_requested: false,
7883

7984
sdo_requested: false,
8085
sdo_data: None,
@@ -84,7 +89,9 @@ impl Default for MyApp {
8489
modal_open_for: None,
8590
modal_interval_str: String::new(),
8691

87-
sdo_search_query: String::new()
92+
sdo_search_query: String::new(),
93+
94+
error_message: None,
8895
}
8996
}
9097
}
@@ -112,6 +119,16 @@ impl eframe::App for MyApp {
112119
}
113120
}
114121
}
122+
Update::ConnectionFailed(error) => {
123+
self.error_message = Some(format!("Connection Error: {}", error));
124+
self.connection_status = false;
125+
}
126+
Update::ConnectionStatus(is_alive) => {
127+
self.connection_status = is_alive;
128+
}
129+
Update::SdoReadError { address, error } => {
130+
self.error_message = Some(format!("SDO Read Error [{:#06X}:{:02X}]: {}", address.index, address.sub_index, error));
131+
}
115132
_ => {
116133

117134
}
@@ -305,21 +322,56 @@ impl MyApp {
305322

306323
/// Draws the main application view.
307324
fn draw_main_view(&mut self, ui: &mut egui::Ui) {
308-
if !self.connection_status {
325+
// Request connection only once at startup
326+
if !self.connection_requested {
309327
if let Some(tx) = &self.command_tx {
310328
tx.send(Command::Connect).unwrap();
311329
}
312-
self.connection_status = true;
330+
self.connection_requested = true;
313331
}
314-
315-
332+
316333
if !self.sdo_requested {
317334
if let Some(tx) = &self.command_tx {
318335
tx.send(Command::FetchSdos).unwrap();
319336
self.sdo_requested = true;
320337
}
321338
}
322339

340+
// Top panel for status and error display
341+
egui::TopBottomPanel::top("status_panel").show_inside(ui, |ui| {
342+
ui.horizontal(|ui| {
343+
// Connection status indicator
344+
let status_color = if self.connection_status {
345+
Color32::from_rgb(0, 200, 0) // Green
346+
} else {
347+
Color32::from_rgb(200, 0, 0) // Red
348+
};
349+
let status_text = if self.connection_status { "● Connected" } else { "● Disconnected" };
350+
ui.colored_label(status_color, status_text);
351+
352+
ui.separator();
353+
354+
// Show interface and node ID info
355+
if let Some(interface) = &self.selected_can_interface {
356+
ui.label(format!("Interface: {}", interface));
357+
}
358+
if let Some(node_id) = self.selected_node_id {
359+
ui.label(format!("Node ID: {}", node_id));
360+
}
361+
});
362+
363+
// Error banner
364+
if let Some(error_msg) = self.error_message.clone() {
365+
ui.separator();
366+
ui.horizontal(|ui| {
367+
ui.colored_label(Color32::from_rgb(255, 100, 100), format!("⚠ {}", error_msg));
368+
if ui.button("✖").clicked() {
369+
self.error_message = None; // Clear error on click
370+
}
371+
});
372+
}
373+
});
374+
323375
// Creating panels. Left panel for SDO data, right panel for graphing.
324376
egui::SidePanel::left("sdo_list_panel").show_inside(ui, |ui| {
325377
self.draw_sdo_list(ui);

0 commit comments

Comments
 (0)