Skip to main content

canopen_host/
transport.rs

1//! Linux SocketCAN transport for `canopen-rs`.
2//!
3//! Bridges the CANopen core codecs to a Linux SocketCAN interface. The
4//! [`socketcan`] crate's [`CanFrame`](socketcan::CanFrame) implements the
5//! [`embedded_can`] traits, so the core's frame helpers
6//! ([`canopen_rs::transport`]) turn COB-IDs and payloads straight into bus
7//! traffic.
8//!
9//! [`SocketCan::open`](crate::transport::SocketCan::open) binds a named
10//! interface (e.g. `"can0"`, or `"vcan0"` for a virtual bus). Beyond raw
11//! [`SocketCan::send`](crate::transport::SocketCan::send) /
12//! [`SocketCan::recv`](crate::transport::SocketCan::recv), the
13//! [`SocketCan::sdo_read`](crate::transport::SocketCan::sdo_read) and
14//! [`SocketCan::sdo_write`](crate::transport::SocketCan::sdo_write) helpers run
15//! a whole SDO transaction — expedited or segmented — against a remote node in
16//! one call.
17//!
18//! [`socketcan`]: https://docs.rs/socketcan
19
20use std::fmt;
21use std::io;
22use std::time::Duration;
23
24use embedded_can::Frame as _;
25use socketcan::{CanFrame, CanSocket, Socket};
26
27use canopen_rs::datatypes::{DataType, Value};
28use canopen_rs::object_dictionary::Address;
29use canopen_rs::sdo::{SdoClient, SdoEvent};
30use canopen_rs::transport::{cob_id, frame_from};
31use canopen_rs::types::NodeId;
32
33/// A CANopen frame received from the bus: its COB-ID and up to eight bytes.
34#[derive(Debug, Clone)]
35pub struct Received {
36    /// The frame's COB-ID (11-bit standard identifier).
37    pub cob_id: u16,
38    data: [u8; 8],
39    len: usize,
40}
41
42impl Received {
43    /// The received bytes, trimmed to the frame's data length.
44    pub fn data(&self) -> &[u8] {
45        &self.data[..self.len]
46    }
47
48    /// The full eight-byte payload, zero-padded — convenient for the
49    /// fixed-size SDO codecs, which expect a `&[u8; 8]`.
50    pub fn payload(&self) -> &[u8; 8] {
51        &self.data
52    }
53}
54
55/// An error from an SDO transaction over the bus.
56#[derive(Debug)]
57pub enum SdoError {
58    /// An I/O error on the socket (including a read timeout).
59    Io(io::Error),
60    /// The server (or client) aborted with this raw SDO abort code.
61    Aborted(u32),
62    /// The transfer completed without yielding an expected value.
63    NoValue,
64}
65
66impl fmt::Display for SdoError {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            SdoError::Io(e) => write!(f, "SDO transport I/O error: {e}"),
70            SdoError::Aborted(code) => write!(f, "SDO transfer aborted (code {code:#010x})"),
71            SdoError::NoValue => f.write_str("SDO transfer completed without a value"),
72        }
73    }
74}
75
76impl std::error::Error for SdoError {
77    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
78        match self {
79            SdoError::Io(e) => Some(e),
80            _ => None,
81        }
82    }
83}
84
85impl From<io::Error> for SdoError {
86    fn from(e: io::Error) -> Self {
87        SdoError::Io(e)
88    }
89}
90
91/// A CANopen transport over a Linux SocketCAN interface.
92#[derive(Debug)]
93pub struct SocketCan {
94    socket: CanSocket,
95}
96
97impl SocketCan {
98    /// Open the named CAN interface (e.g. `"can0"` or `"vcan0"`).
99    pub fn open(interface: &str) -> io::Result<Self> {
100        Ok(Self {
101            socket: CanSocket::open(interface)?,
102        })
103    }
104
105    /// Set a read timeout, so [`SocketCan::recv`] (and the SDO helpers) fail
106    /// with a timeout error rather than blocking forever.
107    pub fn set_read_timeout(&self, timeout: Duration) -> io::Result<()> {
108        self.socket.set_read_timeout(timeout)
109    }
110
111    /// Put the socket into (non-)blocking mode.
112    pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
113        self.socket.set_nonblocking(nonblocking)
114    }
115
116    /// Transmit `data` on COB-ID `cob_id` as a standard data frame.
117    pub fn send(&self, cob_id: u16, data: &[u8]) -> io::Result<()> {
118        let frame: CanFrame = frame_from(cob_id, data).ok_or_else(|| {
119            io::Error::new(
120                io::ErrorKind::InvalidInput,
121                "invalid COB-ID or over-long data",
122            )
123        })?;
124        self.socket.write_frame(&frame)
125    }
126
127    /// Receive the next CANopen data frame, skipping remote and error frames
128    /// and any frame with a 29-bit extended identifier.
129    pub fn recv(&self) -> io::Result<Received> {
130        loop {
131            let frame = self.socket.read_frame()?;
132            if !frame.is_data_frame() {
133                continue;
134            }
135            let Some(cob) = cob_id(&frame) else { continue };
136            let bytes = frame.data();
137            let mut data = [0u8; 8];
138            data[..bytes.len()].copy_from_slice(bytes);
139            return Ok(Received {
140                cob_id: cob,
141                data,
142                len: bytes.len(),
143            });
144        }
145    }
146
147    /// Receive frames until one arrives on `cob_id`, discarding the rest.
148    fn recv_on(&self, cob_id: u16) -> io::Result<Received> {
149        loop {
150            let frame = self.recv()?;
151            if frame.cob_id == cob_id {
152                return Ok(frame);
153            }
154        }
155    }
156
157    /// Read object `addr` from `node`, interpreting the result as `data_type`.
158    ///
159    /// Runs the full SDO upload transaction (expedited or segmented) and
160    /// returns the value, or an [`SdoError`] on abort or I/O failure. Set a
161    /// read timeout first so an unresponsive node cannot block forever.
162    pub fn sdo_read(
163        &self,
164        node: NodeId,
165        addr: Address,
166        data_type: DataType,
167    ) -> Result<Value, SdoError> {
168        let mut client = SdoClient::new(node);
169        let mut request = client.read(addr, data_type);
170        loop {
171            self.send(client.request_cob_id(), &request)?;
172            let reply = self.recv_on(client.response_cob_id())?;
173            match client.on_response(reply.payload()) {
174                SdoEvent::Send(next) => request = next,
175                SdoEvent::Complete(value) => return value.ok_or(SdoError::NoValue),
176                SdoEvent::Aborted(code) => return Err(SdoError::Aborted(code)),
177            }
178        }
179    }
180
181    /// Write `value` to object `addr` on `node`.
182    ///
183    /// Runs the full SDO download transaction (expedited or segmented),
184    /// choosing the transfer type from the value's size.
185    pub fn sdo_write(&self, node: NodeId, addr: Address, value: Value) -> Result<(), SdoError> {
186        let mut client = SdoClient::new(node);
187        let mut request = client.write(addr, value);
188        loop {
189            self.send(client.request_cob_id(), &request)?;
190            let reply = self.recv_on(client.response_cob_id())?;
191            match client.on_response(reply.payload()) {
192                SdoEvent::Send(next) => request = next,
193                SdoEvent::Complete(_) => return Ok(()),
194                SdoEvent::Aborted(code) => return Err(SdoError::Aborted(code)),
195            }
196        }
197    }
198}