Skip to main content

vcan_loopback/
vcan_loopback.rs

1//! End-to-end SDO over a Linux `vcan0` virtual CAN bus — the first time the
2//! SocketCAN transport actually executes on a bus (no hardware needed).
3//!
4//! A server thread exposes an object dictionary on `vcan0`; the main thread is
5//! a client that reads and writes it, exercising both expedited and segmented
6//! transfers. Because SocketCAN delivers every frame to all sockets bound to
7//! the interface, the two ends simply share `vcan0`.
8//!
9//! Set up the interface first (see `tools/vcan_setup.sh`), then:
10//!
11//! ```text
12//! cargo run -p canopen-host --example vcan_loopback
13//! ```
14
15fn main() {
16    #[cfg(target_os = "linux")]
17    {
18        if let Err(e) = linux::run() {
19            eprintln!("vcan_loopback FAILED: {e}");
20            std::process::exit(1);
21        }
22    }
23    #[cfg(not(target_os = "linux"))]
24    {
25        eprintln!("This example requires Linux SocketCAN (vcan0); see tools/vcan_setup.sh.");
26    }
27}
28
29#[cfg(target_os = "linux")]
30mod linux {
31    use std::error::Error;
32    use std::sync::mpsc;
33    use std::thread;
34    use std::time::Duration;
35
36    use canopen_host::transport::{Received, SocketCan};
37    use canopen_rs::node::{Node, MAX_PDO_MAPPING};
38    use canopen_rs::sync::SYNC_COB_ID;
39    use canopen_rs::{
40        Address, DataType, Entry, MappingEntry, NmtCommand, NodeId, ObjectDictionary, PdoMapping,
41        TransmissionType, Value,
42    };
43
44    const IFACE: &str = "vcan0";
45
46    pub fn run() -> Result<(), Box<dyn Error>> {
47        let node = NodeId::new(0x10)?;
48
49        // --- Server node: serve an object dictionary on vcan0 in a thread. ---
50        let (ready_tx, ready_rx) = mpsc::channel::<()>();
51        thread::spawn(move || {
52            if let Err(e) = serve(node, ready_tx) {
53                eprintln!("server thread error: {e}");
54            }
55        });
56        // Wait until the server socket is open and listening.
57        ready_rx.recv().map_err(|_| "server failed to start")?;
58
59        // --- Client: talk to the node over vcan0. ---
60        let bus = SocketCan::open(IFACE)?;
61        bus.set_read_timeout(Duration::from_secs(2))?;
62
63        // Expedited read (4-byte object).
64        let device_type = bus.sdo_read(node, Address::new(0x1000, 0), DataType::Unsigned32)?;
65        println!("read  0x1000 device type      = {device_type:?}");
66        assert_eq!(device_type, Value::Unsigned32(0x0004_0192));
67
68        // Expedited write then read-back (2-byte object).
69        bus.sdo_write(node, Address::new(0x1017, 0), Value::Unsigned16(2500))?;
70        let heartbeat = bus.sdo_read(node, Address::new(0x1017, 0), DataType::Unsigned16)?;
71        println!("write 0x1017 heartbeat -> read = {heartbeat:?}");
72        assert_eq!(heartbeat, Value::Unsigned16(2500));
73
74        // 8-byte object forces SEGMENTED transfer over the real bus.
75        let big = Value::Unsigned64(0x0102_0304_0506_0708);
76        bus.sdo_write(node, Address::new(0x2000, 0), big)?;
77        let back = bus.sdo_read(node, Address::new(0x2000, 0), DataType::Unsigned64)?;
78        println!("write 0x2000 (segmented) -> read = {back:?}");
79        assert_eq!(back, big);
80
81        // --- PDO: go operational, drive an RPDO in and a SYNC-triggered TPDO
82        //     out. RPDO1 (0x200+node) writes 0x6000/1; TPDO1 (0x180+node) maps
83        //     the same object, so a SYNC echoes back what the RPDO wrote. ---
84        let rpdo1 = 0x200 + node.raw() as u16;
85        let tpdo1 = 0x180 + node.raw() as u16;
86        bus.send_nmt(NmtCommand::StartRemoteNode, node)?; // -> operational
87        bus.send(rpdo1, &[0xCD, 0xAB])?; // RPDO1: 0x6000/1 <- 0xABCD
88        bus.send(SYNC_COB_ID, &[])?; // SYNC: node emits synchronous TPDOs
89
90        let tpdo = recv_cob(&bus, tpdo1)?;
91        println!("RPDO in 0xABCD -> SYNC -> TPDO out = {:02X?}", tpdo.data());
92        assert_eq!(tpdo.data(), &[0xCD, 0xAB]);
93
94        println!(
95            "\nvcan0 loopback OK — SDO (expedited + segmented), NMT, and PDO all round-tripped."
96        );
97        Ok(())
98    }
99
100    /// Receive frames until one arrives on `cob_id`.
101    fn recv_cob(bus: &SocketCan, cob_id: u16) -> Result<Received, Box<dyn Error>> {
102        loop {
103            let frame = bus.recv()?;
104            if frame.cob_id == cob_id {
105                return Ok(frame);
106            }
107        }
108    }
109
110    /// A device node: build an OD, configure a PDO pair, boot, and serve frames
111    /// (SDO, NMT, RPDO) plus SYNC-triggered TPDOs until the bus goes quiet.
112    fn serve(node_id: NodeId, ready: mpsc::Sender<()>) -> Result<(), Box<dyn Error>> {
113        let bus = SocketCan::open(IFACE)?;
114        bus.set_read_timeout(Duration::from_secs(3))?;
115
116        let mut od = ObjectDictionary::<8>::new();
117        od.insert(
118            Address::new(0x1000, 0),
119            Entry::constant(Value::Unsigned32(0x0004_0192)),
120        )?;
121        od.insert(Address::new(0x1017, 0), Entry::rw(Value::Unsigned16(1000)))?;
122        od.insert(Address::new(0x2000, 0), Entry::rw(Value::Unsigned64(0)))?;
123        od.insert(Address::new(0x6000, 1), Entry::rw(Value::Unsigned16(0)))?;
124
125        let mut node = Node::new(node_id, od);
126        // RPDO1 receives into 0x6000/1; TPDO1 transmits it back on SYNC.
127        node.add_rpdo(0x200 + node_id.raw() as u16, mapping(0x6000, 1, 16))?;
128        node.add_tpdo(
129            0x180 + node_id.raw() as u16,
130            mapping(0x6000, 1, 16),
131            TransmissionType::SynchronousAcyclic,
132        )?;
133        node.boot(); // enter pre-operational so SDO is served
134        ready.send(()).map_err(|_| "client went away")?;
135
136        // Serve until a read times out (the client is done and the bus is idle).
137        while let Ok(frame) = bus.recv() {
138            if let Some(tx) = node.on_frame(frame.cob_id, frame.data()) {
139                bus.send(tx.cob_id, tx.data())?;
140            }
141            if frame.cob_id == SYNC_COB_ID {
142                for tx in node.sync_tpdos() {
143                    bus.send(tx.cob_id, tx.data())?;
144                }
145            }
146        }
147        Ok(())
148    }
149
150    /// A single-object PDO mapping.
151    fn mapping(index: u16, subindex: u8, bits: u8) -> PdoMapping<MAX_PDO_MAPPING> {
152        let mut m = PdoMapping::new();
153        m.push(MappingEntry::new(index, subindex, bits))
154            .expect("one entry fits");
155        m
156    }
157}