zencan_client/
sdo_client.rs

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