Skip to main content

canopen_rs/sdo/
client.rs

1//! SDO client โ€” drives read (upload) and write (download) transactions.
2//!
3//! [`SdoClient`] is a sans-I/O state machine. Start a transaction with
4//! [`SdoClient::read`] or [`SdoClient::write`] โ€” each returns the first request
5//! frame to transmit โ€” then feed every server response to
6//! [`SdoClient::on_response`], which returns the next [`SdoEvent`]: another
7//! frame to send, the completed result, or an abort. The client picks
8//! expedited or segmented transfer automatically from the value size, so
9//! callers need not care which is used.
10//!
11//! ```no_run
12//! # use canopen_rs::sdo::{SdoClient, SdoEvent};
13//! # use canopen_rs::{NodeId, Address, DataType};
14//! # fn bus_send(_cob: u16, _f: &[u8; 8]) {}
15//! # fn bus_recv() -> [u8; 8] { [0; 8] }
16//! let mut client = SdoClient::new(NodeId::new(0x10).unwrap());
17//! let mut frame = client.read(Address::new(0x1000, 0), DataType::Unsigned32);
18//! loop {
19//!     bus_send(client.request_cob_id(), &frame);
20//!     match client.on_response(&bus_recv()) {
21//!         SdoEvent::Send(next) => frame = next,
22//!         SdoEvent::Complete(value) => break, // value: Option<Value>
23//!         SdoEvent::Aborted(code) => break,
24//!     }
25//! }
26//! ```
27
28use heapless::Vec;
29
30use super::{
31    decode_abort, decode_data_segment, decode_download_response, decode_download_segment_response,
32    decode_upload_expedited_response, decode_upload_initiate_segmented_response,
33    encode_data_segment, encode_download_expedited, encode_download_initiate_segmented,
34    encode_upload_request, encode_upload_segment_request, request_cob_id, response_cob_id,
35    SdoPayload, SEGMENT_DATA_MAX,
36};
37use crate::datatypes::{DataType, Value};
38use crate::object_dictionary::Address;
39use crate::types::NodeId;
40
41/// The abort code for "toggle bit not alternated" (CiA 301 ยง7.2.4.3.17), used
42/// when the client itself detects an out-of-sequence segment.
43const ABORT_TOGGLE: u32 = 0x0503_0000;
44/// The abort code for a general internal error.
45const ABORT_GENERAL: u32 = 0x0800_0000;
46
47/// The outcome of feeding a response to [`SdoClient::on_response`].
48#[derive(Debug, Clone, PartialEq)]
49pub enum SdoEvent {
50    /// Transmit this frame, then feed the reply back to [`SdoClient::on_response`].
51    Send(SdoPayload),
52    /// The transaction finished. A read yields `Some(value)`; a write `None`.
53    Complete(Option<Value>),
54    /// The transaction was aborted with this raw SDO abort code.
55    Aborted(u32),
56}
57
58#[derive(Debug)]
59enum State {
60    Idle,
61    /// Awaiting the response to an expedited download (write).
62    DownloadExpedited,
63    /// Awaiting the segmented-download initiate response, then sending segments.
64    DownloadInit {
65        data: [u8; 8],
66        len: usize,
67    },
68    /// Awaiting the acknowledgement of the last-sent segment.
69    DownloadSeg {
70        data: [u8; 8],
71        len: usize,
72        pos: usize,
73        last_toggle: bool,
74    },
75    /// Awaiting the upload initiate response.
76    UploadInit {
77        data_type: DataType,
78    },
79    /// Awaiting the next upload data segment; `toggle` is the polled toggle.
80    UploadSeg {
81        data_type: DataType,
82        buf: Vec<u8, 8>,
83        toggle: bool,
84    },
85}
86
87/// An SDO client bound to the target node's id.
88#[derive(Debug)]
89pub struct SdoClient {
90    node: NodeId,
91    state: State,
92}
93
94impl SdoClient {
95    /// Create a client targeting `node`.
96    pub const fn new(node: NodeId) -> Self {
97        Self {
98            node,
99            state: State::Idle,
100        }
101    }
102
103    /// The COB-ID the client transmits requests on (`0x600 + node`).
104    pub fn request_cob_id(&self) -> u16 {
105        request_cob_id(self.node)
106    }
107
108    /// The COB-ID the client receives responses on (`0x580 + node`).
109    pub fn response_cob_id(&self) -> u16 {
110        response_cob_id(self.node)
111    }
112
113    /// Whether a transaction is in progress.
114    pub fn is_busy(&self) -> bool {
115        !matches!(self.state, State::Idle)
116    }
117
118    /// Begin reading `addr`, interpreting the result as `data_type`. Returns the
119    /// first request frame to transmit.
120    pub fn read(&mut self, addr: Address, data_type: DataType) -> SdoPayload {
121        self.state = State::UploadInit { data_type };
122        encode_upload_request(addr)
123    }
124
125    /// Begin writing `value` to `addr`. Returns the first request frame to
126    /// transmit; expedited or segmented is chosen from the value's size.
127    pub fn write(&mut self, addr: Address, value: Value) -> SdoPayload {
128        let size = value.size();
129        if size <= 4 {
130            self.state = State::DownloadExpedited;
131            encode_download_expedited(addr, &value).expect("size <= 4")
132        } else {
133            let mut data = [0u8; 8];
134            value.encode_le(&mut data[..size]).expect("size <= 8");
135            self.state = State::DownloadInit { data, len: size };
136            encode_download_initiate_segmented(addr, size as u32)
137        }
138    }
139
140    /// Feed a server response frame and advance the transaction.
141    pub fn on_response(&mut self, resp: &SdoPayload) -> SdoEvent {
142        // Any response may be an abort; handle it uniformly first.
143        if let Ok((_, code)) = decode_abort(resp) {
144            self.state = State::Idle;
145            return SdoEvent::Aborted(code);
146        }
147        match core::mem::replace(&mut self.state, State::Idle) {
148            State::Idle => SdoEvent::Aborted(ABORT_GENERAL),
149            State::DownloadExpedited => self.on_download_expedited(resp),
150            State::DownloadInit { data, len } => self.on_download_init(resp, data, len),
151            State::DownloadSeg {
152                data,
153                len,
154                pos,
155                last_toggle,
156            } => self.on_download_seg(resp, data, len, pos, last_toggle),
157            State::UploadInit { data_type } => self.on_upload_init(resp, data_type),
158            State::UploadSeg {
159                data_type,
160                buf,
161                toggle,
162            } => self.on_upload_seg(resp, data_type, buf, toggle),
163        }
164    }
165
166    fn on_download_expedited(&mut self, resp: &SdoPayload) -> SdoEvent {
167        match decode_download_response(resp) {
168            Ok(_) => SdoEvent::Complete(None),
169            Err(_) => SdoEvent::Aborted(ABORT_GENERAL),
170        }
171    }
172
173    fn on_download_init(&mut self, resp: &SdoPayload, data: [u8; 8], len: usize) -> SdoEvent {
174        // The initiate response for a segmented download is the same frame as
175        // an expedited download response.
176        if decode_download_response(resp).is_err() {
177            return SdoEvent::Aborted(ABORT_GENERAL);
178        }
179        self.send_download_segment(data, len, 0, false)
180    }
181
182    fn on_download_seg(
183        &mut self,
184        resp: &SdoPayload,
185        data: [u8; 8],
186        len: usize,
187        pos: usize,
188        last_toggle: bool,
189    ) -> SdoEvent {
190        match decode_download_segment_response(resp) {
191            Ok(t) if t == last_toggle => {
192                if pos >= len {
193                    SdoEvent::Complete(None)
194                } else {
195                    self.send_download_segment(data, len, pos, !last_toggle)
196                }
197            }
198            Ok(_) => SdoEvent::Aborted(ABORT_TOGGLE),
199            Err(_) => SdoEvent::Aborted(ABORT_GENERAL),
200        }
201    }
202
203    /// Emit the next download data segment starting at `pos` with `toggle`, and
204    /// park in [`State::DownloadSeg`] awaiting its acknowledgement.
205    fn send_download_segment(
206        &mut self,
207        data: [u8; 8],
208        len: usize,
209        pos: usize,
210        toggle: bool,
211    ) -> SdoEvent {
212        let remaining = len - pos;
213        let n = remaining.min(SEGMENT_DATA_MAX);
214        let last = remaining <= SEGMENT_DATA_MAX;
215        let frame = encode_data_segment(&data[pos..pos + n], toggle, last).expect("1..=7");
216        self.state = State::DownloadSeg {
217            data,
218            len,
219            pos: pos + n,
220            last_toggle: toggle,
221        };
222        SdoEvent::Send(frame)
223    }
224
225    fn on_upload_init(&mut self, resp: &SdoPayload, data_type: DataType) -> SdoEvent {
226        // A small value arrives expedited; a large one starts a segmented read.
227        if let Ok((_, value)) = decode_upload_expedited_response(resp, data_type) {
228            return SdoEvent::Complete(Some(value));
229        }
230        if decode_upload_initiate_segmented_response(resp).is_ok() {
231            self.state = State::UploadSeg {
232                data_type,
233                buf: Vec::new(),
234                toggle: false,
235            };
236            return SdoEvent::Send(encode_upload_segment_request(false));
237        }
238        SdoEvent::Aborted(ABORT_GENERAL)
239    }
240
241    fn on_upload_seg(
242        &mut self,
243        resp: &SdoPayload,
244        data_type: DataType,
245        mut buf: Vec<u8, 8>,
246        toggle: bool,
247    ) -> SdoEvent {
248        let seg = match decode_data_segment(resp) {
249            Ok(s) => s,
250            Err(_) => return SdoEvent::Aborted(ABORT_GENERAL),
251        };
252        if seg.toggle != toggle {
253            return SdoEvent::Aborted(ABORT_TOGGLE);
254        }
255        if buf.extend_from_slice(seg.data).is_err() {
256            return SdoEvent::Aborted(ABORT_GENERAL);
257        }
258        if seg.last {
259            match Value::decode_le(data_type, &buf) {
260                Ok(value) => SdoEvent::Complete(Some(value)),
261                Err(_) => SdoEvent::Aborted(ABORT_GENERAL),
262            }
263        } else {
264            let next = !toggle;
265            self.state = State::UploadSeg {
266                data_type,
267                buf,
268                toggle: next,
269            };
270            SdoEvent::Send(encode_upload_segment_request(next))
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    fn client() -> SdoClient {
280        SdoClient::new(NodeId::new(0x10).unwrap())
281    }
282
283    #[test]
284    fn cob_ids_track_node() {
285        let c = client();
286        assert_eq!(c.request_cob_id(), 0x610);
287        assert_eq!(c.response_cob_id(), 0x590);
288    }
289
290    #[test]
291    fn read_emits_upload_request_then_completes_on_expedited_response() {
292        let mut c = client();
293        let req = c.read(Address::new(0x1000, 0), DataType::Unsigned32);
294        assert_eq!(req[0], 0x40); // upload initiate
295        assert!(c.is_busy());
296        // Server replies with an expedited upload response carrying 0x192.
297        let resp = super::super::encode_upload_expedited_response(
298            Address::new(0x1000, 0),
299            &Value::Unsigned32(0x192),
300        )
301        .unwrap();
302        assert_eq!(
303            c.on_response(&resp),
304            SdoEvent::Complete(Some(Value::Unsigned32(0x192)))
305        );
306        assert!(!c.is_busy());
307    }
308
309    #[test]
310    fn write_small_value_is_expedited() {
311        let mut c = client();
312        let req = c.write(Address::new(0x1017, 0), Value::Unsigned16(1234));
313        assert_eq!(req[0] & 0xE0, 0x20); // download initiate
314        assert_ne!(req[0] & 0x02, 0); // expedited
315        let resp = super::super::encode_download_response(Address::new(0x1017, 0));
316        assert_eq!(c.on_response(&resp), SdoEvent::Complete(None));
317    }
318
319    #[test]
320    fn abort_response_surfaces_code() {
321        let mut c = client();
322        c.read(Address::new(0x9999, 0), DataType::Unsigned32);
323        let abort = super::super::encode_abort(
324            Address::new(0x9999, 0),
325            super::super::SdoAbortCode::ObjectDoesNotExist,
326        );
327        assert_eq!(c.on_response(&abort), SdoEvent::Aborted(0x0602_0000));
328        assert!(!c.is_busy());
329    }
330}