Skip to main content

mcumgr_toolkit/commands/
os.rs

1use std::collections::HashMap;
2
3use chrono::Timelike;
4use serde::{Deserialize, Serialize};
5
6use super::{
7    is_default,
8    macros::{impl_deserialize_from_empty_map_and_into_unit, impl_serialize_as_empty_map},
9};
10
11/// [Echo](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#echo-command) command
12#[derive(Debug, Serialize, Eq, PartialEq)]
13pub struct Echo<'a> {
14    /// string to be replied by echo service
15    pub d: &'a str,
16}
17
18/// Response for [`Echo`] command
19#[derive(Debug, Deserialize, Eq, PartialEq)]
20pub struct EchoResponse {
21    /// replying echo string
22    pub r: String,
23}
24
25/// [Task statistics](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#task-statistics-command) command
26#[derive(Debug, Eq, PartialEq)]
27pub struct TaskStatistics;
28impl_serialize_as_empty_map!(TaskStatistics);
29
30/// Statistics of an MCU task/thread
31#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
32pub struct TaskStatisticsEntry {
33    /// task priority
34    pub prio: i32,
35    /// numeric task ID
36    pub tid: u32,
37    /// numeric task state
38    pub state: u32,
39    /// task’s/thread’s stack usage
40    pub stkuse: Option<u64>,
41    /// task’s/thread’s stack size
42    pub stksiz: Option<u64>,
43    /// task’s/thread’s context switches
44    pub cswcnt: Option<u64>,
45    /// task’s/thread’s runtime in “ticks”
46    pub runtime: Option<u64>,
47}
48
49/// Flags inside of [`TaskStatisticsEntry::state`]
50#[derive(strum::Display, strum::AsRefStr, strum::EnumIter, Debug, Copy, Clone, PartialEq, Eq)]
51#[repr(u8)]
52#[strum(serialize_all = "snake_case")]
53pub enum ThreadStateFlags {
54    /** Not a real thread */
55    DUMMY = 1 << 0,
56
57    /** Thread is waiting on an object */
58    PENDING = 1 << 1,
59
60    /** Thread is sleeping */
61    SLEEPING = 1 << 2,
62
63    /** Thread has terminated */
64    DEAD = 1 << 3,
65
66    /** Thread is suspended */
67    SUSPENDED = 1 << 4,
68
69    /** Thread is in the process of aborting */
70    ABORTING = 1 << 5,
71
72    /** Thread is in the process of suspending */
73    SUSPENDING = 1 << 6,
74
75    /** Thread is present in the ready queue */
76    QUEUED = 1 << 7,
77}
78
79impl ThreadStateFlags {
80    /// Converts the thread state to a human readable string
81    pub fn pretty_print(thread_state: u8) -> String {
82        use strum::IntoEnumIterator;
83
84        let mut bit_names = vec![];
85        for bit in Self::iter() {
86            if (thread_state & bit as u8) != 0 {
87                bit_names.push(format!("{bit}"));
88            }
89        }
90
91        bit_names.join(" | ")
92    }
93}
94
95/// Response for [`TaskStatistics`] command
96#[derive(Debug, Deserialize, Eq, PartialEq)]
97pub struct TaskStatisticsResponse {
98    /// Dictionary of task names with their respective statistics
99    pub tasks: HashMap<String, TaskStatisticsEntry>,
100}
101
102/// Parses a [`chrono::NaiveDateTime`] object with optional timezone specifiers
103fn deserialize_datetime_and_ignore_timezone<'de, D>(
104    de: D,
105) -> Result<chrono::NaiveDateTime, D::Error>
106where
107    D: serde::Deserializer<'de>,
108{
109    #[derive(Deserialize)]
110    #[serde(untagged)]
111    enum NaiveOrFixed {
112        Naive(chrono::NaiveDateTime),
113        Fixed(chrono::DateTime<chrono::FixedOffset>),
114    }
115
116    NaiveOrFixed::deserialize(de).map(|val| match val {
117        NaiveOrFixed::Naive(naive_date_time) => naive_date_time,
118        NaiveOrFixed::Fixed(date_time) => date_time.naive_local(),
119    })
120}
121
122/// Serializes a [`chrono::NaiveDateTime`] object with zero or three fractional digits,
123/// which is most compatible with Zephyr
124fn serialize_datetime_for_zephyr<S>(
125    value: &chrono::NaiveDateTime,
126    serializer: S,
127) -> Result<S::Ok, S::Error>
128where
129    S: serde::Serializer,
130{
131    if value.time().nanosecond() != 0 {
132        serializer.serialize_str(&format!("{}", value.format("%Y-%m-%dT%H:%M:%S%.3f")))
133    } else {
134        serializer.serialize_str(&format!("{}", value.format("%Y-%m-%dT%H:%M:%S")))
135    }
136}
137
138/// [Date-Time Get](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#date-time-get) command
139#[derive(Debug, Eq, PartialEq)]
140pub struct DateTimeGet;
141impl_serialize_as_empty_map!(DateTimeGet);
142
143/// Response for [`DateTimeGet`] command
144#[derive(Debug, Deserialize, Eq, PartialEq)]
145pub struct DateTimeGetResponse {
146    /// String in format: `yyyy-MM-dd'T'HH:mm:ss.SSS`.
147    #[serde(deserialize_with = "deserialize_datetime_and_ignore_timezone")]
148    pub datetime: chrono::NaiveDateTime,
149}
150
151/// [Date-Time Set](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#date-time-set) command
152#[derive(Serialize, Debug, Eq, PartialEq)]
153pub struct DateTimeSet {
154    /// String in format: `yyyy-MM-dd'T'HH:mm:ss.SSS`.
155    #[serde(serialize_with = "serialize_datetime_for_zephyr")]
156    pub datetime: chrono::NaiveDateTime,
157}
158
159/// Response for [`DateTimeSet`] command
160#[derive(Default, Debug, Eq, PartialEq)]
161pub struct DateTimeSetResponse;
162impl_deserialize_from_empty_map_and_into_unit!(DateTimeSetResponse);
163
164/// [System Reset](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#system-reset) command
165#[derive(Serialize, Debug, Eq, PartialEq)]
166pub struct SystemReset {
167    /// Forces reset
168    #[serde(skip_serializing_if = "is_default")]
169    pub force: bool,
170    /// Boot mode
171    ///
172    /// - 0: Normal boot
173    /// - 1: Bootloader recovery mode
174    ///
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub boot_mode: Option<u8>,
177}
178
179/// Response for [`SystemReset`] command
180#[derive(Default, Debug, Eq, PartialEq)]
181pub struct SystemResetResponse;
182impl_deserialize_from_empty_map_and_into_unit!(SystemResetResponse);
183
184/// [MCUmgr Parameters](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#mcumgr-parameters) command
185#[derive(Debug, Eq, PartialEq)]
186pub struct MCUmgrParameters;
187impl_serialize_as_empty_map!(MCUmgrParameters);
188
189/// Response for [`MCUmgrParameters`] command
190#[derive(Debug, Deserialize, Eq, PartialEq)]
191pub struct MCUmgrParametersResponse {
192    /// Single SMP buffer size, this includes SMP header and CBOR payload
193    pub buf_size: u32,
194    /// Number of SMP buffers supported
195    pub buf_count: u32,
196}
197
198/// [OS/Application Info](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#os-application-info) command
199#[derive(Serialize, Debug, Eq, PartialEq)]
200pub struct ApplicationInfo<'a> {
201    /// Format specifier of returned response
202    ///
203    /// For more info, see [the SMP documentation](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#os-application-info-request).
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub format: Option<&'a str>,
206}
207
208/// Response for [`ApplicationInfo`] command
209#[derive(Debug, Deserialize, Eq, PartialEq)]
210pub struct ApplicationInfoResponse {
211    /// Text response including requested parameters
212    pub output: String,
213}
214
215/// [Bootloader Information](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#bootloader-information) command
216#[derive(Debug, Eq, PartialEq)]
217pub struct BootloaderInfo;
218impl_serialize_as_empty_map!(BootloaderInfo);
219
220/// Response for [`BootloaderInfo`] command
221#[derive(Debug, Deserialize, Eq, PartialEq)]
222pub struct BootloaderInfoResponse {
223    /// String representing bootloader name
224    pub bootloader: String,
225}
226
227/// [Bootloader Information MCUboot Mode](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#bootloader-information-mcuboot) subcommand
228#[derive(Serialize, Debug, Eq, PartialEq)]
229#[serde(tag = "query", rename = "mode")]
230pub struct BootloaderInfoMcubootMode {}
231
232/// Response for [`BootloaderInfoMcubootMode`] command
233#[derive(Debug, Deserialize, Eq, PartialEq)]
234pub struct BootloaderInfoMcubootModeResponse {
235    /// The bootloader mode
236    pub mode: i32,
237    /// MCUboot has downgrade prevention enabled
238    #[serde(default, rename = "no-downgrade")]
239    pub no_downgrade: bool,
240}
241
242#[cfg(test)]
243mod tests {
244    use super::super::macros::command_encode_decode_test;
245    use super::*;
246    use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
247    use ciborium::cbor;
248
249    #[test]
250    fn thread_state_flags_to_string() {
251        assert_eq!(
252            ThreadStateFlags::pretty_print(0xff),
253            "dummy | pending | sleeping | dead | suspended | aborting | suspending | queued"
254        );
255
256        assert_eq!(ThreadStateFlags::pretty_print(0b00000001), "dummy");
257        assert_eq!(ThreadStateFlags::pretty_print(0b00000010), "pending");
258        assert_eq!(ThreadStateFlags::pretty_print(0b00000100), "sleeping");
259        assert_eq!(ThreadStateFlags::pretty_print(0b00001000), "dead");
260        assert_eq!(ThreadStateFlags::pretty_print(0b00010000), "suspended");
261        assert_eq!(ThreadStateFlags::pretty_print(0b00100000), "aborting");
262        assert_eq!(ThreadStateFlags::pretty_print(0b01000000), "suspending");
263        assert_eq!(ThreadStateFlags::pretty_print(0b10000000), "queued");
264
265        assert_eq!(ThreadStateFlags::pretty_print(0), "");
266    }
267
268    command_encode_decode_test! {
269        echo,
270        (0, 0, 0),
271        Echo{d: "Hello World!"},
272        cbor!({"d" => "Hello World!"}),
273        cbor!({"r" => "Hello World!"}),
274        EchoResponse{r: "Hello World!".to_string()},
275    }
276
277    command_encode_decode_test! {
278        task_statistics_empty,
279        (0, 0, 2),
280        TaskStatistics,
281        cbor!({}),
282        cbor!({"tasks" => {}}),
283        TaskStatisticsResponse{ tasks: HashMap::new() },
284    }
285
286    command_encode_decode_test! {
287        task_statistics,
288        (0, 0, 2),
289        TaskStatistics,
290        cbor!({}),
291        cbor!({"tasks" => {
292            "task_a" => {
293                "prio" => 20,
294                "tid" => 5,
295                "state" => 10,
296            },
297            "task_b" => {
298                "prio"         => 30,
299                "tid"          => 31,
300                "state"        => 32,
301                "stkuse"       => 33,
302                "stksiz"       => 34,
303                "cswcnt"       => 35,
304                "runtime"      => 36,
305                "last_checkin" => 0,
306                "next_checkin" => 0,
307            },
308        }}),
309        TaskStatisticsResponse{ tasks: HashMap::from([
310            (
311                "task_a".to_string(),
312                TaskStatisticsEntry{
313                    prio: 20,
314                    tid: 5,
315                    state: 10,
316                    stkuse: None,
317                    stksiz: None,
318                    cswcnt: None,
319                    runtime: None,
320                },
321            ), (
322                "task_b".to_string(),
323                TaskStatisticsEntry{
324                    prio: 30,
325                    tid: 31,
326                    state: 32,
327                    stkuse: Some(33),
328                    stksiz: Some(34),
329                    cswcnt: Some(35),
330                    runtime: Some(36),
331                },
332            ),
333        ]) },
334    }
335
336    command_encode_decode_test! {
337        datetime_get_with_timezone,
338        (0, 0, 4),
339        DateTimeGet,
340        cbor!({}),
341        cbor!({
342            "datetime" => "2025-11-20T11:56:05.366345+01:00"
343        }),
344        DateTimeGetResponse{
345            datetime: NaiveDateTime::new(NaiveDate::from_ymd_opt(2025, 11, 20).unwrap(), NaiveTime::from_hms_micro_opt(11,56,5,366345).unwrap()),
346        },
347    }
348
349    command_encode_decode_test! {
350        datetime_get_with_millis,
351        (0, 0, 4),
352        DateTimeGet,
353        cbor!({}),
354        cbor!({
355            "datetime" => "2025-11-20T11:56:05.366"
356        }),
357        DateTimeGetResponse{
358            datetime: NaiveDateTime::new(NaiveDate::from_ymd_opt(2025, 11, 20).unwrap(), NaiveTime::from_hms_milli_opt(11,56,5,366).unwrap()),
359        },
360    }
361
362    command_encode_decode_test! {
363        datetime_get_without_millis,
364        (0, 0, 4),
365        DateTimeGet,
366        cbor!({}),
367        cbor!({
368            "datetime" => "2025-11-20T11:56:05"
369        }),
370        DateTimeGetResponse{
371            datetime: NaiveDateTime::new(NaiveDate::from_ymd_opt(2025, 11, 20).unwrap(), NaiveTime::from_hms_opt(11,56,5).unwrap()),
372        },
373    }
374
375    command_encode_decode_test! {
376        datetime_set_with_millis,
377        (2, 0, 4),
378        DateTimeSet{
379            datetime: NaiveDateTime::new(NaiveDate::from_ymd_opt(2025, 11, 20).unwrap(), NaiveTime::from_hms_micro_opt(12,3,56,642133).unwrap())
380        },
381        cbor!({
382            "datetime" => "2025-11-20T12:03:56.642"
383        }),
384        cbor!({}),
385        DateTimeSetResponse,
386    }
387
388    command_encode_decode_test! {
389        datetime_set_without_millis,
390        (2, 0, 4),
391        DateTimeSet{
392            datetime: NaiveDateTime::new(NaiveDate::from_ymd_opt(2025, 11, 20).unwrap(), NaiveTime::from_hms_opt(12,3,56).unwrap())
393        },
394        cbor!({
395            "datetime" => "2025-11-20T12:03:56"
396        }),
397        cbor!({}),
398        DateTimeSetResponse,
399    }
400
401    command_encode_decode_test! {
402        system_reset_minimal,
403        (2, 0, 5),
404        SystemReset{
405            force: false,
406            boot_mode: None,
407        },
408        cbor!({}),
409        cbor!({}),
410        SystemResetResponse,
411    }
412
413    command_encode_decode_test! {
414        system_reset_full,
415        (2, 0, 5),
416        SystemReset{
417            force: true,
418            boot_mode: Some(42),
419        },
420        cbor!({
421            "force" => true,
422            "boot_mode" => 42,
423        }),
424        cbor!({}),
425        SystemResetResponse,
426    }
427
428    command_encode_decode_test! {
429        mcumgr_parameters,
430        (0, 0, 6),
431        MCUmgrParameters,
432        cbor!({}),
433        cbor!({"buf_size" => 42, "buf_count" => 69}),
434        MCUmgrParametersResponse{buf_size: 42, buf_count: 69 },
435    }
436
437    command_encode_decode_test! {
438        application_info_without_format,
439        (0, 0, 7),
440        ApplicationInfo{
441            format: None,
442        },
443        cbor!({}),
444        cbor!({
445            "output" => "foo",
446        }),
447        ApplicationInfoResponse{
448            output: "foo".to_string(),
449        }
450    }
451
452    command_encode_decode_test! {
453        application_info_with_format,
454        (0, 0, 7),
455        ApplicationInfo{
456            format: Some("abc"),
457        },
458        cbor!({
459            "format" => "abc",
460        }),
461        cbor!({
462            "output" => "bar",
463        }),
464        ApplicationInfoResponse{
465            output: "bar".to_string(),
466        }
467    }
468
469    command_encode_decode_test! {
470        bootloader_info,
471        (0, 0, 8),
472        BootloaderInfo,
473        cbor!({}),
474        cbor!({
475            "bootloader" => "MCUboot",
476        }),
477        BootloaderInfoResponse{
478            bootloader: "MCUboot".to_string(),
479        }
480    }
481
482    command_encode_decode_test! {
483        bootloader_info_mcuboot_mode,
484        (0, 0, 8),
485        BootloaderInfoMcubootMode{},
486        cbor!({
487            "query" => "mode",
488        }),
489        cbor!({
490            "mode" => 5,
491            "no-downgrade" => true,
492        }),
493        BootloaderInfoMcubootModeResponse{
494            mode: 5,
495            no_downgrade: true,
496        }
497    }
498
499    command_encode_decode_test! {
500        bootloader_info_mcuboot_mode_default_values,
501        (0, 0, 8),
502        BootloaderInfoMcubootMode{},
503        cbor!({
504            "query" => "mode",
505        }),
506        cbor!({
507            "mode" => -1,
508        }),
509        BootloaderInfoMcubootModeResponse{
510            mode: -1,
511            no_downgrade: false,
512        }
513    }
514}