Skip to main content

canopen_rs/sdo/
mod.rs

1//! Service Data Object (SDO) protocol (CiA 301 §7.2.4).
2//!
3//! SDOs provide confirmed, addressed read/write access to any object
4//! dictionary entry. Each transfer step is a client request frame answered by
5//! a server response frame, exchanged on a pair of CAN ids (by default
6//! `0x600 + node` for requests and `0x580 + node` for responses).
7//!
8//! This module implements **expedited** and **segmented** transfer: a value of
9//! one to four bytes carried inline in a single exchange, or a larger value
10//! split across a run of segment frames. Block transfer follows.
11//!
12//! The free functions here encode and decode the raw 8-byte CAN *data field*.
13//! On top of them, [`SdoServer`] services requests against an
14//! [`ObjectDictionary`](crate::object_dictionary::ObjectDictionary) and
15//! [`SdoClient`] drives read/write transactions — both as sans-I/O state
16//! machines that consume and produce frames without touching a bus, so the
17//! transport (see [`request_cob_id`]/[`response_cob_id`]) stays a separate
18//! concern.
19//!
20//! # Example: encode an expedited download
21//!
22//! Writing `UNSIGNED32 0x1234_5678` to object `0x2000` is a single 8-byte
23//! request frame (see [`SdoClient`] / [`SdoServer`] to drive a whole exchange):
24//!
25//! ```
26//! use canopen_rs::sdo::encode_download_expedited;
27//! use canopen_rs::{Address, Value};
28//!
29//! let frame = encode_download_expedited(
30//!     Address::new(0x2000, 0),
31//!     &Value::Unsigned32(0x1234_5678),
32//! )
33//! .unwrap();
34//! // cmd | index (LE) | subindex | value (LE)
35//! assert_eq!(frame, [0x23, 0x00, 0x20, 0x00, 0x78, 0x56, 0x34, 0x12]);
36//! ```
37
38use heapless::Vec;
39
40use crate::datatypes::{DataType, Value};
41use crate::object_dictionary::Address;
42use crate::types::NodeId;
43use crate::{Error, Result};
44
45pub mod block;
46pub mod client;
47pub mod server;
48
49pub use client::{SdoClient, SdoEvent};
50pub use server::SdoServer;
51
52/// COB-ID base for SDO client→server (request) frames: `0x600 + node id`.
53pub const SDO_REQUEST_COB_BASE: u16 = 0x600;
54/// COB-ID base for SDO server→client (response) frames: `0x580 + node id`.
55pub const SDO_RESPONSE_COB_BASE: u16 = 0x580;
56
57/// The 8-byte SDO payload carried in a CAN frame's data field.
58pub type SdoPayload = [u8; 8];
59
60/// The COB-ID of the SDO request channel (client → server) for `node`.
61pub const fn request_cob_id(node: NodeId) -> u16 {
62    SDO_REQUEST_COB_BASE + node.raw() as u16
63}
64
65/// The COB-ID of the SDO response channel (server → client) for `node`.
66pub const fn response_cob_id(node: NodeId) -> u16 {
67    SDO_RESPONSE_COB_BASE + node.raw() as u16
68}
69
70// --- Command specifiers (top three bits of byte 0) -------------------------
71const CCS_DOWNLOAD_SEGMENT: u8 = 0x00; // client: 000xxxxx
72const CCS_DOWNLOAD_INITIATE: u8 = 0x20; // client: 001xxxxx
73const CCS_UPLOAD_INITIATE: u8 = 0x40; // client: 010xxxxx
74const CCS_UPLOAD_SEGMENT: u8 = 0x60; // client: 011xxxxx
75                                     // The server's upload-segment specifier (scs 000) equals CCS_DOWNLOAD_SEGMENT,
76                                     // which is why one data-segment codec serves both directions.
77const SCS_DOWNLOAD_SEGMENT: u8 = 0x20; // server: 001xxxxx
78const SCS_UPLOAD_INITIATE: u8 = 0x40; // server: 010xxxxx
79const SCS_DOWNLOAD_INITIATE: u8 = 0x60; // server: 011xxxxx
80const CS_ABORT: u8 = 0x80; // either:  100xxxxx
81
82// Top-three-bit command-specifier mask.
83const CS_MASK: u8 = 0xE0;
84
85// Low-byte flag bits.
86const EXPEDITED: u8 = 0x02; // 'e' in an initiate frame
87const SIZE_INDICATED: u8 = 0x01; // 's' in an initiate frame
88const EXPEDITED_SIZED: u8 = EXPEDITED | SIZE_INDICATED; // expedited + size (0x03)
89const TOGGLE: u8 = 0x10; // 't' in a segment frame
90const NO_MORE_SEGMENTS: u8 = 0x01; // 'c' in a data-segment frame (last segment)
91
92/// Maximum data bytes carried by a single SDO segment frame.
93pub const SEGMENT_DATA_MAX: usize = 7;
94
95/// SDO abort codes (CiA 301 §7.2.4.3.17). The value is the 32-bit code sent
96/// little-endian in bytes 4..8 of an abort frame.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98#[non_exhaustive]
99pub enum SdoAbortCode {
100    /// Toggle bit not alternated.
101    ToggleBitNotAlternated = 0x0503_0000,
102    /// SDO protocol timed out.
103    ProtocolTimedOut = 0x0504_0000,
104    /// Client/server command specifier not valid or unknown.
105    CommandInvalid = 0x0504_0001,
106    /// Unsupported access to an object.
107    UnsupportedAccess = 0x0601_0000,
108    /// Attempt to read a write-only object.
109    ReadOfWriteOnly = 0x0601_0001,
110    /// Attempt to write a read-only object.
111    WriteOfReadOnly = 0x0601_0002,
112    /// Object does not exist in the object dictionary.
113    ObjectDoesNotExist = 0x0602_0000,
114    /// Data type does not match; length of service parameter too high.
115    DataTypeMismatchLengthHigh = 0x0607_0012,
116    /// Data type does not match; length of service parameter too low.
117    DataTypeMismatchLengthLow = 0x0607_0013,
118    /// Sub-index does not exist.
119    SubIndexDoesNotExist = 0x0609_0011,
120    /// General error.
121    General = 0x0800_0000,
122}
123
124/// Encode an expedited SDO **download** (write) request writing `value` to
125/// `addr`.
126///
127/// Returns [`Error::UnsupportedTransfer`] for values larger than four bytes,
128/// which require segmented transfer.
129pub fn encode_download_expedited(addr: Address, value: &Value) -> Result<SdoPayload> {
130    let len = value.size();
131    // Expedited carries 1..=4 data bytes; empty and larger values go segmented
132    // (the 2-bit unused-byte field cannot express a zero-length payload).
133    if len == 0 || len > 4 {
134        return Err(Error::UnsupportedTransfer);
135    }
136    let mut p = [0u8; 8];
137    // n = number of *unused* data bytes = 4 - len.
138    p[0] = CCS_DOWNLOAD_INITIATE | (((4 - len) as u8) << 2) | EXPEDITED_SIZED;
139    p[1..3].copy_from_slice(&addr.index.to_le_bytes());
140    p[3] = addr.subindex;
141    value.encode_le(&mut p[4..4 + len])?;
142    Ok(p)
143}
144
145/// Encode the server's **download response** (write confirmation) for `addr`.
146pub fn encode_download_response(addr: Address) -> SdoPayload {
147    let mut p = [0u8; 8];
148    p[0] = SCS_DOWNLOAD_INITIATE;
149    p[1..3].copy_from_slice(&addr.index.to_le_bytes());
150    p[3] = addr.subindex;
151    p
152}
153
154/// Decode a server download response, returning the confirmed address.
155pub fn decode_download_response(p: &SdoPayload) -> Result<Address> {
156    if p[0] != SCS_DOWNLOAD_INITIATE {
157        return Err(Error::UnexpectedCommand);
158    }
159    Ok(Address::new(u16::from_le_bytes([p[1], p[2]]), p[3]))
160}
161
162/// Encode an SDO **upload** (read) request for `addr`.
163pub fn encode_upload_request(addr: Address) -> SdoPayload {
164    let mut p = [0u8; 8];
165    p[0] = CCS_UPLOAD_INITIATE;
166    p[1..3].copy_from_slice(&addr.index.to_le_bytes());
167    p[3] = addr.subindex;
168    p
169}
170
171/// Encode the server's expedited **upload response** carrying `value` for
172/// `addr`.
173///
174/// Returns [`Error::UnsupportedTransfer`] for values larger than four bytes.
175pub fn encode_upload_expedited_response(addr: Address, value: &Value) -> Result<SdoPayload> {
176    let len = value.size();
177    // Expedited carries 1..=4 data bytes; empty and larger values go segmented.
178    if len == 0 || len > 4 {
179        return Err(Error::UnsupportedTransfer);
180    }
181    let mut p = [0u8; 8];
182    p[0] = SCS_UPLOAD_INITIATE | (((4 - len) as u8) << 2) | EXPEDITED_SIZED;
183    p[1..3].copy_from_slice(&addr.index.to_le_bytes());
184    p[3] = addr.subindex;
185    value.encode_le(&mut p[4..4 + len])?;
186    Ok(p)
187}
188
189/// Decode an expedited upload response into `(address, value)`, interpreting
190/// the inline data as `data_type` (the client knows the expected type from
191/// its OD/EDS).
192///
193/// Returns [`Error::UnexpectedCommand`] if the frame is not an expedited
194/// upload response, or [`Error::TypeMismatch`] if the server's data length
195/// disagrees with `data_type`.
196pub fn decode_upload_expedited_response(
197    p: &SdoPayload,
198    data_type: DataType,
199) -> Result<(Address, Value)> {
200    let cmd = p[0];
201    // scs must be "upload initiate" and the frame must be expedited + sized.
202    if cmd & 0xE0 != SCS_UPLOAD_INITIATE || cmd & EXPEDITED_SIZED != EXPEDITED_SIZED {
203        return Err(Error::UnexpectedCommand);
204    }
205    let n = (cmd >> 2) & 0x03;
206    let len = 4 - n as usize;
207    // A fixed-size type must match exactly; a variable-length one accepts the
208    // server's indicated length as the (short) string content.
209    if let Some(fixed) = data_type.fixed_size() {
210        if len != fixed {
211            return Err(Error::TypeMismatch);
212        }
213    }
214    let addr = Address::new(u16::from_le_bytes([p[1], p[2]]), p[3]);
215    let value = Value::decode_le(data_type, &p[4..4 + len])?;
216    Ok((addr, value))
217}
218
219/// Encode an SDO **abort** for `addr` with `code`.
220pub fn encode_abort(addr: Address, code: SdoAbortCode) -> SdoPayload {
221    let mut p = [0u8; 8];
222    p[0] = CS_ABORT;
223    p[1..3].copy_from_slice(&addr.index.to_le_bytes());
224    p[3] = addr.subindex;
225    p[4..8].copy_from_slice(&(code as u32).to_le_bytes());
226    p
227}
228
229/// Decode an SDO abort frame into `(address, raw_abort_code)`.
230pub fn decode_abort(p: &SdoPayload) -> Result<(Address, u32)> {
231    if p[0] != CS_ABORT {
232        return Err(Error::UnexpectedCommand);
233    }
234    let addr = Address::new(u16::from_le_bytes([p[1], p[2]]), p[3]);
235    let code = u32::from_le_bytes([p[4], p[5], p[6], p[7]]);
236    Ok((addr, code))
237}
238
239// === Segmented transfer ====================================================
240//
241// For values larger than four bytes, transfer proceeds in two phases: an
242// *initiate* exchange declaring the total byte count, then a run of *segment*
243// exchanges each carrying up to seven data bytes. A per-transfer *toggle* bit
244// alternates on every segment (starting at 0) to detect lost or duplicated
245// frames, and the final data segment sets the "no more segments" bit.
246//
247// The initiate *download response* (server) and initiate *upload request*
248// (client) are byte-identical to the expedited case, so reuse
249// [`encode_download_response`] / [`decode_download_response`] and
250// [`encode_upload_request`] for them.
251
252/// A decoded SDO data segment: its toggle bit, whether it is the last segment,
253/// and the (borrowed) data bytes it carries.
254///
255/// The download-segment request (client → server) and the upload-segment
256/// response (server → client) share this exact frame layout, so one type and
257/// one codec serve both directions.
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub struct Segment<'a> {
260    /// The toggle bit for this segment (alternates each segment from `false`).
261    pub toggle: bool,
262    /// Whether this is the final segment of the transfer.
263    pub last: bool,
264    /// The segment's payload (1..=7 bytes).
265    pub data: &'a [u8],
266}
267
268/// Encode a client **segmented download initiate** request declaring a
269/// `size`-byte transfer to `addr` (command `0x21`).
270pub fn encode_download_initiate_segmented(addr: Address, size: u32) -> SdoPayload {
271    let mut p = [0u8; 8];
272    p[0] = CCS_DOWNLOAD_INITIATE | SIZE_INDICATED;
273    p[1..3].copy_from_slice(&addr.index.to_le_bytes());
274    p[3] = addr.subindex;
275    p[4..8].copy_from_slice(&size.to_le_bytes());
276    p
277}
278
279/// Decode a download initiate request into `(address, size)`, requiring a
280/// segmented (non-expedited), size-indicated request.
281pub fn decode_download_initiate_segmented(p: &SdoPayload) -> Result<(Address, u32)> {
282    if p[0] & CS_MASK != CCS_DOWNLOAD_INITIATE
283        || p[0] & EXPEDITED != 0
284        || p[0] & SIZE_INDICATED == 0
285    {
286        return Err(Error::UnexpectedCommand);
287    }
288    let addr = Address::new(u16::from_le_bytes([p[1], p[2]]), p[3]);
289    Ok((addr, u32::from_le_bytes([p[4], p[5], p[6], p[7]])))
290}
291
292/// Encode the server's **segmented upload initiate response** declaring a
293/// `size`-byte transfer for `addr` (command `0x41`).
294pub fn encode_upload_initiate_segmented_response(addr: Address, size: u32) -> SdoPayload {
295    let mut p = [0u8; 8];
296    p[0] = SCS_UPLOAD_INITIATE | SIZE_INDICATED;
297    p[1..3].copy_from_slice(&addr.index.to_le_bytes());
298    p[3] = addr.subindex;
299    p[4..8].copy_from_slice(&size.to_le_bytes());
300    p
301}
302
303/// Decode a segmented upload initiate response into `(address, size)`.
304///
305/// Returns [`Error::UnexpectedCommand`] if the frame is not an upload initiate
306/// response, or if it is expedited (use [`decode_upload_expedited_response`]
307/// for that case).
308pub fn decode_upload_initiate_segmented_response(p: &SdoPayload) -> Result<(Address, u32)> {
309    if p[0] & CS_MASK != SCS_UPLOAD_INITIATE || p[0] & EXPEDITED != 0 || p[0] & SIZE_INDICATED == 0
310    {
311        return Err(Error::UnexpectedCommand);
312    }
313    let addr = Address::new(u16::from_le_bytes([p[1], p[2]]), p[3]);
314    Ok((addr, u32::from_le_bytes([p[4], p[5], p[6], p[7]])))
315}
316
317/// Encode a **data segment** carrying 1..=7 bytes of `data`.
318///
319/// Used for both the download-segment request and the upload-segment response
320/// (identical layout). `toggle` alternates each segment (the first is
321/// `false`); `last` marks the final segment. Returns [`Error::BadLength`] if
322/// `data` is longer than 7 bytes. An empty final segment (0 bytes) is allowed,
323/// so a zero-length value can be transferred.
324pub fn encode_data_segment(data: &[u8], toggle: bool, last: bool) -> Result<SdoPayload> {
325    if data.len() > SEGMENT_DATA_MAX {
326        return Err(Error::BadLength);
327    }
328    let mut p = [0u8; 8];
329    // ccs/scs for a data segment are both 000, so byte 0's top bits stay 0.
330    let n = (SEGMENT_DATA_MAX - data.len()) as u8;
331    p[0] = (n << 1) & 0x0E;
332    if toggle {
333        p[0] |= TOGGLE;
334    }
335    if last {
336        p[0] |= NO_MORE_SEGMENTS;
337    }
338    p[1..1 + data.len()].copy_from_slice(data);
339    Ok(p)
340}
341
342/// Decode a **data segment** frame (download request or upload response).
343pub fn decode_data_segment(p: &SdoPayload) -> Result<Segment<'_>> {
344    if p[0] & CS_MASK != CCS_DOWNLOAD_SEGMENT {
345        return Err(Error::UnexpectedCommand);
346    }
347    let n = ((p[0] >> 1) & 0x07) as usize;
348    if n > SEGMENT_DATA_MAX {
349        return Err(Error::BadLength);
350    }
351    Ok(Segment {
352        toggle: p[0] & TOGGLE != 0,
353        last: p[0] & NO_MORE_SEGMENTS != 0,
354        data: &p[1..1 + (SEGMENT_DATA_MAX - n)],
355    })
356}
357
358/// Encode the server's **download segment response** (acknowledgement) with the
359/// segment's `toggle` bit (command `0x20 | toggle`).
360pub fn encode_download_segment_response(toggle: bool) -> SdoPayload {
361    let mut p = [0u8; 8];
362    p[0] = SCS_DOWNLOAD_SEGMENT | if toggle { TOGGLE } else { 0 };
363    p
364}
365
366/// Decode a download segment response, returning its toggle bit.
367pub fn decode_download_segment_response(p: &SdoPayload) -> Result<bool> {
368    if p[0] & CS_MASK != SCS_DOWNLOAD_SEGMENT {
369        return Err(Error::UnexpectedCommand);
370    }
371    Ok(p[0] & TOGGLE != 0)
372}
373
374/// Encode the client's **upload segment request** (poll for the next segment)
375/// with the expected `toggle` bit (command `0x60 | toggle`).
376pub fn encode_upload_segment_request(toggle: bool) -> SdoPayload {
377    let mut p = [0u8; 8];
378    p[0] = CCS_UPLOAD_SEGMENT | if toggle { TOGGLE } else { 0 };
379    p
380}
381
382/// Decode an upload segment request, returning its toggle bit.
383pub fn decode_upload_segment_request(p: &SdoPayload) -> Result<bool> {
384    if p[0] & CS_MASK != CCS_UPLOAD_SEGMENT {
385        return Err(Error::UnexpectedCommand);
386    }
387    Ok(p[0] & TOGGLE != 0)
388}
389
390/// Splits a byte buffer into SDO download data segments, tracking the toggle
391/// bit. Drive it after a successful download-initiate handshake: emit each
392/// frame, await its acknowledgement, then take the next.
393#[derive(Debug)]
394pub struct SegmentWriter<'a> {
395    data: &'a [u8],
396    pos: usize,
397    toggle: bool,
398}
399
400impl<'a> SegmentWriter<'a> {
401    /// Start splitting `data` (which should be the >4-byte value already
402    /// declared in the initiate request).
403    pub const fn new(data: &'a [u8]) -> Self {
404        Self {
405            data,
406            pos: 0,
407            toggle: false,
408        }
409    }
410
411    /// Whether every byte has been emitted.
412    pub const fn is_done(&self) -> bool {
413        self.pos >= self.data.len()
414    }
415
416    /// Produce the next download data-segment frame, or `None` when finished.
417    pub fn next_segment(&mut self) -> Option<SdoPayload> {
418        if self.is_done() {
419            return None;
420        }
421        let remaining = self.data.len() - self.pos;
422        let len = remaining.min(SEGMENT_DATA_MAX);
423        let last = remaining <= SEGMENT_DATA_MAX;
424        let frame = encode_data_segment(&self.data[self.pos..self.pos + len], self.toggle, last)
425            .expect("len is 1..=7");
426        self.pos += len;
427        self.toggle = !self.toggle;
428        Some(frame)
429    }
430}
431
432/// Reassembles SDO upload data segments into a bounded buffer of capacity `N`,
433/// tracking and validating the toggle bit. Push each decoded [`Segment`] until
434/// [`SegmentReader::is_done`], then read [`SegmentReader::data`].
435#[derive(Debug)]
436pub struct SegmentReader<const N: usize> {
437    buf: Vec<u8, N>,
438    toggle: bool,
439    done: bool,
440}
441
442impl<const N: usize> Default for SegmentReader<N> {
443    fn default() -> Self {
444        Self::new()
445    }
446}
447
448impl<const N: usize> SegmentReader<N> {
449    /// A new, empty reassembler.
450    pub const fn new() -> Self {
451        Self {
452            buf: Vec::new(),
453            toggle: false,
454            done: false,
455        }
456    }
457
458    /// Whether the final segment has been received.
459    pub const fn is_done(&self) -> bool {
460        self.done
461    }
462
463    /// The reassembled bytes so far.
464    pub fn data(&self) -> &[u8] {
465        &self.buf
466    }
467
468    /// Append a decoded segment.
469    ///
470    /// Returns [`Error::ToggleMismatch`] if the segment's toggle bit is out of
471    /// sequence, [`Error::UnexpectedCommand`] if the transfer is already
472    /// complete, or [`Error::Overflow`] if the data exceeds capacity `N`.
473    pub fn push(&mut self, segment: &Segment) -> Result<()> {
474        if self.done {
475            return Err(Error::UnexpectedCommand);
476        }
477        if segment.toggle != self.toggle {
478            return Err(Error::ToggleMismatch);
479        }
480        self.buf
481            .extend_from_slice(segment.data)
482            .map_err(|_| Error::Overflow)?;
483        self.toggle = !self.toggle;
484        if segment.last {
485            self.done = true;
486        }
487        Ok(())
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494
495    // --- COB-IDs -----------------------------------------------------------
496    #[test]
497    fn cob_ids_follow_convention() {
498        let node = NodeId::new(0x05).unwrap();
499        assert_eq!(request_cob_id(node), 0x605);
500        assert_eq!(response_cob_id(node), 0x585);
501    }
502
503    // --- Download (write) --------------------------------------------------
504    // Known-good frame: expedited download of UNSIGNED32 0x12345678 to
505    // object 0x2000 sub 0. Command 0x23 = download initiate, expedited,
506    // size indicated, 4 data bytes. Index and value are little-endian.
507    #[test]
508    fn download_u32_matches_known_frame() {
509        let f = encode_download_expedited(Address::new(0x2000, 0), &Value::Unsigned32(0x1234_5678))
510            .unwrap();
511        assert_eq!(f, [0x23, 0x00, 0x20, 0x00, 0x78, 0x56, 0x34, 0x12]);
512    }
513
514    // Known-good frame: expedited download of UNSIGNED8 0x7F to 0x2001 sub 0.
515    // Command 0x2F = download initiate, expedited, size indicated, 1 data byte.
516    #[test]
517    fn download_u8_matches_known_frame() {
518        let f =
519            encode_download_expedited(Address::new(0x2001, 0), &Value::Unsigned8(0x7F)).unwrap();
520        assert_eq!(f, [0x2F, 0x01, 0x20, 0x00, 0x7F, 0x00, 0x00, 0x00]);
521    }
522
523    #[test]
524    fn download_i16_matches_known_frame() {
525        // -2 as INTEGER16 = 0xFFFE little-endian; command 0x2B = 2 data bytes.
526        let f = encode_download_expedited(Address::new(0x6000, 1), &Value::Integer16(-2)).unwrap();
527        assert_eq!(f, [0x2B, 0x00, 0x60, 0x01, 0xFE, 0xFF, 0x00, 0x00]);
528    }
529
530    #[test]
531    fn download_response_roundtrips() {
532        let f = encode_download_response(Address::new(0x2000, 0));
533        assert_eq!(f, [0x60, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00]);
534        assert_eq!(
535            decode_download_response(&f).unwrap(),
536            Address::new(0x2000, 0)
537        );
538    }
539
540    #[test]
541    fn value_too_large_for_expedited_rejected() {
542        assert_eq!(
543            encode_download_expedited(Address::new(0x2000, 0), &Value::Unsigned64(1)),
544            Err(Error::UnsupportedTransfer)
545        );
546    }
547
548    // --- Upload (read) -----------------------------------------------------
549    // Known-good frame: upload (read) request for object 0x1000 sub 0.
550    #[test]
551    fn upload_request_matches_known_frame() {
552        let f = encode_upload_request(Address::new(0x1000, 0));
553        assert_eq!(f, [0x40, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00]);
554    }
555
556    // Known-good frame: expedited upload response for device type object
557    // 0x1000 = UNSIGNED32 0x00000192. Command 0x43 = upload initiate,
558    // expedited, size indicated, 4 data bytes.
559    #[test]
560    fn upload_response_device_type_decodes() {
561        let f = [0x43, 0x00, 0x10, 0x00, 0x92, 0x01, 0x00, 0x00];
562        let (addr, value) = decode_upload_expedited_response(&f, DataType::Unsigned32).unwrap();
563        assert_eq!(addr, Address::new(0x1000, 0));
564        assert_eq!(value, Value::Unsigned32(0x0000_0192));
565    }
566
567    #[test]
568    fn upload_response_encode_matches_known_frame() {
569        let f = encode_upload_expedited_response(
570            Address::new(0x1000, 0),
571            &Value::Unsigned32(0x0000_0192),
572        )
573        .unwrap();
574        assert_eq!(f, [0x43, 0x00, 0x10, 0x00, 0x92, 0x01, 0x00, 0x00]);
575    }
576
577    #[test]
578    fn upload_response_wrong_type_size_errors() {
579        // Frame declares 4 data bytes; decoding as U16 (2 bytes) must fail.
580        let f = [0x43, 0x00, 0x10, 0x00, 0x92, 0x01, 0x00, 0x00];
581        assert_eq!(
582            decode_upload_expedited_response(&f, DataType::Unsigned16),
583            Err(Error::TypeMismatch)
584        );
585    }
586
587    #[test]
588    fn decode_upload_rejects_non_upload_frame() {
589        let f = encode_download_response(Address::new(0x1000, 0));
590        assert_eq!(
591            decode_upload_expedited_response(&f, DataType::Unsigned32),
592            Err(Error::UnexpectedCommand)
593        );
594    }
595
596    // --- Abort -------------------------------------------------------------
597    // Known-good frame: abort of 0x1000 sub 0 with code 0x06020000
598    // (object does not exist), sent little-endian in bytes 4..8.
599    #[test]
600    fn abort_object_missing_matches_known_frame() {
601        let f = encode_abort(Address::new(0x1000, 0), SdoAbortCode::ObjectDoesNotExist);
602        assert_eq!(f, [0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x02, 0x06]);
603        let (addr, code) = decode_abort(&f).unwrap();
604        assert_eq!(addr, Address::new(0x1000, 0));
605        assert_eq!(code, 0x0602_0000);
606    }
607
608    // --- Segmented transfer ------------------------------------------------
609    // Known-good frame: segmented download initiate of a 20-byte value to
610    // 0x2000 sub 0. Command 0x21 = download initiate, size indicated, not
611    // expedited; size 20 little-endian in bytes 4..8.
612    #[test]
613    fn download_initiate_segmented_matches_known_frame() {
614        let f = encode_download_initiate_segmented(Address::new(0x2000, 0), 20);
615        assert_eq!(f, [0x21, 0x00, 0x20, 0x00, 20, 0x00, 0x00, 0x00]);
616        assert_eq!(
617            decode_download_initiate_segmented(&f).unwrap(),
618            (Address::new(0x2000, 0), 20)
619        );
620    }
621
622    // Known-good frame: segmented upload initiate response, 20-byte value from
623    // 0x2000 sub 0. Command 0x41 = upload initiate, size indicated, segmented.
624    #[test]
625    fn upload_initiate_segmented_response_matches_known_frame() {
626        let f = encode_upload_initiate_segmented_response(Address::new(0x2000, 0), 20);
627        assert_eq!(f, [0x41, 0x00, 0x20, 0x00, 20, 0x00, 0x00, 0x00]);
628        assert_eq!(
629            decode_upload_initiate_segmented_response(&f).unwrap(),
630            (Address::new(0x2000, 0), 20)
631        );
632    }
633
634    // A segmented initiate must not decode an expedited response and vice versa.
635    #[test]
636    fn segmented_initiate_rejects_expedited_frame() {
637        let expedited = [0x43, 0x00, 0x10, 0x00, 0x92, 0x01, 0x00, 0x00];
638        assert_eq!(
639            decode_upload_initiate_segmented_response(&expedited),
640            Err(Error::UnexpectedCommand)
641        );
642    }
643
644    // Known-good frame: a full 7-byte first data segment, toggle 0, not last.
645    // Command 0x00: n = 0 unused bytes, toggle clear, continue.
646    #[test]
647    fn data_segment_full_matches_known_frame() {
648        let f = encode_data_segment(&[1, 2, 3, 4, 5, 6, 7], false, false).unwrap();
649        assert_eq!(f, [0x00, 1, 2, 3, 4, 5, 6, 7]);
650    }
651
652    // Known-good frame: a final 3-byte segment, toggle 1, last. Command 0x19 =
653    // toggle (0x10) | n=4 unused bytes (4 << 1 = 0x08) | last (0x01).
654    #[test]
655    fn data_segment_last_matches_known_frame() {
656        let f = encode_data_segment(&[0xAA, 0xBB, 0xCC], true, true).unwrap();
657        assert_eq!(f, [0x19, 0xAA, 0xBB, 0xCC, 0x00, 0x00, 0x00, 0x00]);
658        let seg = decode_data_segment(&f).unwrap();
659        assert!(seg.toggle);
660        assert!(seg.last);
661        assert_eq!(seg.data, &[0xAA, 0xBB, 0xCC]);
662    }
663
664    #[test]
665    fn segment_ack_and_poll_toggle_roundtrip() {
666        assert_eq!(
667            encode_download_segment_response(false),
668            [0x20, 0, 0, 0, 0, 0, 0, 0]
669        );
670        assert_eq!(
671            encode_download_segment_response(true),
672            [0x30, 0, 0, 0, 0, 0, 0, 0]
673        );
674        assert!(decode_download_segment_response(&encode_download_segment_response(true)).unwrap());
675
676        assert_eq!(
677            encode_upload_segment_request(false),
678            [0x60, 0, 0, 0, 0, 0, 0, 0]
679        );
680        assert_eq!(
681            encode_upload_segment_request(true),
682            [0x70, 0, 0, 0, 0, 0, 0, 0]
683        );
684        assert!(decode_upload_segment_request(&encode_upload_segment_request(true)).unwrap());
685    }
686
687    // End-to-end: split a 12-byte value into segments and reassemble it. The
688    // writer emits two frames (7 + 5), toggles alternating false/true, the
689    // second marked last; the reader validates the toggle and rebuilds it.
690    #[test]
691    fn segment_writer_reader_roundtrip() {
692        let payload: [u8; 12] = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120];
693        let mut writer = SegmentWriter::new(&payload);
694        let mut reader = SegmentReader::<64>::new();
695
696        let f1 = writer.next_segment().unwrap();
697        assert_eq!(f1[0] & TOGGLE, 0); // first toggle is 0
698        reader.push(&decode_data_segment(&f1).unwrap()).unwrap();
699        assert!(!reader.is_done());
700
701        let f2 = writer.next_segment().unwrap();
702        assert_ne!(f2[0] & TOGGLE, 0); // second toggle is 1
703        reader.push(&decode_data_segment(&f2).unwrap()).unwrap();
704
705        assert!(reader.is_done());
706        assert!(writer.next_segment().is_none());
707        assert_eq!(reader.data(), &payload);
708    }
709
710    #[test]
711    fn reader_rejects_toggle_out_of_sequence() {
712        let mut reader = SegmentReader::<16>::new();
713        // Second push should expect toggle=true; supplying false must fail.
714        reader
715            .push(&Segment {
716                toggle: false,
717                last: false,
718                data: &[1, 2, 3],
719            })
720            .unwrap();
721        assert_eq!(
722            reader.push(&Segment {
723                toggle: false,
724                last: true,
725                data: &[4, 5]
726            }),
727            Err(Error::ToggleMismatch)
728        );
729    }
730
731    #[test]
732    fn reader_overflow_is_reported() {
733        let mut reader = SegmentReader::<4>::new();
734        assert_eq!(
735            reader.push(&Segment {
736                toggle: false,
737                last: false,
738                data: &[1, 2, 3, 4, 5]
739            }),
740            Err(Error::Overflow)
741        );
742    }
743}