Skip to main content

vcan_loopback/
vcan_loopback.rs

1//! End-to-end CANopen over a Linux `vcan0` virtual CAN bus — the whole stack on
2//! a real bus (no hardware needed).
3//!
4//! A server thread runs a device [`Node`] that comes up **unconfigured**; the
5//! main thread is a master that walks it through the full lifecycle:
6//!
7//! 1. **LSS** — assign the node its id (`0x10`) over the bus, then reset it.
8//! 2. **SDO** — read/write objects (expedited and segmented).
9//! 3. **NMT** — start the node into operational.
10//! 4. **PDO** — drive an RPDO in and a SYNC-triggered TPDO out.
11//! 5. **Block transfer** — stream a 50-byte block download and CRC-verify it.
12//!
13//! Set up the interface first (see `tools/vcan_setup.sh`), then:
14//!
15//! ```text
16//! cargo run -p canopen-host --example vcan_loopback
17//! ```
18
19fn main() {
20    #[cfg(target_os = "linux")]
21    {
22        if let Err(e) = linux::run() {
23            eprintln!("vcan_loopback FAILED: {e}");
24            std::process::exit(1);
25        }
26    }
27    #[cfg(not(target_os = "linux"))]
28    {
29        eprintln!("This example requires Linux SocketCAN (vcan0); see tools/vcan_setup.sh.");
30    }
31}
32
33#[cfg(target_os = "linux")]
34mod linux {
35    use std::error::Error;
36    use std::sync::mpsc;
37    use std::thread;
38    use std::time::Duration;
39
40    use canopen_host::transport::{Received, SocketCan};
41    use canopen_rs::lss::{
42        self, encode_configure_node_id, encode_store, encode_switch_global, LssAddress,
43    };
44    use canopen_rs::nmt::NMT_COMMAND_COB_ID;
45    use canopen_rs::node::{Node, MAX_PDO_MAPPING};
46    use canopen_rs::sdo::block::{
47        self, decode_end, decode_sub_segment, encode_download_initiate, BlockReceiver, BlockWriter,
48    };
49    use canopen_rs::sync::SYNC_COB_ID;
50    use canopen_rs::{
51        Address, DataType, Entry, MappingEntry, NmtCommand, NodeId, ObjectDictionary, PdoMapping,
52        TransmissionType, Value,
53    };
54
55    const IFACE: &str = "vcan0";
56    // A private channel for the block-transfer demonstration (any unused ids).
57    const BLOCK_REQ: u16 = 0x123;
58    const BLOCK_RESP: u16 = 0x124;
59
60    pub fn run() -> Result<(), Box<dyn Error>> {
61        let (ready_tx, ready_rx) = mpsc::channel::<()>();
62        thread::spawn(move || {
63            if let Err(e) = serve(ready_tx) {
64                eprintln!("server thread error: {e}");
65            }
66        });
67        ready_rx.recv().map_err(|_| "server failed to start")?;
68
69        let bus = SocketCan::open(IFACE)?;
70        bus.set_read_timeout(Duration::from_secs(2))?;
71
72        // --- 1. LSS: assign the unconfigured node its id (0x10), then reset. ---
73        bus.send(lss::LSS_MASTER_COB_ID, &encode_switch_global(true))?;
74        bus.send(lss::LSS_MASTER_COB_ID, &encode_configure_node_id(0x10))?;
75        let cfg = recv_cob(&bus, lss::LSS_SLAVE_COB_ID)?;
76        assert_eq!(&cfg.data()[..2], &[0x11, 0x00]); // configure success
77        bus.send(lss::LSS_MASTER_COB_ID, &encode_store())?;
78        recv_cob(&bus, lss::LSS_SLAVE_COB_ID)?; // store response
79        bus.send(lss::LSS_MASTER_COB_ID, &encode_switch_global(false))?;
80        bus.send_nmt(NmtCommand::ResetCommunication, NodeId::BROADCAST)?;
81        let bootup = recv_cob(&bus, 0x700 + 0x10)?; // node boots on 0x710
82        println!(
83            "LSS: assigned node-id 0x10; node booted (bootup {:02X?})",
84            bootup.data()
85        );
86
87        let node = NodeId::new(0x10)?;
88
89        // --- 2. SDO: expedited and segmented. ---
90        let device_type = bus.sdo_read(node, Address::new(0x1000, 0), DataType::Unsigned32)?;
91        println!("SDO: read 0x1000 device type   = {device_type:?}");
92        assert_eq!(device_type, Value::Unsigned32(0x0004_0192));
93        bus.sdo_write(node, Address::new(0x1017, 0), Value::Unsigned16(2500))?;
94        assert_eq!(
95            bus.sdo_read(node, Address::new(0x1017, 0), DataType::Unsigned16)?,
96            Value::Unsigned16(2500)
97        );
98        let big = Value::Unsigned64(0x0102_0304_0506_0708);
99        bus.sdo_write(node, Address::new(0x2000, 0), big)?; // segmented
100        assert_eq!(
101            bus.sdo_read(node, Address::new(0x2000, 0), DataType::Unsigned64)?,
102            big
103        );
104        println!("SDO: expedited + segmented read/write OK");
105
106        // --- 3+4. NMT start, then RPDO in -> SYNC -> TPDO out. ---
107        bus.send_nmt(NmtCommand::StartRemoteNode, node)?;
108        bus.send(0x200 + 0x10, &[0xCD, 0xAB])?; // RPDO1 -> 0x6000/1 = 0xABCD
109        bus.send(SYNC_COB_ID, &[])?;
110        let tpdo = recv_cob(&bus, 0x180 + 0x10)?;
111        assert_eq!(tpdo.data(), &[0xCD, 0xAB]);
112        println!(
113            "NMT+PDO: RPDO in 0xABCD -> SYNC -> TPDO out {:02X?}",
114            tpdo.data()
115        );
116
117        // --- 5. Block transfer: stream 50 bytes, server CRC-verifies. ---
118        let payload: [u8; 50] = core::array::from_fn(|i| i as u8);
119        bus.send(
120            BLOCK_REQ,
121            &encode_download_initiate(Address::new(0x3000, 0), Some(payload.len() as u32), true),
122        )?;
123        let mut writer = BlockWriter::new(&payload, block::MAX_BLKSIZE);
124        while let Some(segment) = writer.next_segment() {
125            bus.send(BLOCK_REQ, &segment)?;
126        }
127        bus.send(BLOCK_REQ, &writer.end_frame(true))?;
128        let confirm = recv_cob(&bus, BLOCK_RESP)?;
129        assert_eq!(confirm.data(), &[1], "server CRC-verified the block");
130        println!(
131            "BLOCK: downloaded {} bytes -> server CRC-verified",
132            payload.len()
133        );
134
135        println!("\nvcan0 loopback OK — LSS, SDO, NMT, PDO, and block transfer all round-tripped.");
136        Ok(())
137    }
138
139    /// Receive frames until one arrives on `cob_id`.
140    fn recv_cob(bus: &SocketCan, cob_id: u16) -> Result<Received, Box<dyn Error>> {
141        loop {
142            let frame = bus.recv()?;
143            if frame.cob_id == cob_id {
144                return Ok(frame);
145            }
146        }
147    }
148
149    /// The device node: unconfigured until LSS assigns an id, then a full node.
150    fn serve(ready: mpsc::Sender<()>) -> Result<(), Box<dyn Error>> {
151        let bus = SocketCan::open(IFACE)?;
152        bus.set_read_timeout(Duration::from_secs(3))?;
153
154        let mut od = ObjectDictionary::<8>::new();
155        od.insert(
156            Address::new(0x1000, 0),
157            Entry::constant(Value::Unsigned32(0x0004_0192)),
158        )?;
159        od.insert(Address::new(0x1017, 0), Entry::rw(Value::Unsigned16(1000)))?;
160        od.insert(Address::new(0x2000, 0), Entry::rw(Value::Unsigned64(0)))?;
161        od.insert(Address::new(0x6000, 1), Entry::rw(Value::Unsigned16(0)))?;
162
163        // Provisional id 1 while unconfigured; LSS assigns the real one.
164        let mut node = Node::new(NodeId::new(1)?, od);
165        node.enable_lss(LssAddress {
166            vendor_id: 0x1F,
167            product_code: 0x2A,
168            revision_number: 1,
169            serial_number: 0x99,
170        });
171        ready.send(()).map_err(|_| "client went away")?;
172
173        let mut block_rx = BlockReceiver::<256>::new();
174        let mut booted = false;
175
176        while let Ok(frame) = bus.recv() {
177            if frame.cob_id == BLOCK_REQ {
178                handle_block(&bus, &mut block_rx, &frame)?;
179                continue;
180            }
181            if let Some(tx) = node.on_frame(frame.cob_id, frame.data()) {
182                bus.send(tx.cob_id, tx.data())?;
183            }
184            // The reset that follows LSS configuration: adopt the id and boot.
185            if !booted && frame.cob_id == NMT_COMMAND_COB_ID {
186                node.apply_lss_node_id();
187                let id = node.node_id().raw() as u16;
188                node.add_rpdo(0x200 + id, single_map(0x6000, 1, 16))?;
189                node.add_tpdo(
190                    0x180 + id,
191                    single_map(0x6000, 1, 16),
192                    TransmissionType::SynchronousAcyclic,
193                )?;
194                let boot = node.boot();
195                bus.send(boot.cob_id, boot.data())?;
196                booted = true;
197            }
198            if frame.cob_id == SYNC_COB_ID {
199                for tx in node.sync_tpdos() {
200                    bus.send(tx.cob_id, tx.data())?;
201                }
202            }
203        }
204        Ok(())
205    }
206
207    /// Reassemble a streamed block download and CRC-verify it on the end frame.
208    fn handle_block(
209        bus: &SocketCan,
210        rx: &mut BlockReceiver<256>,
211        frame: &Received,
212    ) -> Result<(), Box<dyn Error>> {
213        let p = pad8(frame.data());
214        let cmd = p[0];
215        if cmd & 0xE0 == 0xC0 && cmd & 0x01 == 0 {
216            *rx = BlockReceiver::new(); // initiate: start fresh
217        } else if cmd & 0xE0 == 0xC0 && cmd & 0x01 == 1 {
218            let (unused, crc) = decode_end(&p)?;
219            let ok = rx.finish(unused, crc, true).is_ok();
220            bus.send(BLOCK_RESP, &[ok as u8])?; // one confirmation frame
221        } else {
222            rx.push(&decode_sub_segment(&p))?; // a sub-block segment
223        }
224        Ok(())
225    }
226
227    fn pad8(data: &[u8]) -> [u8; 8] {
228        let mut p = [0u8; 8];
229        let n = data.len().min(8);
230        p[..n].copy_from_slice(&data[..n]);
231        p
232    }
233
234    fn single_map(index: u16, sub: u8, bits: u8) -> PdoMapping<MAX_PDO_MAPPING> {
235        let mut m = PdoMapping::new();
236        m.push(MappingEntry::new(index, sub, bits))
237            .expect("one entry fits");
238        m
239    }
240}