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::nmt::{encode_command, NMT_COMMAND_COB_ID};
29use canopen_rs::object_dictionary::Address;
30use canopen_rs::sdo::{SdoClient, SdoEvent};
31use canopen_rs::transport::{cob_id, frame_from};
32use canopen_rs::types::NodeId;
33use canopen_rs::NmtCommand;
34
35/// A CANopen frame received from the bus: its COB-ID and up to eight bytes.
36#[derive(Debug, Clone)]
37pub struct Received {
38    /// The frame's COB-ID (11-bit standard identifier).
39    pub cob_id: u16,
40    data: [u8; 8],
41    len: usize,
42}
43
44impl Received {
45    /// Build a received frame from a COB-ID and its data bytes (clamped to 8).
46    pub(crate) fn new(cob_id: u16, bytes: &[u8]) -> Self {
47        let len = bytes.len().min(8);
48        let mut data = [0u8; 8];
49        data[..len].copy_from_slice(&bytes[..len]);
50        Self { cob_id, data, len }
51    }
52
53    /// The received bytes, trimmed to the frame's data length.
54    pub fn data(&self) -> &[u8] {
55        &self.data[..self.len]
56    }
57
58    /// The full eight-byte payload, zero-padded — convenient for the
59    /// fixed-size SDO codecs, which expect a `&[u8; 8]`.
60    pub fn payload(&self) -> &[u8; 8] {
61        &self.data
62    }
63}
64
65/// An error from an SDO transaction over the bus.
66#[derive(Debug)]
67pub enum SdoError {
68    /// An I/O error on the socket (including a read timeout).
69    Io(io::Error),
70    /// The server (or client) aborted with this raw SDO abort code.
71    Aborted(u32),
72    /// The transfer completed without yielding an expected value.
73    NoValue,
74}
75
76impl fmt::Display for SdoError {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            SdoError::Io(e) => write!(f, "SDO transport I/O error: {e}"),
80            SdoError::Aborted(code) => write!(f, "SDO transfer aborted (code {code:#010x})"),
81            SdoError::NoValue => f.write_str("SDO transfer completed without a value"),
82        }
83    }
84}
85
86impl std::error::Error for SdoError {
87    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
88        match self {
89            SdoError::Io(e) => Some(e),
90            _ => None,
91        }
92    }
93}
94
95impl From<io::Error> for SdoError {
96    fn from(e: io::Error) -> Self {
97        SdoError::Io(e)
98    }
99}
100
101/// Wrap a socket-open failure with the interface name and, when the interface
102/// does not exist (`ENODEV`), a hint that it may not be up.
103pub(crate) fn open_error(interface: &str, e: io::Error) -> io::Error {
104    // ENODEV (19) is what SocketCAN returns for a missing/downed interface.
105    let hint = if e.raw_os_error() == Some(19) {
106        format!(" — interface not found; is it up? (e.g. `sudo ip link set up {interface}`)")
107    } else {
108        String::new()
109    };
110    io::Error::new(
111        e.kind(),
112        format!("opening CAN interface '{interface}': {e}{hint}"),
113    )
114}
115
116/// A CANopen transport over a Linux SocketCAN interface.
117#[derive(Debug)]
118pub struct SocketCan {
119    socket: CanSocket,
120}
121
122impl SocketCan {
123    /// Open the named CAN interface (e.g. `"can0"` or `"vcan0"`).
124    ///
125    /// Fails with a message naming the interface — and hinting that it may not
126    /// be up — if it does not exist.
127    pub fn open(interface: &str) -> io::Result<Self> {
128        CanSocket::open(interface)
129            .map(|socket| Self { socket })
130            .map_err(|e| open_error(interface, e))
131    }
132
133    /// Set a read timeout, so [`SocketCan::recv`] (and the SDO helpers) fail
134    /// with a timeout error rather than blocking forever.
135    pub fn set_read_timeout(&self, timeout: Duration) -> io::Result<()> {
136        self.socket.set_read_timeout(timeout)
137    }
138
139    /// Put the socket into (non-)blocking mode.
140    pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
141        self.socket.set_nonblocking(nonblocking)
142    }
143
144    /// Transmit `data` on COB-ID `cob_id` as a standard data frame.
145    pub fn send(&self, cob_id: u16, data: &[u8]) -> io::Result<()> {
146        let frame: CanFrame = frame_from(cob_id, data).ok_or_else(|| {
147            io::Error::new(
148                io::ErrorKind::InvalidInput,
149                "invalid COB-ID or over-long data",
150            )
151        })?;
152        self.socket.write_frame(&frame)
153    }
154
155    /// Receive the next CANopen data frame, skipping remote and error frames
156    /// and any frame with a 29-bit extended identifier.
157    pub fn recv(&self) -> io::Result<Received> {
158        loop {
159            let frame = self.socket.read_frame()?;
160            if !frame.is_data_frame() {
161                continue;
162            }
163            let Some(cob) = cob_id(&frame) else { continue };
164            return Ok(Received::new(cob, frame.data()));
165        }
166    }
167
168    /// Send an NMT node-control command to `target` on COB-ID `0x000`.
169    ///
170    /// Use [`NodeId::BROADCAST`] to address every node at once — e.g.
171    /// `send_nmt(NmtCommand::StartRemoteNode, NodeId::BROADCAST)`.
172    pub fn send_nmt(&self, command: NmtCommand, target: NodeId) -> io::Result<()> {
173        self.send(NMT_COMMAND_COB_ID, &encode_command(command, target))
174    }
175
176    /// Receive frames until one arrives on `cob_id`, discarding the rest.
177    fn recv_on(&self, cob_id: u16) -> io::Result<Received> {
178        loop {
179            let frame = self.recv()?;
180            if frame.cob_id == cob_id {
181                return Ok(frame);
182            }
183        }
184    }
185
186    /// Read object `addr` from `node`, interpreting the result as `data_type`.
187    ///
188    /// Runs the full SDO upload transaction (expedited or segmented) and
189    /// returns the value, or an [`SdoError`] on abort or I/O failure. Set a
190    /// read timeout first so an unresponsive node cannot block forever.
191    pub fn sdo_read(
192        &self,
193        node: NodeId,
194        addr: Address,
195        data_type: DataType,
196    ) -> Result<Value, SdoError> {
197        let mut client = SdoClient::new(node);
198        let mut request = client.read(addr, data_type);
199        loop {
200            self.send(client.request_cob_id(), &request)?;
201            let reply = self.recv_on(client.response_cob_id())?;
202            match client.on_response(reply.payload()) {
203                SdoEvent::Send(next) => request = next,
204                SdoEvent::Complete(value) => return value.ok_or(SdoError::NoValue),
205                SdoEvent::Aborted(code) => return Err(SdoError::Aborted(code)),
206            }
207        }
208    }
209
210    /// Write `value` to object `addr` on `node`.
211    ///
212    /// Runs the full SDO download transaction (expedited or segmented),
213    /// choosing the transfer type from the value's size.
214    pub fn sdo_write(&self, node: NodeId, addr: Address, value: Value) -> Result<(), SdoError> {
215        let mut client = SdoClient::new(node);
216        let mut request = client.write(addr, value);
217        loop {
218            self.send(client.request_cob_id(), &request)?;
219            let reply = self.recv_on(client.response_cob_id())?;
220            match client.on_response(reply.payload()) {
221                SdoEvent::Send(next) => request = next,
222                SdoEvent::Complete(_) => return Ok(()),
223                SdoEvent::Aborted(code) => return Err(SdoError::Aborted(code)),
224            }
225        }
226    }
227}