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