Skip to content

Commit 639f258

Browse files
committed
Add support for TPDO
1 parent edea238 commit 639f258

6 files changed

Lines changed: 74 additions & 57 deletions

File tree

canopen-viewer/src/communication.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,20 @@ fn parse_tpdos_from_eds(eds_file: &PathBuf, sdo_data: &BTreeMap<u16, SdoObject>)
298298
let cob_id = match config.get(&comm_section, "DefaultValue") {
299299
Some(value_str) => {
300300
// Parse hex value (format: "0x184" or "$NODEID+0x180")
301-
let cleaned = value_str.replace("$NODEID", "0").replace("+", "");
302-
if let Ok(val) = if cleaned.starts_with("0x") {
303-
u32::from_str_radix(&cleaned[2..], 16)
301+
// For "$NODEID+0x180", we ignore $NODEID (treat as 0) and parse the hex part
302+
let to_parse = if value_str.contains("$NODEID+") {
303+
value_str.split("+").nth(1).unwrap_or("0")
304+
} else if value_str.contains("+") {
305+
// Handle "NODEID+0x180" without $ (some EDS formats)
306+
value_str.split("+").nth(1).unwrap_or("0")
304307
} else {
305-
cleaned.parse::<u32>()
308+
value_str.as_str()
309+
};
310+
311+
if let Ok(val) = if to_parse.starts_with("0x") || to_parse.starts_with("0X") {
312+
u32::from_str_radix(&to_parse[2..], 16)
313+
} else {
314+
to_parse.parse::<u32>()
306315
} {
307316
// Check valid bit (bit 31)
308317
if val & 0x80000000 != 0 {
@@ -311,7 +320,7 @@ fn parse_tpdos_from_eds(eds_file: &PathBuf, sdo_data: &BTreeMap<u16, SdoObject>)
311320
}
312321
(val & 0x7FF) as u16
313322
} else {
314-
println!("EDS: Failed to parse COB-ID for TPDO {}", tpdo_num);
323+
println!("EDS: Failed to parse COB-ID '{}' for TPDO {}", to_parse, tpdo_num);
315324
continue;
316325
}
317326
}

canopen-viewer/src/main.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,15 @@ impl MyApp {
818818
// 1. Use a Frame to visually group each plot and its title.
819819
egui::Frame::group(ui.style()).show(ui, |ui| {
820820
let plot_id = format!("sdo_plot_{:x}_{}", address.index, address.sub_index);
821-
let plot_title = format!("SDO {:#06X}:{}", address.index, address.sub_index);
821+
822+
// Get human-readable name from EDS
823+
let field_name = self.sdo_data.as_ref()
824+
.and_then(|sdo_map| sdo_map.get(&address.index))
825+
.and_then(|sdo_object| sdo_object.sub_objects.get(&address.sub_index))
826+
.map(|sub_object| sub_object.name.clone())
827+
.unwrap_or_else(|| format!("0x{:04X}:{:02X}", address.index, address.sub_index));
828+
829+
let plot_title = format!("SDO - {} ({:#06X}:{})", field_name, address.index, address.sub_index);
822830

823831
// Add a title for the individual plot.
824832
ui.label(&plot_title);
@@ -844,7 +852,7 @@ impl MyApp {
844852
let points_vec: Vec<[f64; 2]> = subscription.plot_data.iter().cloned().collect();
845853

846854
let line = Line::new(PlotPoints::from(points_vec))
847-
.name(&plot_title)
855+
.name(&field_name) // Use field name in legend (without hex address)
848856
.color(color);
849857

850858
plot_ui.line(line);

examples/Example.eds

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,34 +1420,34 @@ ObjectType=0x7
14201420
;StorageLocation=RAM
14211421
DataType=0x0005
14221422
AccessType=rw
1423-
DefaultValue=3
1423+
DefaultValue=2
14241424
PDOMapping=0
14251425

14261426
[1A00sub1]
1427-
ParameterName=Temperature
1427+
ParameterName=CabinTemperature
14281428
ObjectType=0x7
14291429
;StorageLocation=RAM
14301430
DataType=0x0007
14311431
AccessType=rw
1432-
DefaultValue=0x60000110
1432+
DefaultValue=0x20000120
14331433
PDOMapping=0
14341434

14351435
[1A00sub2]
1436-
ParameterName=Pressure
1436+
ParameterName=OutsideTemperature
14371437
ObjectType=0x7
14381438
;StorageLocation=RAM
14391439
DataType=0x0007
14401440
AccessType=rw
1441-
DefaultValue=0x60010110
1441+
DefaultValue=0x20000220
14421442
PDOMapping=0
14431443

14441444
[1A00sub3]
1445-
ParameterName=Status
1445+
ParameterName=Application object 3
14461446
ObjectType=0x7
14471447
;StorageLocation=RAM
14481448
DataType=0x0007
14491449
AccessType=rw
1450-
DefaultValue=0x60020108
1450+
DefaultValue=0x00000000
14511451
PDOMapping=0
14521452

14531453
[1A00sub4]
@@ -1788,7 +1788,7 @@ ObjectType=0x7
17881788
DataType=0x0008
17891789
AccessType=rw
17901790
DefaultValue=0
1791-
PDOMapping=0
1791+
PDOMapping=1
17921792

17931793
[2000sub2]
17941794
ParameterName=OutsideTemperature
@@ -1797,7 +1797,7 @@ ObjectType=0x7
17971797
DataType=0x0008
17981798
AccessType=rw
17991799
DefaultValue=0
1800-
PDOMapping=0
1800+
PDOMapping=1
18011801

18021802
[2001]
18031803
ParameterName=PressureReadings

mock-canopen-node/src/main.rs

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,12 @@ fn main() {
7979
println!("🚀 Mock node is running!");
8080
println!(" Waiting for SDO requests on COB-ID 0x{:03X}...", 0x600 + node_id as u16);
8181
println!(" Broadcasting TPDO1 on COB-ID 0x{:03X} every 100ms", 0x180 + node_id as u16);
82+
println!(" TPDO1 contains: CabinTemperature (0x2000:01), OutsideTemperature (0x2000:02)");
8283
println!(" Press Ctrl+C to stop\n");
8384

8485
// TPDO broadcasting state
8586
let mut last_tpdo_time = Instant::now();
8687
let tpdo_interval = Duration::from_millis(100);
87-
let mut temperature: u16 = 2350; // 23.50°C
88-
let mut pressure: u16 = 1013; // 1013 hPa
89-
let mut status: u8 = 1; // OK
9088

9189
// Main loop: listen for CAN frames and respond to SDO requests
9290
loop {
@@ -112,35 +110,36 @@ fn main() {
112110

113111
// Broadcast TPDO periodically
114112
if last_tpdo_time.elapsed() >= tpdo_interval {
115-
// Update test values (simulate changing sensor data)
116-
temperature = (temperature + 1) % 3000; // Varies from 0 to 30.00°C
117-
pressure = 1000 + (pressure - 1000 + 1) % 50; // Varies from 1000 to 1050 hPa
118-
status = if status == 1 { 2 } else { 1 }; // Toggle between 1 and 2
119-
120-
// Create TPDO frame
121-
// TPDO1 COB-ID = 0x180 + node_id
122-
// Mapping: Temperature (16-bit), Pressure (16-bit), Status (8-bit)
123-
let tpdo_cob_id = 0x180 + node_id as u16;
124-
125-
if let Some(std_id) = StandardId::new(tpdo_cob_id) {
126-
let mut data = [0u8; 8];
127-
128-
// Pack data in little-endian format
129-
data[0] = (temperature & 0xFF) as u8;
130-
data[1] = ((temperature >> 8) & 0xFF) as u8;
131-
data[2] = (pressure & 0xFF) as u8;
132-
data[3] = ((pressure >> 8) & 0xFF) as u8;
133-
data[4] = status;
134-
// Remaining bytes are padding (0)
135-
136-
if let Some(frame) = CanFrame::new(std_id, &data[..5]) {
137-
if let Err(e) = socket.write_frame(&frame) {
138-
eprintln!("⚠ Failed to send TPDO: {}", e);
139-
} else {
140-
print!("📤 TPDO1: Temp={:.2}°C, Press={}hPa, Status={}\r",
141-
temperature as f32 / 100.0, pressure, status);
142-
use std::io::Write;
143-
std::io::stdout().flush().ok();
113+
// Read current values from Object Dictionary
114+
// TPDO1 mapping: 0x2000:01 (CabinTemperature, Real32), 0x2000:02 (OutsideTemperature, Real32)
115+
let cabin_temp = sdo_server.object_dict().get(0x2000, 0x01);
116+
let outside_temp = sdo_server.object_dict().get(0x2000, 0x02);
117+
118+
if let (Some((cabin_data, _)), Some((outside_data, _))) = (cabin_temp, outside_temp) {
119+
// Create TPDO frame
120+
// TPDO1 COB-ID = 0x180 + node_id
121+
let tpdo_cob_id = 0x180 + node_id as u16;
122+
123+
if let Some(std_id) = StandardId::new(tpdo_cob_id) {
124+
let mut data = [0u8; 8];
125+
126+
// Pack data according to TPDO mapping
127+
// Bytes 0-3: CabinTemperature (Real32, little-endian)
128+
data[0..4].copy_from_slice(&cabin_data[..4]);
129+
// Bytes 4-7: OutsideTemperature (Real32, little-endian)
130+
data[4..8].copy_from_slice(&outside_data[..4]);
131+
132+
if let Some(frame) = CanFrame::new(std_id, &data) {
133+
if let Err(e) = socket.write_frame(&frame) {
134+
eprintln!("⚠ Failed to send TPDO: {}", e);
135+
} else {
136+
// Decode for display
137+
let cabin_f32 = f32::from_le_bytes([cabin_data[0], cabin_data[1], cabin_data[2], cabin_data[3]]);
138+
let outside_f32 = f32::from_le_bytes([outside_data[0], outside_data[1], outside_data[2], outside_data[3]]);
139+
print!("📤 TPDO1: CabinTemp={:.2}°C, OutsideTemp={:.2}°C\r", cabin_f32, outside_f32);
140+
use std::io::Write;
141+
std::io::stdout().flush().ok();
142+
}
144143
}
145144
}
146145
}

mock-canopen-node/src/object_dictionary.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -203,21 +203,17 @@ impl ObjectDictionary {
203203

204204
// TPDO1 Mapping Parameters (0x1A00)
205205
// 0x1A00:00 - Number of mapped objects (UInt8)
206-
self.add_static(0x1A00, 0x00, vec![0x03], SdoDataType::UInt8);
206+
self.add_static(0x1A00, 0x00, vec![0x02], SdoDataType::UInt8);
207207

208-
// 0x1A00:01 - Mapping entry 1: Temperature (0x6000:01, 16 bits)
208+
// 0x1A00:01 - Mapping entry 1: CabinTemperature (0x2000:01, 32 bits Real32)
209209
// Format: bits 31-16 = index, bits 15-8 = subindex, bits 7-0 = bit length
210-
let mapping1: u32 = (0x6000 << 16) | (0x01 << 8) | 16;
210+
let mapping1: u32 = (0x2000 << 16) | (0x01 << 8) | 32;
211211
self.add_static(0x1A00, 0x01, mapping1.to_le_bytes().to_vec(), SdoDataType::UInt32);
212212

213-
// 0x1A00:02 - Mapping entry 2: Pressure (0x6001:01, 16 bits)
214-
let mapping2: u32 = (0x6001 << 16) | (0x01 << 8) | 16;
213+
// 0x1A00:02 - Mapping entry 2: OutsideTemperature (0x2000:02, 32 bits Real32)
214+
let mapping2: u32 = (0x2000 << 16) | (0x02 << 8) | 32;
215215
self.add_static(0x1A00, 0x02, mapping2.to_le_bytes().to_vec(), SdoDataType::UInt32);
216216

217-
// 0x1A00:03 - Mapping entry 3: Status (0x6002:01, 8 bits)
218-
let mapping3: u32 = (0x6002 << 16) | (0x01 << 8) | 8;
219-
self.add_static(0x1A00, 0x03, mapping3.to_le_bytes().to_vec(), SdoDataType::UInt32);
220-
221217
// === TPDO Data Objects (synchronized with transmitted TPDO) ===
222218

223219
// 0x6000:01 - Temperature (UInt16) - Dynamic (same as transmitted in TPDO)

mock-canopen-node/src/sdo_server.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ impl SdoServer {
2121
}
2222
}
2323

24+
/// Get a reference to the object dictionary for TPDO data reads
25+
pub fn object_dict(&self) -> &ObjectDictionary {
26+
&self.object_dict
27+
}
28+
2429
/// Handle an incoming CAN frame
2530
/// Returns Some(response_frame) if this was an SDO request for us
2631
pub fn handle_frame(&mut self, frame: &CanFrame) -> Option<CanFrame> {

0 commit comments

Comments
 (0)