bread-crumbs-contracts 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ """Pure-Python codec for the BREAD CRUMBS contracts.
2
+
3
+ Mirrors ``include/bread/*.h``: one module per header, functions named after
4
+ the C helpers. Encoders return the payload bytes a ``*_send_*`` or
5
+ ``*_query_*`` helper puts on the wire; parsers turn a reply payload into a
6
+ frozen dataclass named after the C result struct. Framing, CRC and I2C
7
+ belong to the transport.
8
+
9
+ The constants and every encoder and parser are checked against
10
+ ``tests/golden_vectors/vectors.json``, which the C program
11
+ ``tests/golden_vectors/gen_vectors.c`` writes from the real headers.
12
+ """
13
+
14
+ from importlib.metadata import version as _distribution_version
15
+
16
+ from . import bread_caps, bread_version_helpers, bread_watchdog, crumbs, dcmt_ops, rlht_ops
17
+ from .bread_caps import * # noqa: F403
18
+ from .bread_version_helpers import * # noqa: F403
19
+ from .bread_watchdog import * # noqa: F403
20
+ from .crumbs import * # noqa: F403
21
+ from .dcmt_ops import * # noqa: F403
22
+ from .rlht_ops import * # noqa: F403
23
+
24
+ #: The installed distribution's version, which ``pyproject.toml`` sets and the
25
+ #: tests hold equal to the header package's ``library.json`` version.
26
+ __version__ = _distribution_version("bread-crumbs-contracts")
27
+
28
+ __all__: list[str] = []
29
+ __all__ += crumbs.__all__
30
+ __all__ += bread_caps.__all__
31
+ __all__ += bread_watchdog.__all__
32
+ __all__ += bread_version_helpers.__all__
33
+ __all__ += rlht_ops.__all__
34
+ __all__ += dcmt_ops.__all__
@@ -0,0 +1,64 @@
1
+ """Argument range checks and payload length checks shared by the codec modules.
2
+
3
+ CRUMBS payload fields are little-endian; each module packs them with
4
+ ``struct`` using explicit ``<`` format strings. The checks here turn an
5
+ out-of-range argument into a ``ValueError`` before ``struct`` could wrap or
6
+ reject it, and a reply of the wrong length into a ``ValueError`` before
7
+ ``struct`` could raise its own error type.
8
+
9
+ The two length rules mirror the C parsers as they are today, not a single
10
+ policy: ``dcmt_parse_state_payload()`` and ``bread_watchdog_parse_payload()``
11
+ reject any length other than the fixed one, while
12
+ ``rlht_parse_state_payload()``, ``bread_caps_parse_payload()`` and
13
+ ``bread_parse_version()`` read a fixed prefix and ignore trailing bytes.
14
+ feastorg/bread-crumbs-contracts#18 is where the rule becomes uniform; each
15
+ Python parser follows its C namesake until then, and the golden vectors
16
+ record which lengths the C accepts.
17
+ """
18
+
19
+ U8_MIN, U8_MAX = 0, 0xFF
20
+ U16_MIN, U16_MAX = 0, 0xFFFF
21
+ I16_MIN, I16_MAX = -0x8000, 0x7FFF
22
+
23
+
24
+ def _check_int(name: str, value: int, lo: int, hi: int) -> int:
25
+ if not isinstance(value, int): # pyright: ignore[reportUnnecessaryIsInstance]
26
+ raise TypeError(f"{name} must be an int, got {type(value).__name__}")
27
+ if not lo <= value <= hi:
28
+ raise ValueError(f"{name} must be in {lo}..{hi}, got {value}")
29
+ return value
30
+
31
+
32
+ def u8(name: str, value: int) -> int:
33
+ """Return ``value`` if it fits a ``uint8_t``, else raise."""
34
+ return _check_int(name, value, U8_MIN, U8_MAX)
35
+
36
+
37
+ def u16(name: str, value: int) -> int:
38
+ """Return ``value`` if it fits a ``uint16_t``, else raise."""
39
+ return _check_int(name, value, U16_MIN, U16_MAX)
40
+
41
+
42
+ def i16(name: str, value: int) -> int:
43
+ """Return ``value`` if it fits an ``int16_t``, else raise."""
44
+ return _check_int(name, value, I16_MIN, I16_MAX)
45
+
46
+
47
+ def require_exact_len(name: str, payload: bytes, fixed_len: int) -> None:
48
+ """Reject a payload whose length is not exactly ``fixed_len``.
49
+
50
+ The C check is ``data_len != FIXED_LEN``: a short payload and one with
51
+ trailing bytes are both rejected.
52
+ """
53
+ if len(payload) != fixed_len:
54
+ raise ValueError(f"{name} payload must be {fixed_len} bytes, got {len(payload)}")
55
+
56
+
57
+ def require_min_len(name: str, payload: bytes, fixed_len: int) -> None:
58
+ """Reject a payload shorter than ``fixed_len``; trailing bytes are ignored.
59
+
60
+ The C check is ``len < FIXED_LEN`` (or a field-by-field read that stops
61
+ at the last fixed offset), so a longer payload parses as its prefix.
62
+ """
63
+ if len(payload) < fixed_len:
64
+ raise ValueError(f"{name} payload needs {fixed_len} bytes, got {len(payload)}")
@@ -0,0 +1,66 @@
1
+ """Shared sentinels and the capability query (``bread_caps.h``)."""
2
+
3
+ import struct
4
+ from dataclasses import dataclass
5
+
6
+ from ._wire import require_min_len
7
+
8
+ __all__ = [
9
+ "BREAD_CAPS_SCHEMA_V1",
10
+ "BREAD_CAPS_V1_PAYLOAD_LEN",
11
+ "BREAD_INVALID_I16",
12
+ "BREAD_INVALID_U8",
13
+ "BREAD_OP_GET_CAPS",
14
+ "BreadCapsResult",
15
+ "bread_caps_parse_payload",
16
+ "bread_is_valid_i16",
17
+ "bread_is_valid_u8",
18
+ ]
19
+
20
+ #: Fixed-layout fields carry these instead of zero when the field does not
21
+ #: apply in the current mode or build. Check with the ``bread_is_valid_*``
22
+ #: helpers before consuming such a field.
23
+ BREAD_INVALID_I16 = -32768
24
+ BREAD_INVALID_U8 = 0xFF
25
+
26
+ #: Capability query opcode shared by every BREADS-compatible slice.
27
+ BREAD_OP_GET_CAPS = 0x7F
28
+
29
+ #: Capability payload schema version.
30
+ BREAD_CAPS_SCHEMA_V1 = 0x01
31
+
32
+ #: v1 payload: ``[schema:u8][level:u8][flags:u32]``.
33
+ BREAD_CAPS_V1_PAYLOAD_LEN = 6
34
+
35
+ _CAPS_V1 = struct.Struct("<BBI")
36
+ assert _CAPS_V1.size == BREAD_CAPS_V1_PAYLOAD_LEN
37
+
38
+
39
+ def bread_is_valid_i16(value: int) -> bool:
40
+ """``BREAD_IS_VALID_I16``: the field is populated, not the sentinel."""
41
+ return value != BREAD_INVALID_I16
42
+
43
+
44
+ def bread_is_valid_u8(value: int) -> bool:
45
+ """``BREAD_IS_VALID_U8``: the field is populated, not the sentinel."""
46
+ return value != BREAD_INVALID_U8
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class BreadCapsResult:
51
+ """``bread_caps_result_t``."""
52
+
53
+ schema: int
54
+ level: int
55
+ flags: int
56
+
57
+
58
+ def bread_caps_parse_payload(payload: bytes) -> BreadCapsResult:
59
+ """Parse a GET_CAPS reply payload (``bread_caps_parse_payload``).
60
+
61
+ Like the C parser, this reads the 6-byte v1 prefix and ignores any
62
+ trailing bytes; a shorter payload is rejected.
63
+ """
64
+ require_min_len("GET_CAPS", payload, BREAD_CAPS_V1_PAYLOAD_LEN)
65
+ schema, level, flags = _CAPS_V1.unpack_from(payload)
66
+ return BreadCapsResult(schema, level, flags)
@@ -0,0 +1,74 @@
1
+ """Version reply parsing and compatibility policy (``bread_version_helpers.h``)."""
2
+
3
+ import struct
4
+ from dataclasses import dataclass
5
+
6
+ from ._wire import require_min_len
7
+
8
+ __all__ = [
9
+ "BREAD_MIN_CRUMBS_VERSION",
10
+ "BREAD_OP_GET_VERSION",
11
+ "BREAD_VERSION_PAYLOAD_LEN",
12
+ "BreadVersionResult",
13
+ "bread_check_crumbs_compat",
14
+ "bread_check_module_compat",
15
+ "bread_parse_version",
16
+ ]
17
+
18
+ #: Opcode of the version query. The headers have no macro for it;
19
+ #: ``rlht_query_version()`` and ``dcmt_query_version()`` request the literal
20
+ #: ``0x00``, and ``bread_parse_version()`` documents it as "opcode 0x00".
21
+ BREAD_OP_GET_VERSION = 0x00
22
+
23
+ #: Lowest peripheral CRUMBS version (major*10000 + minor*100 + patch) this
24
+ #: contract set accepts.
25
+ BREAD_MIN_CRUMBS_VERSION = 1200
26
+
27
+ #: Version payload: ``[CRUMBS_VERSION:u16][module_major:u8][module_minor:u8][module_patch:u8]``.
28
+ #: The headers have no macro for this length either; ``bread_parse_version()``
29
+ #: checks ``len < 5``.
30
+ BREAD_VERSION_PAYLOAD_LEN = 5
31
+
32
+ _VERSION = struct.Struct("<HBBB")
33
+ assert _VERSION.size == BREAD_VERSION_PAYLOAD_LEN
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class BreadVersionResult:
38
+ """The out-parameters of ``bread_parse_version``."""
39
+
40
+ crumbs_ver: int
41
+ mod_major: int
42
+ mod_minor: int
43
+ mod_patch: int
44
+
45
+
46
+ def bread_parse_version(payload: bytes) -> BreadVersionResult:
47
+ """Parse a version reply payload (``bread_parse_version``).
48
+
49
+ Like the C parser, this reads the 5-byte prefix and ignores any
50
+ trailing bytes; a shorter payload is rejected.
51
+ """
52
+ require_min_len("version", payload, BREAD_VERSION_PAYLOAD_LEN)
53
+ crumbs_ver, mod_major, mod_minor, mod_patch = _VERSION.unpack_from(payload)
54
+ return BreadVersionResult(crumbs_ver, mod_major, mod_minor, mod_patch)
55
+
56
+
57
+ def bread_check_crumbs_compat(peripheral_ver: int) -> int:
58
+ """``bread_check_crumbs_compat``: 0 when at or above the minimum, else -1."""
59
+ return 0 if peripheral_ver >= BREAD_MIN_CRUMBS_VERSION else -1
60
+
61
+
62
+ def bread_check_module_compat(
63
+ peri_major: int, peri_minor: int, expect_major: int, expect_minor: int
64
+ ) -> int:
65
+ """``bread_check_module_compat``.
66
+
67
+ Returns 0 when compatible, -1 when MAJOR differs, -2 when the
68
+ peripheral MINOR is below the expected MINOR. PATCH is ignored.
69
+ """
70
+ if peri_major != expect_major:
71
+ return -1
72
+ if peri_minor < expect_minor:
73
+ return -2
74
+ return 0
@@ -0,0 +1,66 @@
1
+ """Shared bus-liveness command watchdog (``bread_watchdog.h``).
2
+
3
+ Firmware boots disarmed; a controller arms it with SET_WATCHDOG. While
4
+ armed, any valid inbound frame refreshes it; expiry drives the slice's
5
+ actuators to their safe state without touching estop. Support is
6
+ advertised per type by a capability flag (``RLHT_CAP_CMD_WATCHDOG``,
7
+ ``DCMT_CAP_CMD_WATCHDOG``); do not send SET_WATCHDOG unless it is set.
8
+ """
9
+
10
+ import struct
11
+ from dataclasses import dataclass
12
+
13
+ from ._wire import require_exact_len
14
+
15
+ __all__ = [
16
+ "BREAD_OP_GET_WATCHDOG",
17
+ "BREAD_OP_SET_WATCHDOG",
18
+ "BREAD_WATCHDOG_FIXED_LEN",
19
+ "BREAD_WATCHDOG_OFF_ARMED",
20
+ "BREAD_WATCHDOG_OFF_TIMEOUT_MS",
21
+ "BREAD_WATCHDOG_OFF_TRIPPED",
22
+ "BREAD_WATCHDOG_OFF_TRIP_COUNT",
23
+ "BREAD_WATCHDOG_SET_PAYLOAD_LEN",
24
+ "BreadWatchdogResult",
25
+ "bread_watchdog_parse_payload",
26
+ ]
27
+
28
+ BREAD_OP_SET_WATCHDOG = 0x7E
29
+ BREAD_OP_GET_WATCHDOG = 0x7D
30
+
31
+ #: SET_WATCHDOG payload: ``[timeout_ms:u16]``, 0 = disarm.
32
+ BREAD_WATCHDOG_SET_PAYLOAD_LEN = 2
33
+
34
+ #: GET_WATCHDOG payload: ``[armed:u8][timeout_ms:u16][tripped:u8][trip_count:u8]``.
35
+ BREAD_WATCHDOG_OFF_ARMED = 0
36
+ BREAD_WATCHDOG_OFF_TIMEOUT_MS = 1
37
+ BREAD_WATCHDOG_OFF_TRIPPED = 3
38
+ BREAD_WATCHDOG_OFF_TRIP_COUNT = 4
39
+ BREAD_WATCHDOG_FIXED_LEN = 5
40
+
41
+ _WATCHDOG = struct.Struct("<BHBB")
42
+ assert _WATCHDOG.size == BREAD_WATCHDOG_FIXED_LEN
43
+
44
+
45
+ @dataclass(frozen=True, slots=True)
46
+ class BreadWatchdogResult:
47
+ """``bread_watchdog_result_t``."""
48
+
49
+ armed: int
50
+ #: Configured timeout; 0 when disarmed.
51
+ timeout_ms: int
52
+ #: Currently expired; clears on the next valid SET command.
53
+ tripped: int
54
+ #: Cumulative trips since boot, never cleared.
55
+ trip_count: int
56
+
57
+
58
+ def bread_watchdog_parse_payload(payload: bytes) -> BreadWatchdogResult:
59
+ """Parse a GET_WATCHDOG reply payload (``bread_watchdog_parse_payload``).
60
+
61
+ Like the C parser, this rejects any length other than
62
+ ``BREAD_WATCHDOG_FIXED_LEN``, trailing bytes included.
63
+ """
64
+ require_exact_len("GET_WATCHDOG", payload, BREAD_WATCHDOG_FIXED_LEN)
65
+ armed, timeout_ms, tripped, trip_count = _WATCHDOG.unpack_from(payload)
66
+ return BreadWatchdogResult(armed, timeout_ms, tripped, trip_count)
@@ -0,0 +1,23 @@
1
+ """The few CRUMBS protocol constants the contracts depend on (``crumbs.h``).
2
+
3
+ Framing, CRC and I2C stay in the transport (CRUMBS itself, or a Python
4
+ binding of it). This package stops at the message level: a type id, an
5
+ opcode and the payload bytes between the frame header and the CRC.
6
+ """
7
+
8
+ __all__ = [
9
+ "CRUMBS_CMD_SET_REPLY",
10
+ "CRUMBS_MAX_PAYLOAD",
11
+ "CRUMBS_TYPE_ID_ANY",
12
+ ]
13
+
14
+ #: Wildcard type id; a peripheral never rejects a frame carrying it.
15
+ CRUMBS_TYPE_ID_ANY = 0x00
16
+
17
+ #: Opcode of the SET_REPLY frame a controller sends before reading a reply.
18
+ #: Its one-byte payload names the opcode whose reply the peripheral should
19
+ #: build next. The ``*_query_*`` encoders return that payload.
20
+ CRUMBS_CMD_SET_REPLY = 0xFE
21
+
22
+ #: Largest payload a CRUMBS frame carries.
23
+ CRUMBS_MAX_PAYLOAD = 27
@@ -0,0 +1,214 @@
1
+ """DCMT (DC motor) contract (``dcmt_ops.h``).
2
+
3
+ Each ``dcmt_send_*`` function returns the payload its C namesake puts on
4
+ the wire; send it with ``DCMT_TYPE_ID`` and the matching ``DCMT_OP_*``
5
+ opcode. Each ``dcmt_query_*`` function returns the one-byte payload of the
6
+ SET_REPLY frame (type ``CRUMBS_TYPE_ID_ANY``, opcode ``CRUMBS_CMD_SET_REPLY``)
7
+ that asks the slice to build that reply.
8
+ """
9
+
10
+ import struct
11
+ from dataclasses import dataclass
12
+
13
+ from ._wire import i16, require_exact_len, u8, u16
14
+ from .bread_caps import BREAD_OP_GET_CAPS
15
+ from .bread_version_helpers import BREAD_OP_GET_VERSION
16
+ from .bread_watchdog import BREAD_OP_GET_WATCHDOG
17
+
18
+ __all__ = [
19
+ "DCMT_CAP_BASELINE_FLAGS",
20
+ "DCMT_CAP_BRAKE_CONTROL",
21
+ "DCMT_CAP_CLOSED_LOOP_POSITION",
22
+ "DCMT_CAP_CLOSED_LOOP_SPEED",
23
+ "DCMT_CAP_CMD_WATCHDOG",
24
+ "DCMT_CAP_LEVEL_1",
25
+ "DCMT_CAP_LEVEL_2",
26
+ "DCMT_CAP_LEVEL_3",
27
+ "DCMT_CAP_OPEN_LOOP_CONTROL",
28
+ "DCMT_CAP_PID_TUNING",
29
+ "DCMT_MODE_CLOSED_POSITION",
30
+ "DCMT_MODE_CLOSED_SPEED",
31
+ "DCMT_MODE_OPEN_LOOP",
32
+ "DCMT_MODULE_VER_MAJOR",
33
+ "DCMT_MODULE_VER_MINOR",
34
+ "DCMT_MODULE_VER_PATCH",
35
+ "DCMT_OP_GET_STATE",
36
+ "DCMT_OP_SET_BRAKE",
37
+ "DCMT_OP_SET_MODE",
38
+ "DCMT_OP_SET_OPEN_LOOP",
39
+ "DCMT_OP_SET_PID",
40
+ "DCMT_OP_SET_SETPOINT",
41
+ "DCMT_STATE_FIXED_LEN",
42
+ "DCMT_STATE_OFF_BRAKES",
43
+ "DCMT_STATE_OFF_ESTOP",
44
+ "DCMT_STATE_OFF_M1_PWM",
45
+ "DCMT_STATE_OFF_M2_PWM",
46
+ "DCMT_STATE_OFF_MODE",
47
+ "DCMT_STATE_OFF_POS1",
48
+ "DCMT_STATE_OFF_POS2",
49
+ "DCMT_STATE_OFF_SP1",
50
+ "DCMT_STATE_OFF_SP2",
51
+ "DCMT_STATE_OFF_SPD1",
52
+ "DCMT_STATE_OFF_SPD2",
53
+ "DCMT_TYPE_ID",
54
+ "DcmtStateResult",
55
+ "dcmt_parse_state_payload",
56
+ "dcmt_query_caps",
57
+ "dcmt_query_state",
58
+ "dcmt_query_version",
59
+ "dcmt_query_watchdog",
60
+ "dcmt_send_set_brake",
61
+ "dcmt_send_set_mode",
62
+ "dcmt_send_set_open_loop",
63
+ "dcmt_send_set_pid",
64
+ "dcmt_send_set_setpoint",
65
+ "dcmt_send_set_watchdog",
66
+ ]
67
+
68
+ DCMT_TYPE_ID = 0x02
69
+
70
+ DCMT_MODULE_VER_MAJOR = 1
71
+ DCMT_MODULE_VER_MINOR = 0
72
+ DCMT_MODULE_VER_PATCH = 0
73
+
74
+ DCMT_OP_SET_OPEN_LOOP = 0x01
75
+ DCMT_OP_SET_BRAKE = 0x02
76
+ DCMT_OP_SET_MODE = 0x03
77
+ DCMT_OP_SET_SETPOINT = 0x04
78
+ DCMT_OP_SET_PID = 0x05
79
+
80
+ DCMT_OP_GET_STATE = 0x80
81
+
82
+ #: GET_STATE payload (19 bytes):
83
+ #: ``[mode:u8][m1_pwm:i16][m2_pwm:i16][sp1:i16][sp2:i16]``
84
+ #: ``[pos1:i16][pos2:i16][spd1:i16][spd2:i16][brakes:u8][estop:u8]``.
85
+ #: ``sp1``/``sp2`` are ``BREAD_INVALID_I16`` in ``DCMT_MODE_OPEN_LOOP``;
86
+ #: ``spd1``/``spd2`` are ``BREAD_INVALID_I16`` unless ``DCMT_MODE_CLOSED_SPEED``.
87
+ DCMT_STATE_OFF_MODE = 0
88
+ DCMT_STATE_OFF_M1_PWM = 1
89
+ DCMT_STATE_OFF_M2_PWM = 3
90
+ DCMT_STATE_OFF_SP1 = 5
91
+ DCMT_STATE_OFF_SP2 = 7
92
+ DCMT_STATE_OFF_POS1 = 9
93
+ DCMT_STATE_OFF_POS2 = 11
94
+ DCMT_STATE_OFF_SPD1 = 13
95
+ DCMT_STATE_OFF_SPD2 = 15
96
+ DCMT_STATE_OFF_BRAKES = 17
97
+ DCMT_STATE_OFF_ESTOP = 18
98
+ DCMT_STATE_FIXED_LEN = 19
99
+
100
+ DCMT_MODE_OPEN_LOOP = 0x00
101
+ DCMT_MODE_CLOSED_POSITION = 0x01
102
+ DCMT_MODE_CLOSED_SPEED = 0x02
103
+
104
+ DCMT_CAP_LEVEL_1 = 0x01
105
+ DCMT_CAP_LEVEL_2 = 0x02
106
+ DCMT_CAP_LEVEL_3 = 0x03
107
+
108
+ DCMT_CAP_OPEN_LOOP_CONTROL = 1 << 0
109
+ DCMT_CAP_BRAKE_CONTROL = 1 << 1
110
+ DCMT_CAP_CLOSED_LOOP_POSITION = 1 << 2
111
+ DCMT_CAP_CLOSED_LOOP_SPEED = 1 << 3
112
+ DCMT_CAP_PID_TUNING = 1 << 4
113
+ DCMT_CAP_CMD_WATCHDOG = 1 << 5
114
+
115
+ DCMT_CAP_BASELINE_FLAGS = DCMT_CAP_OPEN_LOOP_CONTROL | DCMT_CAP_BRAKE_CONTROL
116
+
117
+ _STATE = struct.Struct("<BhhhhhhhhBB")
118
+ assert _STATE.size == DCMT_STATE_FIXED_LEN
119
+
120
+ _U8 = struct.Struct("<B")
121
+ _U8_U8 = struct.Struct("<BB")
122
+ _U16 = struct.Struct("<H")
123
+ _I16_I16 = struct.Struct("<hh")
124
+ _PID = struct.Struct("<BBBBBB")
125
+
126
+
127
+ @dataclass(frozen=True, slots=True)
128
+ class DcmtStateResult:
129
+ """``dcmt_state_result_t``."""
130
+
131
+ mode: int
132
+ #: Current PWM output, always valid.
133
+ m1_pwm: int
134
+ m2_pwm: int
135
+ #: Active setpoint; ``BREAD_INVALID_I16`` in OPEN_LOOP.
136
+ sp1: int
137
+ sp2: int
138
+ #: Encoder position, always populated.
139
+ pos1: int
140
+ pos2: int
141
+ #: Tachometer speed; ``BREAD_INVALID_I16`` unless CLOSED_SPEED.
142
+ spd1: int
143
+ spd2: int
144
+ brakes: int
145
+ estop: int
146
+
147
+
148
+ def dcmt_send_set_open_loop(m1_pwm: int, m2_pwm: int) -> bytes:
149
+ """``DCMT_OP_SET_OPEN_LOOP`` payload: ``[m1_pwm:i16][m2_pwm:i16]``."""
150
+ return _I16_I16.pack(i16("m1_pwm", m1_pwm), i16("m2_pwm", m2_pwm))
151
+
152
+
153
+ def dcmt_send_set_brake(m1_brake: int, m2_brake: int) -> bytes:
154
+ """``DCMT_OP_SET_BRAKE`` payload: ``[m1_brake:u8][m2_brake:u8]``."""
155
+ return _U8_U8.pack(u8("m1_brake", m1_brake), u8("m2_brake", m2_brake))
156
+
157
+
158
+ def dcmt_send_set_mode(mode: int) -> bytes:
159
+ """``DCMT_OP_SET_MODE`` payload: ``[mode:u8]`` (``DCMT_MODE_*``)."""
160
+ return _U8.pack(u8("mode", mode))
161
+
162
+
163
+ def dcmt_send_set_setpoint(target1: int, target2: int) -> bytes:
164
+ """``DCMT_OP_SET_SETPOINT`` payload: ``[target1:i16][target2:i16]``."""
165
+ return _I16_I16.pack(i16("target1", target1), i16("target2", target2))
166
+
167
+
168
+ def dcmt_send_set_pid(
169
+ kp1_x10: int, ki1_x10: int, kd1_x10: int, kp2_x10: int, ki2_x10: int, kd2_x10: int
170
+ ) -> bytes:
171
+ """``DCMT_OP_SET_PID`` payload: six ``u8`` gains, each ten times the gain."""
172
+ return _PID.pack(
173
+ u8("kp1_x10", kp1_x10),
174
+ u8("ki1_x10", ki1_x10),
175
+ u8("kd1_x10", kd1_x10),
176
+ u8("kp2_x10", kp2_x10),
177
+ u8("ki2_x10", ki2_x10),
178
+ u8("kd2_x10", kd2_x10),
179
+ )
180
+
181
+
182
+ def dcmt_send_set_watchdog(timeout_ms: int) -> bytes:
183
+ """``BREAD_OP_SET_WATCHDOG`` payload: ``[timeout_ms:u16]``, 0 = disarm."""
184
+ return _U16.pack(u16("timeout_ms", timeout_ms))
185
+
186
+
187
+ def dcmt_query_state() -> bytes:
188
+ """SET_REPLY payload requesting ``DCMT_OP_GET_STATE``."""
189
+ return _U8.pack(DCMT_OP_GET_STATE)
190
+
191
+
192
+ def dcmt_query_version() -> bytes:
193
+ """SET_REPLY payload requesting the version reply."""
194
+ return _U8.pack(BREAD_OP_GET_VERSION)
195
+
196
+
197
+ def dcmt_query_caps() -> bytes:
198
+ """SET_REPLY payload requesting ``BREAD_OP_GET_CAPS``."""
199
+ return _U8.pack(BREAD_OP_GET_CAPS)
200
+
201
+
202
+ def dcmt_query_watchdog() -> bytes:
203
+ """SET_REPLY payload requesting ``BREAD_OP_GET_WATCHDOG``."""
204
+ return _U8.pack(BREAD_OP_GET_WATCHDOG)
205
+
206
+
207
+ def dcmt_parse_state_payload(payload: bytes) -> DcmtStateResult:
208
+ """Parse a GET_STATE reply payload (``dcmt_parse_state_payload``).
209
+
210
+ Like the C parser, this rejects any length other than
211
+ ``DCMT_STATE_FIXED_LEN``, trailing bytes included.
212
+ """
213
+ require_exact_len("DCMT GET_STATE", payload, DCMT_STATE_FIXED_LEN)
214
+ return DcmtStateResult(*_STATE.unpack_from(payload))
File without changes