Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion smp/os_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

from enum import IntEnum, unique
from typing import Any, Dict, Literal, Union
from typing import Annotated, Any, Dict, Literal, Union

from pydantic import BaseModel, ConfigDict, Field

Expand All @@ -30,6 +30,21 @@ class EchoWriteResponse(message.WriteResponse):
"""Echoed string."""


@unique
class BootMode(IntEnum):
"""Boot mode requested by the OS management reset command.

Mirrors Zephyr's `enum BOOT_MODE_TYPES` in
`include/zephyr/retention/bootmode.h`.
"""

NORMAL = 0
"""Default (normal) boot, to the user application."""

BOOTLOADER = 1
"""Bootloader boot mode, e.g. serial recovery for MCUboot."""


class ResetWriteRequest(message.WriteRequest):
"""Performs reset of system.

Expand All @@ -55,6 +70,22 @@ class ResetWriteRequest(message.WriteRequest):
following map may be sent to force a reset
"""

boot_mode: Union[BootMode, Annotated[int, Field(ge=0, le=255)], None] = Field(
default=None, union_mode="left_to_right"
)
"""Boot mode to set via the retention boot mode module before resetting.

A value of `BootMode.BOOTLOADER` (1) requests, for example, that an MCUboot
built with `CONFIG_BOOT_SERIAL_BOOT_MODE` enter serial recovery on the next
boot. The server casts the value to a `uint8_t`, so any value in `[0, 255]`
is accepted and passed to `bootmode_set()`; known values are surfaced as
`BootMode` members.

Requires the server to be built with `CONFIG_MCUMGR_GRP_OS_RESET_BOOT_MODE`,
which depends on `CONFIG_RETENTION_BOOT_MODE`. Added to the SMP OS
management group in Zephyr v4.2.0 (zephyrproject-rtos/zephyr#91510).
"""


class ResetWriteResponse(message.WriteResponse):
"""Success response to a reset request."""
Expand Down
46 changes: 45 additions & 1 deletion tests/test_os_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from typing import Any, Dict, Type, TypeVar

import cbor2
from pydantic import BaseModel
import pytest
from pydantic import BaseModel, ValidationError

from smp import header as smphdr
from smp import message as smpmsg
Expand Down Expand Up @@ -74,6 +75,49 @@ def test_ResetWriteResponse() -> None:
_do_test(smpos.ResetWriteResponse, smphdr.OP.WRITE_RSP, oscmd.RESET, {})


def test_ResetWriteRequest_boot_mode_normal() -> None:
r = _do_test(smpos.ResetWriteRequest, smphdr.OP.WRITE, oscmd.RESET, {"boot_mode": 0})
assert r.boot_mode is smpos.BootMode.NORMAL


def test_ResetWriteRequest_boot_mode_bootloader() -> None:
r = _do_test(smpos.ResetWriteRequest, smphdr.OP.WRITE, oscmd.RESET, {"boot_mode": 1})
assert r.boot_mode is smpos.BootMode.BOOTLOADER


def test_ResetWriteRequest_boot_mode_passes_through_unknown_int() -> None:
"""A wire-valid but unrecognized boot mode stays a plain int."""
r = _do_test(smpos.ResetWriteRequest, smphdr.OP.WRITE, oscmd.RESET, {"boot_mode": 5})
assert r.boot_mode == 5
assert type(r.boot_mode) is int


def test_ResetWriteRequest_force_and_boot_mode() -> None:
r = _do_test(
smpos.ResetWriteRequest,
smphdr.OP.WRITE,
oscmd.RESET,
{"force": 1, "boot_mode": 1},
)
assert r.force == 1
assert r.boot_mode is smpos.BootMode.BOOTLOADER


def test_ResetWriteRequest_boot_mode_accepts_enum_member() -> None:
"""Constructing with a BootMode member serializes identically to its int value."""
from_enum = smpos.ResetWriteRequest(boot_mode=smpos.BootMode.BOOTLOADER)
from_int = smpos.ResetWriteRequest(boot_mode=1)
assert from_enum.BYTES[8:] == from_int.BYTES[8:]
assert from_enum.boot_mode is smpos.BootMode.BOOTLOADER


@pytest.mark.parametrize("boot_mode", [-1, 256])
def test_ResetWriteRequest_boot_mode_rejects_out_of_range(boot_mode: int) -> None:
"""boot_mode is a uint8_t on the wire; values outside [0, 255] are invalid."""
with pytest.raises(ValidationError):
smpos.ResetWriteRequest(boot_mode=boot_mode)


def test_TaskStatisticsReadRequest() -> None:
_do_test(smpos.TaskStatisticsReadRequest, smphdr.OP.READ, oscmd.TASK_STATS, {})

Expand Down
Loading