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(Clone, 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(Clone, 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(Clone, Debug, Eq, PartialEq)]
27pub struct TaskStatistics;
28impl_serialize_as_empty_map!(TaskStatistics);
29
30/// Statistics of an MCU task/thread
31#[derive(Clone, 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(Clone, 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/// [Memory Pool statistics](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#memory-pool-statistics) command
103#[derive(Clone, Debug, Eq, PartialEq)]
104pub struct MemoryPoolStatistics;
105impl_serialize_as_empty_map!(MemoryPoolStatistics);
106
107const fn default_blksiz() -> u64 {
108    1
109}
110
111/// Statistics of a MCU memory pool
112#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
113pub struct MemoryPoolStatisticsEntry {
114    /// size of a memory block in the pool
115    #[serde(default = "default_blksiz")]
116    pub blksiz: u64,
117    /// number of blocks in the pool
118    pub nblks: u64,
119    /// number of free blocks
120    pub nfree: u64,
121    /// lowest number of free blocks the pool reached during runtime
122    pub min: u64,
123}
124
125/// Response for [`MemoryPoolStatistics`] command
126#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
127pub struct MemoryPoolStatisticsResponse {
128    /// Dictionary of pool names with their respective statistics
129    pub pools: HashMap<String, MemoryPoolStatisticsEntry>,
130}
131
132/// Response for [`MemoryPoolStatistics`] command,
133/// before https://github.com/zephyrproject-rtos/zephyr/pull/107251.
134#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
135struct MemoryPoolStatisticsResponseZephyr4_4_0 {
136    /// Dictionary of pool names with their respective statistics
137    pub tasks: HashMap<String, MemoryPoolStatisticsEntry>,
138}
139
140impl<'de> serde::Deserialize<'de> for MemoryPoolStatisticsResponse {
141    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
142    where
143        D: serde::Deserializer<'de>,
144    {
145        let pools: either::Either<
146            HashMap<String, MemoryPoolStatisticsEntry>,
147            MemoryPoolStatisticsResponseZephyr4_4_0,
148        > = either::serde_untagged::deserialize(deserializer)?;
149
150        match pools {
151            either::Either::Left(pools) => Ok(Self { pools }),
152            either::Either::Right(response) => Ok(Self {
153                pools: response.tasks,
154            }),
155        }
156    }
157}
158
159/// Parses a [`chrono::NaiveDateTime`] object with optional timezone specifiers
160fn deserialize_datetime_and_ignore_timezone<'de, D>(
161    de: D,
162) -> Result<chrono::NaiveDateTime, D::Error>
163where
164    D: serde::Deserializer<'de>,
165{
166    #[derive(Deserialize)]
167    #[serde(untagged)]
168    enum NaiveOrFixed {
169        Naive(chrono::NaiveDateTime),
170        Fixed(chrono::DateTime<chrono::FixedOffset>),
171    }
172
173    NaiveOrFixed::deserialize(de).map(|val| match val {
174        NaiveOrFixed::Naive(naive_date_time) => naive_date_time,
175        NaiveOrFixed::Fixed(date_time) => date_time.naive_local(),
176    })
177}
178
179/// Serializes a [`chrono::NaiveDateTime`] object with zero or three fractional digits,
180/// which is most compatible with Zephyr
181fn serialize_datetime_for_zephyr<S>(
182    value: &chrono::NaiveDateTime,
183    serializer: S,
184) -> Result<S::Ok, S::Error>
185where
186    S: serde::Serializer,
187{
188    if value.time().nanosecond() != 0 {
189        serializer.serialize_str(&format!("{}", value.format("%Y-%m-%dT%H:%M:%S%.3f")))
190    } else {
191        serializer.serialize_str(&format!("{}", value.format("%Y-%m-%dT%H:%M:%S")))
192    }
193}
194
195/// [Date-Time Get](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#date-time-get) command
196#[derive(Clone, Debug, Eq, PartialEq)]
197pub struct DateTimeGet;
198impl_serialize_as_empty_map!(DateTimeGet);
199
200/// Response for [`DateTimeGet`] command
201#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
202pub struct DateTimeGetResponse {
203    /// String in format: `yyyy-MM-dd'T'HH:mm:ss.SSS`.
204    #[serde(deserialize_with = "deserialize_datetime_and_ignore_timezone")]
205    pub datetime: chrono::NaiveDateTime,
206}
207
208/// [Date-Time Set](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#date-time-set) command
209#[derive(Clone, Serialize, Debug, Eq, PartialEq)]
210pub struct DateTimeSet {
211    /// String in format: `yyyy-MM-dd'T'HH:mm:ss.SSS`.
212    #[serde(serialize_with = "serialize_datetime_for_zephyr")]
213    pub datetime: chrono::NaiveDateTime,
214}
215
216/// Response for [`DateTimeSet`] command
217#[derive(Clone, Default, Debug, Eq, PartialEq)]
218pub struct DateTimeSetResponse;
219impl_deserialize_from_empty_map_and_into_unit!(DateTimeSetResponse);
220
221/// [System Reset](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#system-reset) command
222#[derive(Clone, Serialize, Debug, Eq, PartialEq)]
223pub struct SystemReset {
224    /// Forces reset
225    #[serde(skip_serializing_if = "is_default")]
226    pub force: bool,
227    /// Boot mode
228    ///
229    /// - 0: Normal boot
230    /// - 1: Bootloader recovery mode
231    ///
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub boot_mode: Option<u8>,
234}
235
236/// Response for [`SystemReset`] command
237#[derive(Clone, Default, Debug, Eq, PartialEq)]
238pub struct SystemResetResponse;
239impl_deserialize_from_empty_map_and_into_unit!(SystemResetResponse);
240
241/// [MCUmgr Parameters](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#mcumgr-parameters) command
242#[derive(Clone, Debug, Eq, PartialEq)]
243pub struct MCUmgrParameters;
244impl_serialize_as_empty_map!(MCUmgrParameters);
245
246/// Response for [`MCUmgrParameters`] command
247#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
248pub struct MCUmgrParametersResponse {
249    /// Single SMP buffer size, this includes SMP header and CBOR payload
250    pub buf_size: u32,
251    /// Number of SMP buffers supported
252    pub buf_count: u32,
253}
254
255/// [OS/Application Info](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#os-application-info) command
256#[derive(Clone, Serialize, Debug, Eq, PartialEq)]
257pub struct ApplicationInfo<'a> {
258    /// Format specifier of returned response
259    ///
260    /// 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).
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub format: Option<&'a str>,
263}
264
265/// Response for [`ApplicationInfo`] command
266#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
267pub struct ApplicationInfoResponse {
268    /// Text response including requested parameters
269    pub output: String,
270}
271
272/// [Bootloader Information](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#bootloader-information) command
273#[derive(Clone, Debug, Eq, PartialEq)]
274pub struct BootloaderInfo;
275impl_serialize_as_empty_map!(BootloaderInfo);
276
277/// Response for [`BootloaderInfo`] command
278#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
279pub struct BootloaderInfoResponse {
280    /// String representing bootloader name
281    pub bootloader: String,
282}
283
284/// [Bootloader Information MCUboot Mode](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#bootloader-information-mcuboot) subcommand
285#[derive(Clone, Serialize, Debug, Eq, PartialEq)]
286#[serde(tag = "query", rename = "mode")]
287pub struct BootloaderInfoMcubootMode {}
288
289/// Response for [`BootloaderInfoMcubootMode`] command
290#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
291pub struct BootloaderInfoMcubootModeResponse {
292    /// The bootloader mode
293    pub mode: i32,
294    /// MCUboot has downgrade prevention enabled
295    #[serde(default, rename = "no-downgrade")]
296    pub no_downgrade: bool,
297}
298
299#[cfg(test)]
300mod tests {
301    use super::super::macros::command_encode_decode_test;
302    use super::*;
303    use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
304    use ciborium::cbor;
305
306    #[test]
307    fn thread_state_flags_to_string() {
308        assert_eq!(
309            ThreadStateFlags::pretty_print(0xff),
310            "dummy | pending | sleeping | dead | suspended | aborting | suspending | queued"
311        );
312
313        assert_eq!(ThreadStateFlags::pretty_print(0b00000001), "dummy");
314        assert_eq!(ThreadStateFlags::pretty_print(0b00000010), "pending");
315        assert_eq!(ThreadStateFlags::pretty_print(0b00000100), "sleeping");
316        assert_eq!(ThreadStateFlags::pretty_print(0b00001000), "dead");
317        assert_eq!(ThreadStateFlags::pretty_print(0b00010000), "suspended");
318        assert_eq!(ThreadStateFlags::pretty_print(0b00100000), "aborting");
319        assert_eq!(ThreadStateFlags::pretty_print(0b01000000), "suspending");
320        assert_eq!(ThreadStateFlags::pretty_print(0b10000000), "queued");
321
322        assert_eq!(ThreadStateFlags::pretty_print(0), "");
323    }
324
325    command_encode_decode_test! {
326        echo,
327        (0, 0, 0),
328        Echo{d: "Hello World!"},
329        cbor!({"d" => "Hello World!"}),
330        cbor!({"r" => "Hello World!"}),
331        EchoResponse{r: "Hello World!".to_string()},
332    }
333
334    command_encode_decode_test! {
335        task_statistics_empty,
336        (0, 0, 2),
337        TaskStatistics,
338        cbor!({}),
339        cbor!({"tasks" => {}}),
340        TaskStatisticsResponse{ tasks: HashMap::new() },
341    }
342
343    command_encode_decode_test! {
344        task_statistics,
345        (0, 0, 2),
346        TaskStatistics,
347        cbor!({}),
348        cbor!({"tasks" => {
349            "task_a" => {
350                "prio" => 20,
351                "tid" => 5,
352                "state" => 10,
353            },
354            "task_b" => {
355                "prio"         => 30,
356                "tid"          => 31,
357                "state"        => 32,
358                "stkuse"       => 33,
359                "stksiz"       => 34,
360                "cswcnt"       => 35,
361                "runtime"      => 36,
362                "last_checkin" => 0,
363                "next_checkin" => 0,
364            },
365        }}),
366        TaskStatisticsResponse{ tasks: HashMap::from([
367            (
368                "task_a".to_string(),
369                TaskStatisticsEntry{
370                    prio: 20,
371                    tid: 5,
372                    state: 10,
373                    stkuse: None,
374                    stksiz: None,
375                    cswcnt: None,
376                    runtime: None,
377                },
378            ), (
379                "task_b".to_string(),
380                TaskStatisticsEntry{
381                    prio: 30,
382                    tid: 31,
383                    state: 32,
384                    stkuse: Some(33),
385                    stksiz: Some(34),
386                    cswcnt: Some(35),
387                    runtime: Some(36),
388                },
389            ),
390        ]) },
391    }
392
393    command_encode_decode_test! {
394        memory_pool_statistics_empty,
395        (0, 0, 3),
396        MemoryPoolStatistics,
397        cbor!({}),
398        cbor!({}),
399        MemoryPoolStatisticsResponse{ pools: HashMap::new() },
400    }
401
402    command_encode_decode_test! {
403        memory_pool_statistics_empty_old,
404        (0, 0, 3),
405        MemoryPoolStatistics,
406        cbor!({}),
407        cbor!({"tasks" => {}}),
408        MemoryPoolStatisticsResponse{ pools: HashMap::new() },
409    }
410
411    command_encode_decode_test! {
412        memory_pool_statistics,
413        (0, 0, 3),
414        MemoryPoolStatistics,
415        cbor!({}),
416        cbor!({
417            "pool_a" => {
418                "blksiz" => 8,
419                "nblks" => 20,
420                "nfree" => 10,
421                "min" => 5,
422            },
423            "pool_b" => {
424                "nblks" => 50,
425                "nfree" => 35,
426                "min" => 30,
427            },
428        }),
429        MemoryPoolStatisticsResponse{ pools: HashMap::from([
430            (
431                "pool_a".to_string(),
432                MemoryPoolStatisticsEntry{
433                    blksiz: 8,
434                    nblks: 20,
435                    nfree: 10,
436                    min: 5,
437                },
438            ), (
439                "pool_b".to_string(),
440                MemoryPoolStatisticsEntry{
441                    blksiz: 1,
442                    nblks: 50,
443                    nfree: 35,
444                    min: 30,
445                },
446            ),
447        ]) },
448    }
449
450    command_encode_decode_test! {
451        memory_pool_statistics_old,
452        (0, 0, 3),
453        MemoryPoolStatistics,
454        cbor!({}),
455        cbor!({ "tasks" => {
456            "pool_a" => {
457                "blksiz" => 8,
458                "nblks" => 20,
459                "nfree" => 10,
460                "min" => 5,
461            },
462            "pool_b" => {
463                "nblks" => 50,
464                "nfree" => 35,
465                "min" => 30,
466            },
467        }}),
468        MemoryPoolStatisticsResponse{ pools: HashMap::from([
469            (
470                "pool_a".to_string(),
471                MemoryPoolStatisticsEntry{
472                    blksiz: 8,
473                    nblks: 20,
474                    nfree: 10,
475                    min: 5,
476                },
477            ), (
478                "pool_b".to_string(),
479                MemoryPoolStatisticsEntry{
480                    blksiz: 1,
481                    nblks: 50,
482                    nfree: 35,
483                    min: 30,
484                },
485            ),
486        ]) },
487    }
488
489    command_encode_decode_test! {
490        datetime_get_with_timezone,
491        (0, 0, 4),
492        DateTimeGet,
493        cbor!({}),
494        cbor!({
495            "datetime" => "2025-11-20T11:56:05.366345+01:00"
496        }),
497        DateTimeGetResponse{
498            datetime: NaiveDateTime::new(NaiveDate::from_ymd_opt(2025, 11, 20).unwrap(), NaiveTime::from_hms_micro_opt(11,56,5,366345).unwrap()),
499        },
500    }
501
502    command_encode_decode_test! {
503        datetime_get_with_millis,
504        (0, 0, 4),
505        DateTimeGet,
506        cbor!({}),
507        cbor!({
508            "datetime" => "2025-11-20T11:56:05.366"
509        }),
510        DateTimeGetResponse{
511            datetime: NaiveDateTime::new(NaiveDate::from_ymd_opt(2025, 11, 20).unwrap(), NaiveTime::from_hms_milli_opt(11,56,5,366).unwrap()),
512        },
513    }
514
515    command_encode_decode_test! {
516        datetime_get_without_millis,
517        (0, 0, 4),
518        DateTimeGet,
519        cbor!({}),
520        cbor!({
521            "datetime" => "2025-11-20T11:56:05"
522        }),
523        DateTimeGetResponse{
524            datetime: NaiveDateTime::new(NaiveDate::from_ymd_opt(2025, 11, 20).unwrap(), NaiveTime::from_hms_opt(11,56,5).unwrap()),
525        },
526    }
527
528    command_encode_decode_test! {
529        datetime_set_with_millis,
530        (2, 0, 4),
531        DateTimeSet{
532            datetime: NaiveDateTime::new(NaiveDate::from_ymd_opt(2025, 11, 20).unwrap(), NaiveTime::from_hms_micro_opt(12,3,56,642133).unwrap())
533        },
534        cbor!({
535            "datetime" => "2025-11-20T12:03:56.642"
536        }),
537        cbor!({}),
538        DateTimeSetResponse,
539    }
540
541    command_encode_decode_test! {
542        datetime_set_without_millis,
543        (2, 0, 4),
544        DateTimeSet{
545            datetime: NaiveDateTime::new(NaiveDate::from_ymd_opt(2025, 11, 20).unwrap(), NaiveTime::from_hms_opt(12,3,56).unwrap())
546        },
547        cbor!({
548            "datetime" => "2025-11-20T12:03:56"
549        }),
550        cbor!({}),
551        DateTimeSetResponse,
552    }
553
554    command_encode_decode_test! {
555        system_reset_minimal,
556        (2, 0, 5),
557        SystemReset{
558            force: false,
559            boot_mode: None,
560        },
561        cbor!({}),
562        cbor!({}),
563        SystemResetResponse,
564    }
565
566    command_encode_decode_test! {
567        system_reset_full,
568        (2, 0, 5),
569        SystemReset{
570            force: true,
571            boot_mode: Some(42),
572        },
573        cbor!({
574            "force" => true,
575            "boot_mode" => 42,
576        }),
577        cbor!({}),
578        SystemResetResponse,
579    }
580
581    command_encode_decode_test! {
582        mcumgr_parameters,
583        (0, 0, 6),
584        MCUmgrParameters,
585        cbor!({}),
586        cbor!({"buf_size" => 42, "buf_count" => 69}),
587        MCUmgrParametersResponse{buf_size: 42, buf_count: 69 },
588    }
589
590    command_encode_decode_test! {
591        application_info_without_format,
592        (0, 0, 7),
593        ApplicationInfo{
594            format: None,
595        },
596        cbor!({}),
597        cbor!({
598            "output" => "foo",
599        }),
600        ApplicationInfoResponse{
601            output: "foo".to_string(),
602        }
603    }
604
605    command_encode_decode_test! {
606        application_info_with_format,
607        (0, 0, 7),
608        ApplicationInfo{
609            format: Some("abc"),
610        },
611        cbor!({
612            "format" => "abc",
613        }),
614        cbor!({
615            "output" => "bar",
616        }),
617        ApplicationInfoResponse{
618            output: "bar".to_string(),
619        }
620    }
621
622    command_encode_decode_test! {
623        bootloader_info,
624        (0, 0, 8),
625        BootloaderInfo,
626        cbor!({}),
627        cbor!({
628            "bootloader" => "MCUboot",
629        }),
630        BootloaderInfoResponse{
631            bootloader: "MCUboot".to_string(),
632        }
633    }
634
635    command_encode_decode_test! {
636        bootloader_info_mcuboot_mode,
637        (0, 0, 8),
638        BootloaderInfoMcubootMode{},
639        cbor!({
640            "query" => "mode",
641        }),
642        cbor!({
643            "mode" => 5,
644            "no-downgrade" => true,
645        }),
646        BootloaderInfoMcubootModeResponse{
647            mode: 5,
648            no_downgrade: true,
649        }
650    }
651
652    command_encode_decode_test! {
653        bootloader_info_mcuboot_mode_default_values,
654        (0, 0, 8),
655        BootloaderInfoMcubootMode{},
656        cbor!({
657            "query" => "mode",
658        }),
659        cbor!({
660            "mode" => -1,
661        }),
662        BootloaderInfoMcubootModeResponse{
663            mode: -1,
664            no_downgrade: false,
665        }
666    }
667}