Skip to main content

zencan_client/
sdo_client.rs

1use std::time::Duration;
2
3use snafu::Snafu;
4use zencan_common::{
5    constants::{object_ids, values::SAVE_CMD},
6    i24,
7    lss::LssIdentity,
8    messages::CanId,
9    node_configuration::PdoConfig,
10    pdo::{PdoCommParameter, PdoMapping},
11    sdo::{AbortCode, BlockSegment, SdoRequest, SdoResponse},
12    traits::{AsyncCanReceiver, AsyncCanSender, CanSendError as _, ReadSize},
13    u24, CanMessage, TimeDifference, TimeOfDay,
14};
15
16const DEFAULT_RESPONSE_TIMEOUT: Duration = Duration::from_millis(150);
17
18/// A wrapper around the AbortCode enum to allow for unknown values
19///
20/// Although the library should "know" all the abort codes, it is possible to receive other values
21/// and this allows those to be captured and exposed.
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub enum RawAbortCode {
24    /// A recognized abort code
25    Valid(AbortCode),
26    /// An unrecognized abort code
27    Unknown(u32),
28}
29
30impl std::fmt::Display for RawAbortCode {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        match self {
33            RawAbortCode::Valid(abort_code) => write!(f, "{abort_code:?}"),
34            RawAbortCode::Unknown(code) => write!(f, "{code:X}"),
35        }
36    }
37}
38
39impl From<u32> for RawAbortCode {
40    fn from(value: u32) -> Self {
41        match AbortCode::try_from(value) {
42            Ok(code) => Self::Valid(code),
43            Err(_) => Self::Unknown(value),
44        }
45    }
46}
47
48/// Error returned by [`SdoClient`] methods
49#[derive(Clone, Debug, PartialEq, Snafu)]
50pub enum SdoClientError {
51    /// Timeout while awaiting an expected response
52    NoResponse,
53    /// Received a response that could not be interpreted
54    MalformedResponse,
55    /// Received a valid SdoResponse, but with an unexpected command specifier
56    #[snafu(display("Unexpected SDO response. Expected {expecting}, got {response:?}"))]
57    UnexpectedResponse {
58        /// The type of response which was expected
59        expecting: String,
60        /// The response which was received
61        response: SdoResponse,
62    },
63    /// Received a ServerAbort response from the node
64    #[snafu(display("Received abort accessing object 0x{index:X}sub{sub}: {abort_code}"))]
65    ServerAbort {
66        /// Index of the SDO access which was aborted
67        index: u16,
68        /// Sub index of the SDO access which was aborted
69        sub: u8,
70        /// Reason for the abort
71        abort_code: RawAbortCode,
72    },
73    /// Received a response with the wrong toggle bit
74    ToggleNotAlternated,
75    /// Received a response with a different index/sub value than was requested
76    #[snafu(display("Received object 0x{:x}sub{} after requesting 0x{:x}sub{}",
77        received.0, received.1, expected.0, expected.1))]
78    MismatchedObjectIndex {
79        /// The object ID which was expected to be echoed back
80        expected: (u16, u8),
81        /// The received object ID
82        received: (u16, u8),
83    },
84    /// An SDO upload response had a size that did not match the expected size
85    UnexpectedSize,
86    /// Failed to write a message to the socket
87    #[snafu(display("Failed to send CAN message: {message}"))]
88    SocketSendFailed {
89        /// A string describing the error reason
90        message: String,
91    },
92    /// An SDO server shrunk the block size while requesting retransmission
93    ///
94    /// Hopefully no node will ever do this, but it's a possible corner case, since servers are
95    /// allowed to change the block size between each block, and can request resend of part of a
96    /// block by not acknowledging all segments.
97    BlockSizeChangedTooSmall,
98    /// The CRC on a block upload did not match
99    CrcMismatch,
100}
101
102type Result<T> = std::result::Result<T, SdoClientError>;
103
104/// Convenience macro for expecting a particular variant of a response and erroring on abort of
105/// unexpected variant
106macro_rules! match_response  {
107    ($resp: ident, $expecting: literal, $($match:pat => $code : expr),*) => {
108                match $resp {
109                    $($match => $code),*
110                    SdoResponse::Abort {
111                        index,
112                        sub,
113                        abort_code,
114                    } => {
115                        return ServerAbortSnafu {
116                            index,
117                            sub,
118                            abort_code,
119                        }
120                        .fail()
121                    }
122                    _ => {
123                        return UnexpectedResponseSnafu {
124                            expecting: $expecting,
125                            response: $resp,
126                        }
127                        .fail()
128                    }
129                }
130    };
131}
132
133use paste::paste;
134macro_rules! access_methods {
135    ($type: ty) => {
136
137        paste! {
138            #[doc = concat!("Read a ", stringify!($type), " sub object from the SDO server")]
139            pub async fn [<read_ $type>](&mut self, index: u16, sub: u8) -> Result<$type> {
140                let data = self.upload(index, sub).await?;
141                if data.len() != <$type as ReadSize>::READ_SIZE {
142                    return UnexpectedSizeSnafu.fail();
143                }
144                Ok($type::from_le_bytes(data.try_into().unwrap()))
145            }
146
147            #[doc = concat!("Write a ", stringify!($type), " sub object from the SDO server")]
148            pub async fn [<write_ $type>](&mut self, index: u16, sub: u8, value: $type) -> Result<()> {
149                let data = value.to_le_bytes();
150                self.download(index, sub, &data).await
151            }
152        }
153    };
154}
155
156#[derive(Debug)]
157/// A client for accessing a node's SDO server
158///
159/// A single server can talk to a single client at a time.
160pub struct SdoClient<S, R> {
161    req_cob_id: CanId,
162    resp_cob_id: CanId,
163    timeout: Duration,
164    sender: S,
165    receiver: R,
166}
167
168impl<S: AsyncCanSender, R: AsyncCanReceiver> SdoClient<S, R> {
169    /// Create a new SdoClient using a node ID
170    ///
171    /// Nodes have a default SDO server, which uses a COB ID based on the node ID. This is a
172    /// shortcut to create a client that that default SDO server.
173    ///
174    /// It is possible for nodes to have other SDO servers on other COB IDs, and clients for these
175    /// can be created using [`Self::new()`]
176    pub fn new_std(server_node_id: u8, sender: S, receiver: R) -> Self {
177        let req_cob_id = CanId::Std(0x600 + server_node_id as u16);
178        let resp_cob_id = CanId::Std(0x580 + server_node_id as u16);
179        Self::new(req_cob_id, resp_cob_id, sender, receiver)
180    }
181
182    /// Create a new SdoClient from request and response COB IDs
183    pub fn new(req_cob_id: CanId, resp_cob_id: CanId, sender: S, receiver: R) -> Self {
184        Self {
185            req_cob_id,
186            resp_cob_id,
187            timeout: DEFAULT_RESPONSE_TIMEOUT,
188            sender,
189            receiver,
190        }
191    }
192
193    /// Set the timeout for waiting on SDO server responses
194    pub fn set_timeout(&mut self, timeout: Duration) {
195        self.timeout = timeout;
196    }
197
198    /// Get the current timeout for waiting on SDO server responses
199    pub fn get_timeout(&self) -> Duration {
200        self.timeout
201    }
202
203    async fn send(&mut self, data: [u8; 8]) -> Result<()> {
204        let frame = CanMessage::new(self.req_cob_id, &data);
205        let mut tries = 3;
206        loop {
207            match self.sender.send(frame).await {
208                Ok(()) => return Ok(()),
209                Err(e) => {
210                    tries -= 1;
211                    tokio::time::sleep(Duration::from_millis(5)).await;
212                    if tries == 0 {
213                        return SocketSendFailedSnafu {
214                            message: e.message(),
215                        }
216                        .fail();
217                    }
218                }
219            }
220        }
221    }
222
223    /// Write data to a sub-object on the SDO server
224    pub async fn download(&mut self, index: u16, sub: u8, data: &[u8]) -> Result<()> {
225        if data.len() <= 4 {
226            // Do an expedited transfer
227            self.send(SdoRequest::expedited_download(index, sub, data).to_bytes())
228                .await?;
229
230            let resp = self.wait_for_response().await?;
231            match_response!(
232                resp,
233                "ConfirmDownload",
234                SdoResponse::ConfirmDownload { index: _, sub: _ } => {
235                    Ok(()) // Success!
236                }
237            )
238        } else {
239            self.send(
240                SdoRequest::initiate_download(index, sub, Some(data.len() as u32)).to_bytes(),
241            )
242            .await?;
243
244            let resp = self.wait_for_response().await?;
245            match_response!(
246                resp,
247                "ConfirmDownload",
248                SdoResponse::ConfirmDownload { index: _, sub: _ } => { }
249            );
250
251            let mut toggle = false;
252            // Send segments
253            let total_segments = data.len().div_ceil(7);
254            for n in 0..total_segments {
255                let last_segment = n == total_segments - 1;
256                let segment_size = (data.len() - n * 7).min(7);
257                let seg_msg = SdoRequest::download_segment(
258                    toggle,
259                    last_segment,
260                    &data[n * 7..n * 7 + segment_size],
261                );
262                self.send(seg_msg.to_bytes()).await?;
263                let resp = self.wait_for_response().await?;
264                match_response!(
265                    resp,
266                    "ConfirmDownloadSegment",
267                    SdoResponse::ConfirmDownloadSegment { t } => {
268                        // Fail if toggle value doesn't match
269                        if t != toggle {
270                            let abort_msg =
271                                SdoRequest::abort(index, sub, AbortCode::ToggleNotAlternated);
272
273                            self.send(abort_msg.to_bytes())
274                                .await?;
275                            return ToggleNotAlternatedSnafu.fail();
276                        }
277                        // Otherwise, carry on
278                    }
279                );
280                toggle = !toggle;
281            }
282            Ok(())
283        }
284    }
285
286    /// Read a sub-object on the SDO server
287    pub async fn upload(&mut self, index: u16, sub: u8) -> Result<Vec<u8>> {
288        let mut read_buf = Vec::new();
289
290        self.send(SdoRequest::initiate_upload(index, sub).to_bytes())
291            .await?;
292
293        let resp = self.wait_for_response().await?;
294
295        let expedited = match_response!(
296            resp,
297            "ConfirmUpload",
298            SdoResponse::ConfirmUpload {
299                n,
300                e,
301                s,
302                index: _,
303                sub: _,
304                data,
305            } => {
306                if e {
307                    let mut len = 0;
308                    if s {
309                        len = 4 - n as usize;
310                    }
311                    read_buf.extend_from_slice(&data[0..len]);
312                }
313                e
314            }
315        );
316
317        if !expedited {
318            // Read segments
319            let mut toggle = false;
320            loop {
321                self.send(SdoRequest::upload_segment_request(toggle).to_bytes())
322                    .await?;
323
324                let resp = self.wait_for_response().await?;
325                match_response!(
326                    resp,
327                    "UploadSegment",
328                    SdoResponse::UploadSegment { t, n, c, data } => {
329                        if t != toggle {
330                            self.send(
331                                    SdoRequest::abort(index, sub, AbortCode::ToggleNotAlternated)
332                                        .to_bytes(),
333                                )
334                                .await?;
335                            return ToggleNotAlternatedSnafu.fail();
336                        }
337                        read_buf.extend_from_slice(&data[0..7 - n as usize]);
338                        if c {
339                            // Transfer complete
340                            break;
341                        }
342                    }
343                );
344                toggle = !toggle;
345            }
346        }
347        Ok(read_buf)
348    }
349
350    /// Perform a block download to transfer data to an object
351    ///
352    /// Block downloads are more efficient for large amounts of data, but may not be supported by
353    /// all devices.
354    pub async fn block_download(&mut self, index: u16, sub: u8, data: &[u8]) -> Result<()> {
355        self.send(
356            SdoRequest::InitiateBlockDownload {
357                cc: true, // CRC supported
358                s: true,  // size specified
359                index,
360                sub,
361                size: data.len() as u32,
362            }
363            .to_bytes(),
364        )
365        .await?;
366
367        let resp = self.wait_for_response().await?;
368
369        let (crc_enabled, mut blksize) = match_response!(
370            resp,
371            "ConfirmBlockDownload",
372            SdoResponse::ConfirmBlockDownload {
373                sc,
374                index: resp_index,
375                sub: resp_sub,
376                blksize,
377            } => {
378                if index != resp_index || sub != resp_sub {
379                    return MismatchedObjectIndexSnafu {
380                        expected: (index, sub),
381                        received: (resp_index, resp_sub),
382                    }
383                    .fail();
384                }
385                (sc, blksize)
386            }
387        );
388
389        let mut seqnum = 1;
390        let mut last_block_start = 0;
391        let mut segment_num = 0;
392        let total_segments = data.len().div_ceil(7);
393
394        while segment_num < total_segments {
395            let segment_start = segment_num * 7;
396            let segment_len = (data.len() - segment_start).min(7);
397            // Is this the last segment?
398            let c = segment_start + segment_len == data.len();
399            let mut segment_data = [0; 7];
400            segment_data[0..segment_len]
401                .copy_from_slice(&data[segment_start..segment_start + segment_len]);
402
403            // Send the segment
404            let segment = BlockSegment {
405                c,
406                seqnum,
407                data: segment_data,
408            };
409            self.send(segment.to_bytes()).await?;
410
411            // Expect a confirmation message after blksize segments are sent, or after sending the
412            // complete flag
413            if c || seqnum == blksize {
414                let resp = self.wait_for_response().await?;
415                match_response!(
416                    resp,
417                    "ConfirmBlock",
418                    SdoResponse::ConfirmBlock {
419                        ackseq,
420                        blksize: new_blksize,
421                    } => {
422                        if ackseq == blksize {
423                            // All segments are acknowledged. Block accepted
424                            seqnum = 1;
425                            segment_num += 1;
426                            last_block_start = segment_num;
427                        } else {
428                            // Missing segments. Resend all segments after ackseq
429                            seqnum = ackseq;
430                            segment_num = last_block_start + ackseq as usize;
431                            // The spec says the block size given by the server can change between
432                            // blocks. What should a client do if it is going to resend a block, and
433                            // the server sets the block size smaller than the already delivered
434                            // segments? This shouldn't happen I think, but, it's possible.
435                            // zencan-node based nodes won't do it, but there are other devices out
436                            // there.
437                            if new_blksize < seqnum {
438                                return BlockSizeChangedTooSmallSnafu.fail();
439                            }
440                        }
441                        blksize = new_blksize;
442                    }
443                );
444            } else {
445                seqnum += 1;
446                segment_num += 1;
447            }
448        }
449
450        // End the download
451        let crc = if crc_enabled {
452            crc16::State::<crc16::XMODEM>::calculate(data)
453        } else {
454            0
455        };
456
457        let n = ((7 - data.len() % 7) % 7) as u8;
458
459        self.send(SdoRequest::EndBlockDownload { n, crc }.to_bytes())
460            .await?;
461
462        let resp = self.wait_for_response().await?;
463        match_response!(
464            resp,
465            "ConfirmBlockDownloadEnd",
466            SdoResponse::ConfirmBlockDownloadEnd => { Ok(()) }
467        )
468    }
469
470    /// Perform a block upload of data from the node
471    pub async fn block_upload(&mut self, index: u16, sub: u8) -> Result<Vec<u8>> {
472        const CRC_SUPPORTED: bool = true;
473        const BLKSIZE: u8 = 127;
474        const PST: u8 = 0;
475        self.send(
476            SdoRequest::initiate_block_upload(index, sub, CRC_SUPPORTED, BLKSIZE, PST).to_bytes(),
477        )
478        .await?;
479
480        let resp = self.wait_for_response().await?;
481
482        let server_supports_crc = match_response!(
483            resp,
484            "ConfirmBlockUpload",
485            SdoResponse::ConfirmBlockUpload { sc, s: _, index: _, sub: _, size: _ } => {sc}
486        );
487
488        self.send(SdoRequest::StartBlockUpload.to_bytes()).await?;
489
490        let mut rx_data = Vec::new();
491        let last_segment;
492        loop {
493            let segment = self.wait_for_block_segment().await?;
494            rx_data.extend_from_slice(&segment.data);
495            if !segment.c && segment.seqnum == BLKSIZE {
496                // Finished sub block, but not yet done. Confirm this sub block and expect more
497                self.send(
498                    SdoRequest::ConfirmBlock {
499                        ackseq: BLKSIZE,
500                        blksize: BLKSIZE,
501                    }
502                    .to_bytes(),
503                )
504                .await?;
505            }
506            if segment.c {
507                last_segment = segment.seqnum;
508                break;
509            }
510        }
511
512        // NOTE: Ignoring the possibility of dropped messages here. Should check seqno to make sure
513        // all blocks are received.
514        self.send(
515            SdoRequest::ConfirmBlock {
516                ackseq: last_segment,
517                blksize: BLKSIZE,
518            }
519            .to_bytes(),
520        )
521        .await?;
522
523        let resp = self.wait_for_response().await?;
524        let (n, crc) = match_response!(
525            resp,
526            "BlockUploadEnd",
527            SdoResponse::BlockUploadEnd { n, crc } => {(n, crc)}
528        );
529
530        // Drop the n invalid data bytes
531        rx_data.resize(rx_data.len() - n as usize, 0);
532
533        if server_supports_crc {
534            let computed_crc = crc16::State::<crc16::XMODEM>::calculate(&rx_data);
535            if crc != computed_crc {
536                self.send(SdoRequest::abort(index, sub, AbortCode::CrcError).to_bytes())
537                    .await?;
538                return Err(SdoClientError::CrcMismatch);
539            }
540        }
541
542        self.send(SdoRequest::EndBlockUpload.to_bytes()).await?;
543
544        Ok(rx_data)
545    }
546
547    access_methods!(f64);
548    access_methods!(f32);
549    access_methods!(u64);
550    access_methods!(u32);
551    access_methods!(u24);
552    access_methods!(u16);
553    access_methods!(u8);
554    access_methods!(i64);
555    access_methods!(i32);
556    access_methods!(i24);
557    access_methods!(i16);
558    access_methods!(i8);
559
560    /// Write to a TimeOfDay object on the SDO server
561    pub async fn write_time_of_day(&mut self, index: u16, sub: u8, data: TimeOfDay) -> Result<()> {
562        let data = data.to_le_bytes();
563        self.download(index, sub, &data).await
564    }
565
566    /// Write to a TimeDifference object on the SDO server
567    pub async fn write_time_difference(
568        &mut self,
569        index: u16,
570        sub: u8,
571        data: TimeDifference,
572    ) -> Result<()> {
573        let data = data.to_le_bytes();
574        self.download(index, sub, &data).await
575    }
576
577    /// Read a string from the SDO server
578    pub async fn read_utf8(&mut self, index: u16, sub: u8) -> Result<String> {
579        let data = self.upload(index, sub).await?;
580        Ok(String::from_utf8_lossy(&data).into())
581    }
582
583    /// Read a TimeOfDay object from the SDO server
584    pub async fn read_time_of_day(&mut self, index: u16, sub: u8) -> Result<TimeOfDay> {
585        let data = self.upload(index, sub).await?;
586        if data.len() != TimeOfDay::SIZE {
587            UnexpectedSizeSnafu.fail()
588        } else {
589            Ok(TimeOfDay::from_le_bytes(data.try_into().unwrap()))
590        }
591    }
592
593    /// Read a TimeOfDay object from the SDO server
594    pub async fn read_time_difference(&mut self, index: u16, sub: u8) -> Result<TimeDifference> {
595        let data = self.upload(index, sub).await?;
596        if data.len() != TimeDifference::SIZE {
597            UnexpectedSizeSnafu.fail()
598        } else {
599            Ok(TimeDifference::from_le_bytes(data.try_into().unwrap()))
600        }
601    }
602
603    /// Read an object as a visible string
604    ///
605    /// It will be read and assumed to contain valid UTF8 characters
606    pub async fn read_visible_string(&mut self, index: u16, sub: u8) -> Result<String> {
607        let bytes = self.upload(index, sub).await?;
608        Ok(String::from_utf8_lossy(&bytes).into())
609    }
610
611    /// Read an object as a boolean
612    pub async fn read_bool(&mut self, index: u16, sub: u8) -> Result<bool> {
613        let bytes = self.upload(index, sub).await?;
614        if bytes.len() != 1 {
615            return UnexpectedSizeSnafu.fail();
616        }
617        Ok(bytes[0] != 0)
618    }
619
620    /// Write an object as a boolean
621    pub async fn write_bool(&mut self, index: u16, sub: u8, value: bool) -> Result<()> {
622        let data = if value { [1u8] } else { [0u8] };
623        self.download(index, sub, &data).await
624    }
625
626    /// Read the identity object
627    ///
628    /// All nodes should implement this object
629    pub async fn read_identity(&mut self) -> Result<LssIdentity> {
630        let vendor_id = self.read_u32(object_ids::IDENTITY, 1).await?;
631        let product_code = self.read_u32(object_ids::IDENTITY, 2).await?;
632        let revision_number = self.read_u32(object_ids::IDENTITY, 3).await?;
633        let serial = self.read_u32(object_ids::IDENTITY, 4).await?;
634        Ok(LssIdentity::new(
635            vendor_id,
636            product_code,
637            revision_number,
638            serial,
639        ))
640    }
641
642    /// Write object 0x1010sub1 to command all objects be saved
643    pub async fn save_objects(&mut self) -> Result<()> {
644        self.write_u32(object_ids::SAVE_OBJECTS, 1, SAVE_CMD).await
645    }
646
647    /// Read the device name object
648    ///
649    /// All nodes should implement this object
650    pub async fn read_device_name(&mut self) -> Result<String> {
651        self.read_visible_string(object_ids::DEVICE_NAME, 0).await
652    }
653
654    /// Read the software version object
655    ///
656    /// All nodes should implement this object
657    pub async fn read_software_version(&mut self) -> Result<String> {
658        self.read_visible_string(object_ids::SOFTWARE_VERSION, 0)
659            .await
660    }
661
662    /// Read the hardware version object
663    ///
664    /// All nodes should implement this object
665    pub async fn read_hardware_version(&mut self) -> Result<String> {
666        self.read_visible_string(object_ids::HARDWARE_VERSION, 0)
667            .await
668    }
669
670    /// Configure a transmit PDO on the device
671    ///
672    /// This is a convenience function to write the PDO comm and mapping objects based on a
673    /// [`PdoConfig`].
674    pub async fn configure_tpdo(&mut self, pdo_num: usize, cfg: &PdoConfig) -> Result<()> {
675        let comm_index = 0x1800 + pdo_num as u16;
676        let mapping_index = 0x1a00 + pdo_num as u16;
677        self.store_pdo_config(comm_index, mapping_index, cfg).await
678    }
679
680    /// Configure a receive PDO on the device
681    ///
682    /// This is a convenience function to write the PDO comm and mapping objects based on a
683    /// [`PdoConfig`].
684    pub async fn configure_rpdo(&mut self, pdo_num: usize, cfg: &PdoConfig) -> Result<()> {
685        let comm_index = 0x1400 + pdo_num as u16;
686        let mapping_index = 0x1600 + pdo_num as u16;
687        self.store_pdo_config(comm_index, mapping_index, cfg).await
688    }
689
690    /// Set the COB_ID config for an RPDO
691    ///
692    /// Can be used to enable/disable, or change COB ID for a PDO without changing other settings
693    pub async fn set_rpdo_cob_id(
694        &mut self,
695        pdo_num: usize,
696        cob_id: CanId,
697        valid: bool,
698        rtr_disabled: bool,
699    ) -> Result<()> {
700        let comm_index = 0x1400 + pdo_num as u16;
701        self.set_pdo_cob_id(comm_index, cob_id, valid, rtr_disabled)
702            .await
703    }
704
705    /// Set the COB_ID config for an RPDO
706    ///
707    /// Can be used to enable/disable, or change COB ID for a PDO without changing other settings
708    pub async fn set_tpdo_cob_id(
709        &mut self,
710        pdo_num: usize,
711        cob_id: CanId,
712        valid: bool,
713        rtr_disabled: bool,
714    ) -> Result<()> {
715        let comm_index = 0x1800 + pdo_num as u16;
716        self.set_pdo_cob_id(comm_index, cob_id, valid, rtr_disabled)
717            .await
718    }
719
720    async fn set_pdo_cob_id(
721        &mut self,
722        comm_index: u16,
723        cob_id: CanId,
724        valid: bool,
725        rtr_disabled: bool,
726    ) -> Result<()> {
727        let mut cob_value = cob_id.raw() & 0x1FFFFFFF;
728        if !valid {
729            cob_value |= 1 << 31;
730        }
731        if cob_id.is_extended() {
732            cob_value |= 1 << 29;
733        }
734        if rtr_disabled {
735            cob_value |= 1 << 30;
736        }
737        self.write_u32(comm_index, 1, cob_value).await?;
738
739        Ok(())
740    }
741
742    /// Write to a PDO Comm parameter
743    async fn set_pdo_comm_parameter(
744        &mut self,
745        comm_index: u16,
746        comm: PdoCommParameter,
747    ) -> Result<()> {
748        self.write_u8(comm_index, 2, comm.transmission_type).await?;
749        self.set_pdo_cob_id(comm_index, comm.cob_id, comm.valid, comm.rtr_disabled)
750            .await?;
751        Ok(())
752    }
753
754    async fn store_pdo_config(
755        &mut self,
756        comm_index: u16,
757        mapping_index: u16,
758        cfg: &PdoConfig,
759    ) -> Result<()> {
760        let disabled_comm = PdoCommParameter {
761            valid: false,
762            ..cfg.comm
763        };
764
765        // Ensure PDO is disabled
766        self.set_pdo_comm_parameter(comm_index, disabled_comm)
767            .await?;
768
769        // Set the number of valid mappings to 0
770        self.write_u8(mapping_index, 0, 0).await?;
771
772        // Write the mappings
773        assert!(cfg.mappings.len() < 0x40);
774        for (i, m) in cfg.mappings.iter().enumerate() {
775            let mapping_value = m.to_object_value();
776            self.write_u32(mapping_index, (i + 1) as u8, mapping_value)
777                .await?;
778        }
779
780        // Set the number of valid mappings to the number configured
781        let num_mappings = cfg.mappings.len() as u8;
782        self.write_u8(mapping_index, 0, num_mappings).await?;
783
784        // Make PDO valid, if requested
785        if cfg.comm.valid {
786            self.set_pdo_comm_parameter(comm_index, cfg.comm).await?;
787        }
788        Ok(())
789    }
790
791    /// Read the configuration of an RPDO from the node
792    pub async fn read_rpdo_config(&mut self, pdo_num: usize) -> Result<PdoConfig> {
793        let comm_index = 0x1400 + pdo_num as u16;
794        let mapping_index = 0x1600 + pdo_num as u16;
795        self.read_pdo_config(comm_index, mapping_index).await
796    }
797
798    /// Read the configuration of a TPDO from the node
799    pub async fn read_tpdo_config(&mut self, pdo_num: usize) -> Result<PdoConfig> {
800        let comm_index = 0x1800 + pdo_num as u16;
801        let mapping_index = 0x1a00 + pdo_num as u16;
802        self.read_pdo_config(comm_index, mapping_index).await
803    }
804
805    async fn read_pdo_config(&mut self, comm_index: u16, mapping_index: u16) -> Result<PdoConfig> {
806        let cob_word = self.read_u32(comm_index, 1).await?;
807        let transmission_type = self.read_u8(comm_index, 2).await?;
808        let num_mappings = self.read_u8(mapping_index, 0).await?;
809        let mut mappings = Vec::with_capacity(num_mappings as usize);
810        for i in 0..num_mappings {
811            let mapping_raw = self.read_u32(mapping_index, i + 1).await?;
812            mappings.push(PdoMapping::from_object_value(mapping_raw));
813        }
814        let valid = cob_word & (1 << 31) == 0;
815        let rtr_disabled = cob_word & (1 << 30) != 0;
816        let extended = cob_word & (1 << 29) != 0;
817        let cob_id = cob_word & 0x1FFFFFFF;
818        let cob_id = if extended {
819            CanId::extended(cob_id)
820        } else {
821            CanId::std(cob_id as u16)
822        };
823        Ok(PdoConfig {
824            comm: PdoCommParameter {
825                valid,
826                rtr_disabled,
827                cob_id,
828                transmission_type,
829            },
830            mappings,
831        })
832    }
833
834    async fn wait_for_block_segment(&mut self) -> Result<BlockSegment> {
835        let wait_until = tokio::time::Instant::now() + self.timeout;
836        loop {
837            match tokio::time::timeout_at(wait_until, self.receiver.recv()).await {
838                // Err indicates the timeout elapsed, so return
839                Err(_) => return NoResponseSnafu.fail(),
840                // Message was recieved. If it is the resp, return. Otherwise, keep waiting
841                Ok(Ok(msg)) => {
842                    if msg.id == self.resp_cob_id {
843                        return msg
844                            .data()
845                            .try_into()
846                            .map_err(|_| MalformedResponseSnafu.build());
847                    }
848                }
849                // Recv returned an error
850                Ok(Err(e)) => {
851                    log::error!("Error reading from socket: {e:?}");
852                    return NoResponseSnafu.fail();
853                }
854            }
855        }
856    }
857
858    async fn wait_for_response(&mut self) -> Result<SdoResponse> {
859        let wait_until = tokio::time::Instant::now() + self.timeout;
860        loop {
861            match tokio::time::timeout_at(wait_until, self.receiver.recv()).await {
862                // Err indicates the timeout elapsed, so return
863                Err(_) => return NoResponseSnafu.fail(),
864                // Message was recieved. If it is the resp, return. Otherwise, keep waiting
865                Ok(Ok(msg)) => {
866                    if msg.id == self.resp_cob_id {
867                        return msg.try_into().map_err(|_| MalformedResponseSnafu.build());
868                    }
869                }
870                // Recv returned an error
871                Ok(Err(e)) => {
872                    log::error!("Error reading from socket: {e:?}");
873                    return NoResponseSnafu.fail();
874                }
875            }
876        }
877    }
878}