Skip to main content

mcumgr_toolkit/
connection.rs

1use std::{io::Cursor, sync::Mutex, time::Duration};
2
3use crate::{
4    DEFAULT_RETRIES,
5    commands::{ErrResponse, ErrResponseV2, McuMgrCommand},
6    smp_errors::{DeviceError, MCUmgrErr},
7    transport::{ReceiveError, SendError, Transport},
8};
9
10use miette::{Diagnostic, IntoDiagnostic};
11use polonius_the_crab::prelude::*;
12use thiserror::Error;
13
14struct Transceiver {
15    transport: Box<dyn Transport + Send>,
16    next_seqnum: u8,
17    receive_buffer: Box<[u8; u16::MAX as usize]>,
18}
19
20struct Inner {
21    transceiver: Transceiver,
22    send_buffer: Box<[u8; u16::MAX as usize]>,
23    retries: u8,
24}
25
26/// An SMP protocol layer connection to a device.
27///
28/// In most cases this struct will not be used directly by the user,
29/// but instead it is used indirectly through [`MCUmgrClient`](crate::MCUmgrClient).
30pub struct Connection {
31    inner: Mutex<Inner>,
32}
33
34/// Errors that can happen on SMP protocol level
35#[derive(Error, Debug, Diagnostic)]
36pub enum ExecuteError {
37    /// An error happened on SMP transport level while sending a request
38    #[error("Sending failed")]
39    #[diagnostic(code(mcumgr_toolkit::connection::execute::send))]
40    SendFailed(#[from] SendError),
41    /// An error happened on SMP transport level while receiving a response
42    #[error("Receiving failed")]
43    #[diagnostic(code(mcumgr_toolkit::connection::execute::receive))]
44    ReceiveFailed(#[from] ReceiveError),
45    /// An error happened while CBOR encoding the request payload
46    #[error("CBOR encoding failed")]
47    #[diagnostic(code(mcumgr_toolkit::connection::execute::encode))]
48    EncodeFailed(#[source] Box<dyn miette::Diagnostic + Send + Sync>),
49    /// An error happened while CBOR decoding the response payload
50    #[error("CBOR decoding failed")]
51    #[diagnostic(code(mcumgr_toolkit::connection::execute::decode))]
52    DecodeFailed(#[source] Box<dyn miette::Diagnostic + Send + Sync>),
53    /// The device returned an SMP error
54    #[error("Device returned error code: {0}")]
55    #[diagnostic(code(mcumgr_toolkit::connection::execute::device_error))]
56    ErrorResponse(DeviceError),
57}
58
59impl ExecuteError {
60    /// Checks if the device reported the command as unsupported
61    pub fn command_not_supported(&self) -> bool {
62        if let Self::ErrorResponse(DeviceError::V1 { rc, .. }) = self {
63            *rc == MCUmgrErr::MGMT_ERR_ENOTSUP as i32
64        } else {
65            false
66        }
67    }
68}
69
70impl Transceiver {
71    fn transceive_command(
72        &mut self,
73        write_operation: bool,
74        group_id: u16,
75        command_id: u8,
76        data: &[u8],
77    ) -> Result<&'_ [u8], ExecuteError> {
78        let sequence_num = self.next_seqnum;
79        self.next_seqnum = self.next_seqnum.wrapping_add(1);
80
81        self.transport
82            .send_frame(write_operation, sequence_num, group_id, command_id, data)?;
83
84        self.transport
85            .receive_frame(
86                &mut self.receive_buffer,
87                write_operation,
88                sequence_num,
89                group_id,
90                command_id,
91            )
92            .map_err(Into::into)
93    }
94
95    fn transceive_command_with_retries(
96        &mut self,
97        write_operation: bool,
98        group_id: u16,
99        command_id: u8,
100        data: &[u8],
101        num_retries: u8,
102    ) -> Result<&'_ [u8], ExecuteError> {
103        let mut this = self;
104
105        let mut counter = 0;
106
107        polonius_loop!(|this| -> Result<&'polonius [u8], ExecuteError> {
108            let result = this.transceive_command(write_operation, group_id, command_id, data);
109
110            if counter >= num_retries {
111                polonius_return!(result)
112            }
113            counter += 1;
114
115            match result {
116                Ok(_) => polonius_return!(result),
117                Err(e) => {
118                    let mut lowest_err: &dyn std::error::Error = &e;
119                    while let Some(lower_err) = lowest_err.source() {
120                        lowest_err = lower_err;
121                    }
122                    log::warn!("Retry transmission, error occurred: {lowest_err}");
123                }
124            }
125        })
126    }
127}
128
129impl Connection {
130    /// Creates a new SMP connection
131    pub fn new<T: Transport + Send + 'static>(transport: T) -> Self {
132        Self {
133            inner: Mutex::new(Inner {
134                transceiver: Transceiver {
135                    transport: Box::new(transport),
136                    next_seqnum: rand::random(),
137                    receive_buffer: Box::new([0; u16::MAX as usize]),
138                },
139                send_buffer: Box::new([0; u16::MAX as usize]),
140                retries: DEFAULT_RETRIES,
141            }),
142        }
143    }
144
145    /// Changes the communication timeout.
146    ///
147    /// When the device does not respond to packets within the set
148    /// duration, an error will be raised.
149    pub fn set_timeout(
150        &self,
151        timeout: Duration,
152    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
153        self.inner
154            .lock()
155            .unwrap()
156            .transceiver
157            .transport
158            .set_timeout(timeout)
159    }
160
161    /// Changes the retry amount.
162    ///
163    /// When the device encounters a transport error, it will retry
164    /// this many times until giving up.
165    pub fn set_retries(&self, retries: u8) {
166        self.inner.lock().unwrap().retries = retries;
167    }
168
169    /// Executes a given CBOR based SMP command.
170    pub fn execute_command<R: McuMgrCommand>(
171        &self,
172        request: &R,
173    ) -> Result<R::Response, ExecuteError> {
174        self.execute_command_impl(request, true)
175    }
176
177    /// Executes a given CBOR based SMP command.
178    ///
179    /// Does not use retries.
180    pub fn execute_command_without_retries<R: McuMgrCommand>(
181        &self,
182        request: &R,
183    ) -> Result<R::Response, ExecuteError> {
184        self.execute_command_impl(request, false)
185    }
186
187    fn execute_command_impl<R: McuMgrCommand>(
188        &self,
189        request: &R,
190        use_retries: bool,
191    ) -> Result<R::Response, ExecuteError> {
192        let mut lock_guard = self.inner.lock().unwrap();
193        let locked_self: &mut Inner = &mut lock_guard;
194
195        let mut cursor = Cursor::new(locked_self.send_buffer.as_mut_slice());
196        ciborium::into_writer(request.data(), &mut cursor)
197            .into_diagnostic()
198            .map_err(Into::into)
199            .map_err(ExecuteError::EncodeFailed)?;
200        let data_size = cursor.position() as usize;
201        let data = &locked_self.send_buffer[..data_size];
202
203        log::debug!("TX data: {}", hex::encode(data));
204
205        let write_operation = request.is_write_operation();
206        let group_id = request.group_id();
207        let command_id = request.command_id();
208
209        let response = locked_self.transceiver.transceive_command_with_retries(
210            write_operation,
211            group_id,
212            command_id,
213            data,
214            if use_retries { locked_self.retries } else { 0 },
215        )?;
216
217        log::debug!("RX data: {}", hex::encode(response));
218
219        let err: ErrResponse = ciborium::from_reader(Cursor::new(response))
220            .into_diagnostic()
221            .map_err(Into::into)
222            .map_err(ExecuteError::DecodeFailed)?;
223
224        if let Some(ErrResponseV2 { rc, group }) = err.err {
225            return Err(ExecuteError::ErrorResponse(DeviceError::V2 { group, rc }));
226        }
227
228        if let Some(rc) = err.rc {
229            if rc != MCUmgrErr::MGMT_ERR_EOK as i32 {
230                return Err(ExecuteError::ErrorResponse(DeviceError::V1 {
231                    rc,
232                    rsn: err.rsn,
233                }));
234            }
235        }
236
237        let decoded_response: R::Response = ciborium::from_reader(Cursor::new(response))
238            .into_diagnostic()
239            .map_err(Into::into)
240            .map_err(ExecuteError::DecodeFailed)?;
241
242        Ok(decoded_response)
243    }
244
245    /// Executes a raw SMP command.
246    ///
247    /// Same as [`Connection::execute_command`], but the payload can be anything and must not
248    /// necessarily be CBOR encoded.
249    ///
250    /// Errors are also not decoded but instead will be returned as raw CBOR data.
251    ///
252    /// Read Zephyr's [SMP Protocol Specification](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_protocol.html)
253    /// for more information.
254    pub fn execute_raw_command(
255        &self,
256        write_operation: bool,
257        group_id: u16,
258        command_id: u8,
259        data: &[u8],
260        use_retries: bool,
261    ) -> Result<Box<[u8]>, ExecuteError> {
262        let mut lock_guard = self.inner.lock().unwrap();
263        let locked_self: &mut Inner = &mut lock_guard;
264
265        locked_self
266            .transceiver
267            .transceive_command_with_retries(
268                write_operation,
269                group_id,
270                command_id,
271                data,
272                if use_retries { locked_self.retries } else { 0 },
273            )
274            .map(|val| val.into())
275    }
276}