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