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, MAX_STRING_LEN};
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; MAX_STRING_LEN],
66        len: usize,
67    },
68    /// Awaiting the acknowledgement of the last-sent segment.
69    DownloadSeg {
70        data: [u8; MAX_STRING_LEN],
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, MAX_STRING_LEN>,
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; MAX_STRING_LEN];
134            value
135                .encode_le(&mut data[..size])
136                .expect("size <= MAX_STRING_LEN");
137            self.state = State::DownloadInit { data, len: size };
138            encode_download_initiate_segmented(addr, size as u32)
139        }
140    }
141
142    /// Feed a server response frame and advance the transaction.
143    pub fn on_response(&mut self, resp: &SdoPayload) -> SdoEvent {
144        // Any response may be an abort; handle it uniformly first.
145        if let Ok((_, code)) = decode_abort(resp) {
146            self.state = State::Idle;
147            return SdoEvent::Aborted(code);
148        }
149        match core::mem::replace(&mut self.state, State::Idle) {
150            State::Idle => SdoEvent::Aborted(ABORT_GENERAL),
151            State::DownloadExpedited => self.on_download_expedited(resp),
152            State::DownloadInit { data, len } => self.on_download_init(resp, data, len),
153            State::DownloadSeg {
154                data,
155                len,
156                pos,
157                last_toggle,
158            } => self.on_download_seg(resp, data, len, pos, last_toggle),
159            State::UploadInit { data_type } => self.on_upload_init(resp, data_type),
160            State::UploadSeg {
161                data_type,
162                buf,
163                toggle,
164            } => self.on_upload_seg(resp, data_type, buf, toggle),
165        }
166    }
167
168    fn on_download_expedited(&mut self, resp: &SdoPayload) -> SdoEvent {
169        match decode_download_response(resp) {
170            Ok(_) => SdoEvent::Complete(None),
171            Err(_) => SdoEvent::Aborted(ABORT_GENERAL),
172        }
173    }
174
175    fn on_download_init(
176        &mut self,
177        resp: &SdoPayload,
178        data: [u8; MAX_STRING_LEN],
179        len: usize,
180    ) -> SdoEvent {
181        // The initiate response for a segmented download is the same frame as
182        // an expedited download response.
183        if decode_download_response(resp).is_err() {
184            return SdoEvent::Aborted(ABORT_GENERAL);
185        }
186        self.send_download_segment(data, len, 0, false)
187    }
188
189    fn on_download_seg(
190        &mut self,
191        resp: &SdoPayload,
192        data: [u8; MAX_STRING_LEN],
193        len: usize,
194        pos: usize,
195        last_toggle: bool,
196    ) -> SdoEvent {
197        match decode_download_segment_response(resp) {
198            Ok(t) if t == last_toggle => {
199                if pos >= len {
200                    SdoEvent::Complete(None)
201                } else {
202                    self.send_download_segment(data, len, pos, !last_toggle)
203                }
204            }
205            Ok(_) => SdoEvent::Aborted(ABORT_TOGGLE),
206            Err(_) => SdoEvent::Aborted(ABORT_GENERAL),
207        }
208    }
209
210    /// Emit the next download data segment starting at `pos` with `toggle`, and
211    /// park in [`State::DownloadSeg`] awaiting its acknowledgement.
212    fn send_download_segment(
213        &mut self,
214        data: [u8; MAX_STRING_LEN],
215        len: usize,
216        pos: usize,
217        toggle: bool,
218    ) -> SdoEvent {
219        let remaining = len - pos;
220        let n = remaining.min(SEGMENT_DATA_MAX);
221        let last = remaining <= SEGMENT_DATA_MAX;
222        let frame = encode_data_segment(&data[pos..pos + n], toggle, last).expect("1..=7");
223        self.state = State::DownloadSeg {
224            data,
225            len,
226            pos: pos + n,
227            last_toggle: toggle,
228        };
229        SdoEvent::Send(frame)
230    }
231
232    fn on_upload_init(&mut self, resp: &SdoPayload, data_type: DataType) -> SdoEvent {
233        // A small value arrives expedited; a large one starts a segmented read.
234        if let Ok((_, value)) = decode_upload_expedited_response(resp, data_type) {
235            return SdoEvent::Complete(Some(value));
236        }
237        if decode_upload_initiate_segmented_response(resp).is_ok() {
238            self.state = State::UploadSeg {
239                data_type,
240                buf: Vec::new(),
241                toggle: false,
242            };
243            return SdoEvent::Send(encode_upload_segment_request(false));
244        }
245        SdoEvent::Aborted(ABORT_GENERAL)
246    }
247
248    fn on_upload_seg(
249        &mut self,
250        resp: &SdoPayload,
251        data_type: DataType,
252        mut buf: Vec<u8, MAX_STRING_LEN>,
253        toggle: bool,
254    ) -> SdoEvent {
255        let seg = match decode_data_segment(resp) {
256            Ok(s) => s,
257            Err(_) => return SdoEvent::Aborted(ABORT_GENERAL),
258        };
259        if seg.toggle != toggle {
260            return SdoEvent::Aborted(ABORT_TOGGLE);
261        }
262        if buf.extend_from_slice(seg.data).is_err() {
263            return SdoEvent::Aborted(ABORT_GENERAL);
264        }
265        if seg.last {
266            match Value::decode_le(data_type, &buf) {
267                Ok(value) => SdoEvent::Complete(Some(value)),
268                Err(_) => SdoEvent::Aborted(ABORT_GENERAL),
269            }
270        } else {
271            let next = !toggle;
272            self.state = State::UploadSeg {
273                data_type,
274                buf,
275                toggle: next,
276            };
277            SdoEvent::Send(encode_upload_segment_request(next))
278        }
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    fn client() -> SdoClient {
287        SdoClient::new(NodeId::new(0x10).unwrap())
288    }
289
290    #[test]
291    fn cob_ids_track_node() {
292        let c = client();
293        assert_eq!(c.request_cob_id(), 0x610);
294        assert_eq!(c.response_cob_id(), 0x590);
295    }
296
297    #[test]
298    fn read_emits_upload_request_then_completes_on_expedited_response() {
299        let mut c = client();
300        let req = c.read(Address::new(0x1000, 0), DataType::Unsigned32);
301        assert_eq!(req[0], 0x40); // upload initiate
302        assert!(c.is_busy());
303        // Server replies with an expedited upload response carrying 0x192.
304        let resp = super::super::encode_upload_expedited_response(
305            Address::new(0x1000, 0),
306            &Value::Unsigned32(0x192),
307        )
308        .unwrap();
309        assert_eq!(
310            c.on_response(&resp),
311            SdoEvent::Complete(Some(Value::Unsigned32(0x192)))
312        );
313        assert!(!c.is_busy());
314    }
315
316    #[test]
317    fn write_small_value_is_expedited() {
318        let mut c = client();
319        let req = c.write(Address::new(0x1017, 0), Value::Unsigned16(1234));
320        assert_eq!(req[0] & 0xE0, 0x20); // download initiate
321        assert_ne!(req[0] & 0x02, 0); // expedited
322        let resp = super::super::encode_download_response(Address::new(0x1017, 0));
323        assert_eq!(c.on_response(&resp), SdoEvent::Complete(None));
324    }
325
326    #[test]
327    fn abort_response_surfaces_code() {
328        let mut c = client();
329        c.read(Address::new(0x9999, 0), DataType::Unsigned32);
330        let abort = super::super::encode_abort(
331            Address::new(0x9999, 0),
332            super::super::SdoAbortCode::ObjectDoesNotExist,
333        );
334        assert_eq!(c.on_response(&abort), SdoEvent::Aborted(0x0602_0000));
335        assert!(!c.is_busy());
336    }
337}