Skip to main content

canopen_rs/sdo/
block.rs

1//! SDO **block** transfer (CiA 301 §7.2.4.3) — high-throughput bulk transfer.
2//!
3//! Where segmented transfer acknowledges every 7-byte segment, block transfer
4//! streams a whole *sub-block* of up to 127 segments before a single
5//! acknowledgement, then verifies the transfer with a CRC. This module
6//! implements both directions — client→server **download** and server→client
7//! **upload** — as frame codecs plus the CRC-16 ([`crc16`]) and the
8//! [`BlockWriter`] / [`BlockReceiver`] helpers that split and reassemble the
9//! data on the happy path (in-order, no retransmission).
10//!
11//! Block download and upload are mirror images: several frames (the sub-block
12//! segment, sub-block response, end, and end-response) are byte-identical in
13//! both directions and share one codec here; only the initiate handshake and
14//! the upload *start* frame differ.
15//!
16//! ```
17//! use canopen_rs::sdo::block::crc16;
18//!
19//! // CRC-16/XMODEM — the algorithm CANopen block transfer uses.
20//! assert_eq!(crc16(b"123456789"), 0x31C3);
21//! ```
22
23use heapless::Vec;
24
25use super::SdoPayload;
26use crate::object_dictionary::Address;
27use crate::{Error, Result};
28
29// Block transfer uses just two command-specifier values in byte 0's top three
30// bits, swapped between client and server by direction:
31//   CS_6 (110b): download client / upload server
32//   CS_5 (101b): download server / upload client
33const CS_6: u8 = 0xC0;
34const CS_5: u8 = 0xA0;
35const CS_MASK: u8 = 0xE0;
36
37const CRC_SUPPORTED: u8 = 0x04; // 'cc'/'sc' bit in an initiate frame
38const SIZE_INDICATED: u8 = 0x02; // 's' bit in an initiate frame
39
40const SUBCMD_MASK: u8 = 0x03;
41const SUBCMD_INITIATE: u8 = 0x00;
42const SUBCMD_END: u8 = 0x01;
43const SUBCMD_RESPONSE: u8 = 0x02;
44const SUBCMD_START: u8 = 0x03;
45
46const LAST_SEGMENT: u8 = 0x80; // 'c' bit in a sub-block segment
47const SEQNO_MASK: u8 = 0x7F;
48
49/// Maximum data bytes in one sub-block segment.
50pub const SEGMENT_DATA_MAX: usize = 7;
51/// Maximum segments in one sub-block.
52pub const MAX_BLKSIZE: u8 = 127;
53
54/// CRC-16/XMODEM (CCITT, polynomial `0x1021`, initial value `0`), computed over
55/// the complete transferred data — the checksum CANopen block transfer uses.
56pub fn crc16(data: &[u8]) -> u16 {
57    let mut crc: u16 = 0;
58    for &byte in data {
59        crc ^= (byte as u16) << 8;
60        for _ in 0..8 {
61            crc = if crc & 0x8000 != 0 {
62                (crc << 1) ^ 0x1021
63            } else {
64                crc << 1
65            };
66        }
67    }
68    crc
69}
70
71/// The size-carrying initiate frame: a download-initiate *request* or an
72/// upload-initiate *response*.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct BlockInitiate {
75    /// The object being transferred.
76    pub address: Address,
77    /// The declared total size in bytes, if indicated.
78    pub size: Option<u32>,
79    /// Whether the sender supports the end-of-transfer CRC.
80    pub crc_support: bool,
81}
82
83/// The blksize-carrying initiate frame: a download-initiate *response* or an
84/// upload-initiate *request*.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct BlockNegotiation {
87    /// The object being transferred.
88    pub address: Address,
89    /// Segments allowed per sub-block before an acknowledgement (`1..=127`).
90    pub blksize: u8,
91    /// Whether the sender supports the end-of-transfer CRC.
92    pub crc_support: bool,
93}
94
95/// A decoded sub-block segment.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct SubSegment<'a> {
98    /// The sequence number within the sub-block (`1..=blksize`).
99    pub seqno: u8,
100    /// Whether this is the last segment of the whole transfer.
101    pub last: bool,
102    /// The segment's seven raw payload bytes (the final segment's unused tail
103    /// is trimmed later using the end frame's byte count).
104    pub data: &'a [u8],
105}
106
107fn address_of(p: &SdoPayload) -> Address {
108    Address::new(u16::from_le_bytes([p[1], p[2]]), p[3])
109}
110
111fn put_address(p: &mut SdoPayload, addr: Address) {
112    p[1..3].copy_from_slice(&addr.index.to_le_bytes());
113    p[3] = addr.subindex;
114}
115
116// --- Initiate handshake ----------------------------------------------------
117
118/// Encode a client **block-download initiate** request (`CS_6`).
119pub fn encode_download_initiate(addr: Address, size: Option<u32>, crc_support: bool) -> SdoPayload {
120    encode_initiate_with_size(CS_6, addr, size, crc_support)
121}
122
123/// Decode a block-download initiate request.
124pub fn decode_download_initiate(p: &SdoPayload) -> Result<BlockInitiate> {
125    decode_initiate_with_size(CS_6, p)
126}
127
128/// Encode the server's **block-download initiate response** (`CS_5`).
129pub fn encode_download_initiate_response(
130    addr: Address,
131    blksize: u8,
132    crc_support: bool,
133) -> SdoPayload {
134    encode_initiate_with_blksize(CS_5, addr, blksize, crc_support)
135}
136
137/// Decode a block-download initiate response.
138pub fn decode_download_initiate_response(p: &SdoPayload) -> Result<BlockNegotiation> {
139    decode_initiate_with_blksize(CS_5, p)
140}
141
142/// Encode a client **block-upload initiate** request (`CS_5`).
143pub fn encode_upload_initiate(addr: Address, blksize: u8, crc_support: bool) -> SdoPayload {
144    encode_initiate_with_blksize(CS_5, addr, blksize, crc_support)
145}
146
147/// Decode a block-upload initiate request.
148pub fn decode_upload_initiate(p: &SdoPayload) -> Result<BlockNegotiation> {
149    decode_initiate_with_blksize(CS_5, p)
150}
151
152/// Encode the server's **block-upload initiate response** (`CS_6`).
153pub fn encode_upload_initiate_response(
154    addr: Address,
155    size: Option<u32>,
156    crc_support: bool,
157) -> SdoPayload {
158    encode_initiate_with_size(CS_6, addr, size, crc_support)
159}
160
161/// Decode a block-upload initiate response.
162pub fn decode_upload_initiate_response(p: &SdoPayload) -> Result<BlockInitiate> {
163    decode_initiate_with_size(CS_6, p)
164}
165
166/// Encode the client's **start block-upload** request, telling the server to
167/// begin streaming segments (`CS_5`, sub-command start).
168pub fn encode_upload_start() -> SdoPayload {
169    let mut p = [0u8; 8];
170    p[0] = CS_5 | SUBCMD_START;
171    p
172}
173
174/// Decode a start block-upload request.
175pub fn decode_upload_start(p: &SdoPayload) -> Result<()> {
176    if p[0] & CS_MASK != CS_5 || p[0] & SUBCMD_MASK != SUBCMD_START {
177        return Err(Error::UnexpectedCommand);
178    }
179    Ok(())
180}
181
182fn encode_initiate_with_size(cs: u8, addr: Address, size: Option<u32>, crc: bool) -> SdoPayload {
183    let mut p = [0u8; 8];
184    p[0] = cs | SUBCMD_INITIATE;
185    if crc {
186        p[0] |= CRC_SUPPORTED;
187    }
188    if let Some(size) = size {
189        p[0] |= SIZE_INDICATED;
190        p[4..8].copy_from_slice(&size.to_le_bytes());
191    }
192    put_address(&mut p, addr);
193    p
194}
195
196fn decode_initiate_with_size(cs: u8, p: &SdoPayload) -> Result<BlockInitiate> {
197    // On a size-carrying initiate, bit 1 is the 's' flag, so the initiate/end
198    // sub-command is bit 0 alone (0 = initiate); don't match the full 2 bits.
199    if p[0] & CS_MASK != cs || p[0] & SUBCMD_END != 0 {
200        return Err(Error::UnexpectedCommand);
201    }
202    let size = (p[0] & SIZE_INDICATED != 0).then(|| u32::from_le_bytes([p[4], p[5], p[6], p[7]]));
203    Ok(BlockInitiate {
204        address: address_of(p),
205        size,
206        crc_support: p[0] & CRC_SUPPORTED != 0,
207    })
208}
209
210fn encode_initiate_with_blksize(cs: u8, addr: Address, blksize: u8, crc: bool) -> SdoPayload {
211    let mut p = [0u8; 8];
212    p[0] = cs | SUBCMD_INITIATE;
213    if crc {
214        p[0] |= CRC_SUPPORTED;
215    }
216    put_address(&mut p, addr);
217    p[4] = blksize;
218    p
219}
220
221fn decode_initiate_with_blksize(cs: u8, p: &SdoPayload) -> Result<BlockNegotiation> {
222    if p[0] & CS_MASK != cs || p[0] & SUBCMD_MASK != SUBCMD_INITIATE {
223        return Err(Error::UnexpectedCommand);
224    }
225    Ok(BlockNegotiation {
226        address: address_of(p),
227        blksize: p[4],
228        crc_support: p[0] & CRC_SUPPORTED != 0,
229    })
230}
231
232// --- Sub-block segments and acknowledgements (shared both directions) -------
233
234/// Encode a **sub-block segment** carrying 1..=7 bytes with sequence number
235/// `seqno` (`1..=127`); `last` marks the final segment of the transfer.
236pub fn encode_sub_segment(seqno: u8, data: &[u8], last: bool) -> Result<SdoPayload> {
237    if seqno == 0 || seqno > MAX_BLKSIZE || data.is_empty() || data.len() > SEGMENT_DATA_MAX {
238        return Err(Error::BadLength);
239    }
240    let mut p = [0u8; 8];
241    p[0] = seqno;
242    if last {
243        p[0] |= LAST_SEGMENT;
244    }
245    p[1..1 + data.len()].copy_from_slice(data);
246    Ok(p)
247}
248
249/// Decode a sub-block segment frame.
250pub fn decode_sub_segment(p: &SdoPayload) -> SubSegment<'_> {
251    SubSegment {
252        seqno: p[0] & SEQNO_MASK,
253        last: p[0] & LAST_SEGMENT != 0,
254        data: &p[1..8],
255    }
256}
257
258/// Encode a **sub-block response**: `ackseq` is the highest sequence number
259/// correctly received, `blksize` the size of the next sub-block. (Sent by the
260/// download server or the upload client — identical bytes.)
261pub fn encode_sub_response(ackseq: u8, blksize: u8) -> SdoPayload {
262    let mut p = [0u8; 8];
263    p[0] = CS_5 | SUBCMD_RESPONSE;
264    p[1] = ackseq;
265    p[2] = blksize;
266    p
267}
268
269/// Decode a sub-block response into `(ackseq, blksize)`.
270pub fn decode_sub_response(p: &SdoPayload) -> Result<(u8, u8)> {
271    if p[0] & CS_MASK != CS_5 || p[0] & SUBCMD_MASK != SUBCMD_RESPONSE {
272        return Err(Error::UnexpectedCommand);
273    }
274    Ok((p[1], p[2]))
275}
276
277/// Encode the **end** frame. `unused` is how many of the last segment's seven
278/// bytes carried no data; `crc` is the CRC over the whole transfer (`0` if
279/// unused). (Sent by the download client or the upload server.)
280pub fn encode_end(unused: u8, crc: u16) -> SdoPayload {
281    let mut p = [0u8; 8];
282    p[0] = CS_6 | ((unused & 0x07) << 2) | SUBCMD_END;
283    p[1..3].copy_from_slice(&crc.to_le_bytes());
284    p
285}
286
287/// Decode an end frame into `(unused, crc)`.
288pub fn decode_end(p: &SdoPayload) -> Result<(u8, u16)> {
289    if p[0] & CS_MASK != CS_6 || p[0] & SUBCMD_MASK != SUBCMD_END {
290        return Err(Error::UnexpectedCommand);
291    }
292    Ok(((p[0] >> 2) & 0x07, u16::from_le_bytes([p[1], p[2]])))
293}
294
295/// Encode the **end response** confirming completion (download server or upload
296/// client).
297pub fn encode_end_response() -> SdoPayload {
298    let mut p = [0u8; 8];
299    p[0] = CS_5 | SUBCMD_END;
300    p
301}
302
303/// Decode an end response.
304pub fn decode_end_response(p: &SdoPayload) -> Result<()> {
305    if p[0] & CS_MASK != CS_5 || p[0] & SUBCMD_MASK != SUBCMD_END {
306        return Err(Error::UnexpectedCommand);
307    }
308    Ok(())
309}
310
311// --- Stateful helpers (happy path, both directions) ------------------------
312
313/// Splits a byte buffer into sub-block segments — used by the download client
314/// and the upload server.
315///
316/// Pull [`BlockWriter::next_segment`] until it yields `None`; if
317/// [`BlockWriter::is_done`] then send the [`BlockWriter::end_frame`], otherwise
318/// transmit the sub-block, await the acknowledgement, call
319/// [`BlockWriter::start_sub_block`] with the next blksize, and repeat.
320#[derive(Debug)]
321pub struct BlockWriter<'a> {
322    data: &'a [u8],
323    pos: usize,
324    seqno: u8,
325    blksize: u8,
326}
327
328impl<'a> BlockWriter<'a> {
329    /// Start splitting `data` using the negotiated first `blksize`.
330    pub fn new(data: &'a [u8], blksize: u8) -> Self {
331        Self {
332            data,
333            pos: 0,
334            seqno: 1,
335            blksize,
336        }
337    }
338
339    /// Whether every byte has been emitted (then send [`Self::end_frame`]).
340    pub const fn is_done(&self) -> bool {
341        self.pos >= self.data.len()
342    }
343
344    /// The next segment of the current sub-block, or `None` when the sub-block
345    /// is full or the data is exhausted (disambiguate with [`Self::is_done`]).
346    pub fn next_segment(&mut self) -> Option<SdoPayload> {
347        if self.is_done() || self.seqno > self.blksize {
348            return None;
349        }
350        let remaining = self.data.len() - self.pos;
351        let take = remaining.min(SEGMENT_DATA_MAX);
352        let last = remaining <= SEGMENT_DATA_MAX;
353        let segment = encode_sub_segment(self.seqno, &self.data[self.pos..self.pos + take], last)
354            .expect("seqno and length are in range");
355        self.pos += take;
356        self.seqno += 1;
357        Some(segment)
358    }
359
360    /// Begin the next sub-block after an acknowledgement, using `blksize`.
361    pub fn start_sub_block(&mut self, blksize: u8) {
362        self.seqno = 1;
363        self.blksize = blksize;
364    }
365
366    /// The end frame: the CRC over all data (when `crc_support`) and the count
367    /// of unused bytes in the last segment.
368    pub fn end_frame(&self, crc_support: bool) -> SdoPayload {
369        let last_len = match self.data.len() % SEGMENT_DATA_MAX {
370            0 if !self.data.is_empty() => SEGMENT_DATA_MAX,
371            r => r,
372        };
373        let unused = (SEGMENT_DATA_MAX - last_len) as u8;
374        let crc = if crc_support { crc16(self.data) } else { 0 };
375        encode_end(unused, crc)
376    }
377}
378
379/// Reassembles sub-block segments into a bounded buffer of capacity `N` — used
380/// by the download server and the upload client.
381#[derive(Debug)]
382pub struct BlockReceiver<const N: usize> {
383    buf: Vec<u8, N>,
384    done: bool,
385}
386
387impl<const N: usize> Default for BlockReceiver<N> {
388    fn default() -> Self {
389        Self::new()
390    }
391}
392
393impl<const N: usize> BlockReceiver<N> {
394    /// A new, empty receiver.
395    pub const fn new() -> Self {
396        Self {
397            buf: Vec::new(),
398            done: false,
399        }
400    }
401
402    /// Whether the last segment has been received (then call [`Self::finish`]).
403    pub const fn is_done(&self) -> bool {
404        self.done
405    }
406
407    /// Append a decoded sub-block segment's seven bytes.
408    ///
409    /// Returns [`Error::Overflow`] beyond capacity `N`, or
410    /// [`Error::UnexpectedCommand`] if the transfer is already complete.
411    pub fn push(&mut self, segment: &SubSegment) -> Result<()> {
412        if self.done {
413            return Err(Error::UnexpectedCommand);
414        }
415        self.buf
416            .extend_from_slice(segment.data)
417            .map_err(|_| Error::Overflow)?;
418        if segment.last {
419            self.done = true;
420        }
421        Ok(())
422    }
423
424    /// Finalise: trim the `unused` tail bytes of the last segment and, when
425    /// `verify_crc`, check the transfer CRC. Returns the reassembled data.
426    ///
427    /// Returns [`Error::BadLength`] if `unused` exceeds the buffer, or
428    /// [`Error::CrcMismatch`] on a CRC failure.
429    pub fn finish(&mut self, unused: u8, crc: u16, verify_crc: bool) -> Result<&[u8]> {
430        let unused = unused as usize;
431        if unused > self.buf.len() {
432            return Err(Error::BadLength);
433        }
434        self.buf.truncate(self.buf.len() - unused);
435        if verify_crc && crc16(&self.buf) != crc {
436            return Err(Error::CrcMismatch);
437        }
438        Ok(&self.buf)
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445
446    #[test]
447    fn crc16_known_vector() {
448        assert_eq!(crc16(b"123456789"), 0x31C3); // canonical CRC-16/XMODEM check
449        assert_eq!(crc16(&[]), 0x0000);
450    }
451
452    // Known-good frames: initiate a 100-byte download to 0x2000 sub 0 with CRC
453    // and size (command 0xC6 = CS_6 | cc | s), and its response with blksize 10
454    // and server CRC (command 0xA4 = CS_5 | sc).
455    #[test]
456    fn download_initiate_frames() {
457        let req = encode_download_initiate(Address::new(0x2000, 0), Some(100), true);
458        assert_eq!(req, [0xC6, 0x00, 0x20, 0x00, 100, 0, 0, 0]);
459        assert_eq!(
460            decode_download_initiate(&req).unwrap(),
461            BlockInitiate {
462                address: Address::new(0x2000, 0),
463                size: Some(100),
464                crc_support: true
465            }
466        );
467        let resp = encode_download_initiate_response(Address::new(0x2000, 0), 10, true);
468        assert_eq!(resp, [0xA4, 0x00, 0x20, 0x00, 10, 0, 0, 0]);
469        assert_eq!(
470            decode_download_initiate_response(&resp).unwrap(),
471            BlockNegotiation {
472                address: Address::new(0x2000, 0),
473                blksize: 10,
474                crc_support: true
475            }
476        );
477    }
478
479    // Upload mirrors download: request carries blksize (CS_5), response carries
480    // size (CS_6), plus the start frame (0xA3).
481    #[test]
482    fn upload_initiate_and_start_frames() {
483        let req = encode_upload_initiate(Address::new(0x2000, 0), 5, false);
484        assert_eq!(req, [0xA0, 0x00, 0x20, 0x00, 5, 0, 0, 0]);
485        assert_eq!(decode_upload_initiate(&req).unwrap().blksize, 5);
486
487        let resp = encode_upload_initiate_response(Address::new(0x2000, 0), Some(42), true);
488        assert_eq!(resp, [0xC6, 0x00, 0x20, 0x00, 42, 0, 0, 0]);
489        assert_eq!(
490            decode_upload_initiate_response(&resp).unwrap().size,
491            Some(42)
492        );
493
494        assert_eq!(encode_upload_start(), [0xA3, 0, 0, 0, 0, 0, 0, 0]);
495        assert!(decode_upload_start(&encode_upload_start()).is_ok());
496    }
497
498    #[test]
499    fn shared_segment_and_ack_frames() {
500        let f = encode_sub_segment(1, &[1, 2, 3, 4, 5, 6, 7], false).unwrap();
501        assert_eq!(f, [0x01, 1, 2, 3, 4, 5, 6, 7]);
502        let last = encode_sub_segment(3, &[0xAA, 0xBB], true).unwrap();
503        assert_eq!(last, [0x83, 0xAA, 0xBB, 0, 0, 0, 0, 0]);
504        assert!(decode_sub_segment(&last).last);
505
506        assert_eq!(encode_sub_response(10, 20), [0xA2, 10, 20, 0, 0, 0, 0, 0]);
507        assert_eq!(
508            decode_sub_response(&encode_sub_response(10, 20)).unwrap(),
509            (10, 20)
510        );
511
512        assert_eq!(encode_end(5, 0xBEEF), [0xD5, 0xEF, 0xBE, 0, 0, 0, 0, 0]);
513        assert_eq!(decode_end(&encode_end(5, 0xBEEF)).unwrap(), (5, 0xBEEF));
514        assert_eq!(encode_end_response(), [0xA1, 0, 0, 0, 0, 0, 0, 0]);
515        assert!(decode_end_response(&encode_end_response()).is_ok());
516    }
517
518    /// Split `data` with a [`BlockWriter`] and reassemble with a
519    /// [`BlockReceiver`], verifying the CRC. This is the shared engine of both
520    /// download and upload; the direction only changes which side runs which.
521    fn roundtrip(data: &[u8], blksize: u8) {
522        let mut writer = BlockWriter::new(data, blksize);
523        let mut receiver = BlockReceiver::<128>::new();
524        loop {
525            while let Some(seg) = writer.next_segment() {
526                receiver.push(&decode_sub_segment(&seg)).unwrap();
527            }
528            if writer.is_done() {
529                break;
530            }
531            writer.start_sub_block(blksize);
532        }
533        assert!(receiver.is_done());
534        let (unused, crc) = decode_end(&writer.end_frame(true)).unwrap();
535        assert_eq!(receiver.finish(unused, crc, true).unwrap(), data);
536    }
537
538    #[test]
539    fn block_transfer_roundtrips() {
540        let data: [u8; 30] = core::array::from_fn(|i| i as u8);
541        roundtrip(&data, 2); // spans multiple sub-blocks
542        roundtrip(&data, 127); // one sub-block
543        roundtrip(&[0xAB; 7], 4); // exactly one full segment
544    }
545
546    #[test]
547    fn crc_mismatch_is_detected() {
548        let data = [1u8, 2, 3, 4, 5];
549        let mut writer = BlockWriter::new(&data, 1);
550        let mut receiver = BlockReceiver::<16>::new();
551        while let Some(seg) = writer.next_segment() {
552            receiver.push(&decode_sub_segment(&seg)).unwrap();
553        }
554        let (unused, _) = decode_end(&writer.end_frame(true)).unwrap();
555        assert_eq!(
556            receiver.finish(unused, 0x0000, true),
557            Err(Error::CrcMismatch)
558        );
559    }
560}