diff --git a/.fusa-reqs.json b/.fusa-reqs.json index d40a02a..8186fb1 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -301,7 +301,7 @@ { "id": "REQ-PWR-002", "title": "Ordinary power-mode transition shape", - "text": "is_power_mode_transition_defined allows only Normal<->StandBy and StandBy<->Sleep; the direct Normal<->Sleep hop, any pair naming Unpowered, and staying in the same mode are all undefined", + "text": "is_power_mode_transition_defined allows exactly the two ordinary (non-start-up) edges of TC18 v0.5.1_RC §12.4 Figure 17 'power and operation modes' (p.46): Normal->StandBy ('Go to StandBy') and Normal->Sleep ('Go to Sleep'). Every other ordered pair is undefined, including StandBy<->Sleep in both directions (Figure 17 draws no edge between the two low-power modes; each is reached from and returns to Normal only), the two wake-up edges back to Normal (start-up paths owned by try_hot_start/try_cold_start), any pair naming Unpowered, and staying in the same mode", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -363,8 +363,8 @@ }, { "id": "REQ-PWRSTART-001", - "title": "try_cold_start requires Unpowered origin and gate", - "text": "try_cold_start returns Ok(PowerMode::Normal) only when from == PowerMode::Unpowered and gate satisfies is_power_mode_gate_satisfied; Err(RcpError::RequestRejected) otherwise, including from any powered mode or an ungated Unpowered origin", + "title": "try_cold_start admits both TC18 cold-start origins (Unpowered and Sleep)", + "text": "TC18 v0.5.1_RC §12.4.1 'Power-On / Wake-Up / Start-Up behavior' (p.46) defines the cold start with two origins — 'a cold start (after power-on or wake-up from sleep)' — and §12.4 Figure 17 labels both the Unpowered->Normal and the Sleep->Normal arrow 'Cold start'. try_cold_start therefore returns Ok(PowerMode::Normal) when from is PowerMode::Unpowered or PowerMode::Sleep and gate satisfies is_power_mode_gate_satisfied, and Err(RcpError::RequestRejected) otherwise — including from PowerMode::Normal, from PowerMode::StandBy (whose resume is the hot start, REQ-PWRSTART-002), and from a valid origin whose gate is unsatisfied. No WakeUp handshake gates this path: §12.4.1 attaches the handshake to the 'Hot-start-up procedure' only", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -372,8 +372,8 @@ }, { "id": "REQ-PWRSTART-002", - "title": "try_hot_start requires Sleep origin, acknowledged handshake, and gate", - "text": "try_hot_start returns Ok(PowerMode::Normal) only when from == PowerMode::Sleep, wakeup == WakeUpHandshakeState::Acknowledged, and gate satisfies is_power_mode_gate_satisfied; Err(RcpError::RequestRejected) otherwise", + "title": "try_hot_start requires StandBy origin, acknowledged handshake, and gate", + "text": "TC18 v0.5.1_RC §12.4.1 (p.46) defines the hot start with exactly one origin — 'a hot start (=wake-up from StandBy)' — drawn in §12.4 Figure 17 as the single arrow labelled 'Hot start', and it is §12.4.1's 'Hot-start-up procedure' that specifies the repeated WakeUp message awaiting 'a valid AVTPDU from the sleep request Client', which WakeUpHandshakeState models. try_hot_start therefore returns Ok(PowerMode::Normal) only when from == PowerMode::StandBy, wakeup == WakeUpHandshakeState::Acknowledged, and gate satisfies is_power_mode_gate_satisfied; Err(RcpError::RequestRejected) otherwise, including from PowerMode::Sleep even with an acknowledged handshake (that origin is a cold start, REQ-PWRSTART-001)", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2694,8 +2694,8 @@ }, { "id": "REQ-RMAP-015", - "title": "RequestStreamConfigEntry models the §3.8 per-stream config row", - "text": "RequestStreamConfigEntry carries rx_stream_id and the remaining fifteen per-stream fields this crate's own §3.8 extraction names, and reports CATEGORY == lifecycle::RegisterCategory::RcpConfig", + "title": "RequestStreamConfigEntry models TC18 Table 22's per-stream config row", + "text": "RequestStreamConfigEntry carries the sixteen fields TC18 v0.5.1_RC §12.7.7 Table 22 'Request stream configuration' (pp.57-58) tabulates — rx_stream_id (0x0000, 64 bit), rx_stream_max_request_size (0x0008, 16 bit), rx_wd_timeout_interval (0x000A, 16 bit), rx_secure_channel_index (0x000C, 8 bit), the eight 1-bit flags rx_enforce_e2e through rx_wd_info_enable sharing the bit-addressed byte 0x000D, rx_safestate_sequencer (0x000E), rx_safe_sequencer_state (0x000F), rx_ack_stream_index (0x0010), and rx_resp_stream_index (0x0011) — with the eight flags typed bool to match their 1-bit width, and reports CATEGORY == lifecycle::RegisterCategory::RcpConfig", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2703,8 +2703,8 @@ }, { "id": "REQ-RMAP-016", - "title": "RequestStreamConfigEntry encode/decode round-trip", - "text": "RequestStreamConfigEntry::encode/decode round-trip every field exactly (including all-default and all-max-value instances) via a fixed-length big-endian wire form, and decode ignores any trailing bytes beyond ENCODED_LEN", + "title": "RequestStreamConfigEntry encodes TC18 Table 22's 24-byte row layout", + "text": "RequestStreamConfigEntry::ENCODED_LEN is 24, the row stride fixed by Table 22's own tabulation of the next row's rx_stream_id2 at relative address 0x0018. encode places every field at its tabulated relative address, packs the eight 1-bit flags into the single byte at FLAGS_OFFSET == 0x000D with rx_enforce_e2e at bit .0 through rx_wd_info_enable at bit .7 in the order Table 22 lists them, and writes the reserved 16-bit block at 0x0012 and reserved 32-bit block at 0x0014 as zero. encode/decode round-trip every specified field exactly (including all-default and all-flags-set instances) and decode ignores any trailing bytes beyond ENCODED_LEN", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2712,8 +2712,8 @@ }, { "id": "REQ-RMAP-017", - "title": "RequestStreamConfigEntry decode rejects short input and never panics", - "text": "RequestStreamConfigEntry::decode returns Err(RcpError::ShortFrame) for every input shorter than ENCODED_LEN and never panics for any input length", + "title": "RequestStreamConfigEntry decode rejects short input, ignores reserved bytes, and never panics", + "text": "RequestStreamConfigEntry::decode returns Err(RcpError::ShortFrame) for every input shorter than ENCODED_LEN, ignores the content of TC18 Table 22's reserved bytes 0x0012-0x0017 entirely (so any reserved content decodes to the same value), and never panics for any input length", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2721,8 +2721,8 @@ }, { "id": "REQ-RMAP-018", - "title": "EpByteBusIdMapEntry models the §3.9 stream/byte_bus_id-to-endpoint row and its end-of-table sentinel", - "text": "EpByteBusIdMapEntry carries map_stream_index, map_byte_bus_id, and map_ep_nr, reports CATEGORY == lifecycle::RegisterCategory::RcpConfig, and is_end_of_table returns true iff map_stream_index == END_OF_TABLE_STREAM_INDEX (0); this module defines no ascending-order validation or enforcement over a table's rows", + "title": "EpByteBusIdMapEntry models TC18 Table 23's EP_ID_config row and its end-of-table sentinel", + "text": "EpByteBusIdMapEntry carries the three fields TC18 v0.5.1_RC §12.7.8 Table 23 'EP_ID_config' (p.59) tabulates — map_stream_index (Request_Stream_Index, 0x0000, 8 bit), map_ep_nr (EP_Nr, 0x0001, 8 bit), and map_byte_bus_id (BBID, 0x0002, 16 bit carrying an 11-bit byte_bus_id) — reports CATEGORY == lifecycle::RegisterCategory::RcpConfig, and is_end_of_table returns true iff map_stream_index == END_OF_TABLE_STREAM_INDEX (0), per Table 23's note 'A Request_Stream_Index = 0 denotes the end of the table'. Per that same section, keeping rows in ascending order is the writing client's responsibility, so this module defines no ascending-order validation or enforcement", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2730,8 +2730,8 @@ }, { "id": "REQ-RMAP-019", - "title": "EpByteBusIdMapEntry encode/decode round-trip", - "text": "EpByteBusIdMapEntry::encode/decode round-trip every field exactly via a fixed-length big-endian wire form, and decode ignores any trailing bytes beyond ENCODED_LEN", + "title": "EpByteBusIdMapEntry encodes TC18 Table 23's 4-byte row layout", + "text": "EpByteBusIdMapEntry::ENCODED_LEN is 4, the row stride fixed by Table 23's own tabulation of the next row's 2_Request_Stream_Index at relative address 0x0004. encode emits [Request_Stream_Index, EP_Nr, BBID big-endian] in that order — EP_Nr at 0x0001 precedes BBID at 0x0002 — and encode/decode round-trip every field exactly; decode ignores any trailing bytes beyond ENCODED_LEN", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2775,8 +2775,8 @@ }, { "id": "REQ-RMAP-024", - "title": "SequencerStateEntry models the §3.11 per-sequencer state register and its power-on default", - "text": "SequencerStateEntry carries seq_state, reports CATEGORY == lifecycle::RegisterCategory::RcpConfig, and power_on_default() returns seq_state == 1", + "title": "SequencerStateEntry models both TC18 Table 25 fields and the power-on default", + "text": "SequencerStateEntry carries both fields TC18 v0.5.1_RC §12.7.10 Table 25 'SEQUENCER_config' (p.61) tabulates per sequencer — seq_state (0x0000, 8 bit) and request_stream_index (0x0001, 8 bit, 'refers the Client Nr allowed to access this sequencer', the access-control binding §12.7.10 describes as 'Each sequencer is dedicated to a specific RC Client and its bound endpoints') — giving a 2-byte row stride, fixed by Table 25's own tabulation of Seq_2's Seq_state at relative address 0x0002. It reports CATEGORY == lifecycle::RegisterCategory::RcpConfig, and power_on_default() returns seq_state == 1 per §12.7.10 ('Upon power-on reset all sequencer state values are set to \"1\"') with request_stream_index == 0, Table 25 documenting no default for that field", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2784,8 +2784,8 @@ }, { "id": "REQ-RMAP-025", - "title": "SequencerStateEntry encode/decode round-trip", - "text": "SequencerStateEntry::encode/decode round-trip every field exactly via a fixed-length wire form, and decode ignores any trailing bytes beyond ENCODED_LEN", + "title": "SequencerStateEntry encodes TC18 Table 25's 2-byte row layout", + "text": "SequencerStateEntry::ENCODED_LEN is 2 and encode emits [seq_state, request_stream_index] at Table 25's relative addresses 0x0000 and 0x0001, so that a multi-sequencer table decoded through decode_rows resolves Seq_2's Seq_state at offset 0x0002. encode/decode round-trip both fields exactly, and decode ignores any trailing bytes beyond ENCODED_LEN", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2794,7 +2794,7 @@ { "id": "REQ-RMAP-026", "title": "SequencerStateEntry decode rejects short input and never panics", - "text": "SequencerStateEntry::decode returns Err(RcpError::ShortFrame) for every input shorter than ENCODED_LEN and never panics for any input length", + "text": "SequencerStateEntry::decode returns Err(RcpError::ShortFrame) for every input shorter than ENCODED_LEN (2), including a 1-byte input, and never panics for any input length", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", diff --git a/CHANGELOG.md b/CHANGELOG.md index 0176571..4540364 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,100 @@ OPEN Alliance TC18 core replacement; from `v1.0.0` on, each entry is a real release. See `docs/SEMVER.md` for the versioning scheme, including why a wire-format change is a MAJOR bump even when it is a fix. +## v5.0.0 (2026-07-31 TC18-conformant power-mode model + register-map config tables) — closed + +**Breaking**, on the wire for three register-map config-table row types and +behaviourally for the power-mode model. Four independently-confirmed +findings, each verified against the specification's own tables and figures +before being changed — and, where the layout question was one of bit +packing or table structure that text extraction cannot settle, against a +600/300 dpi render of the relevant page rather than extracted text. + +The register-map row types are not yet reachable from any live decode path +(nothing in this crate performs register I/O against a real RC Server), so +the interop urgency is lower than `v3.0.0`/`v4.0.0`; the power-mode finding +is a real behavioural bug in code that is callable today. + +A common root cause runs through three of the four: this crate's own +provenance notes asserted that the specification records these tables' +field names and purpose "in prose" with "no explicit per-field bit-width or +byte-offset table" and "no textual basis for a specific bit-position +assignment". That was false — §12.7.7 Table 22, §12.7.8 Table 23 and +§12.7.10 Table 25 each carry explicit "Relative address" and "Type" +columns, and Table 22 additionally gives `0x000D.0`-`0x000D.7` bit +addresses. The layouts built on that false scarcity were wrong, and the +notes have been corrected rather than merely the code. + +A second contributing cause: every affected encoder's tests asserted only +`decode(encode(x)) == x`. A round trip through one's own encoder cannot +detect a wrong layout. Every fix below adds literal expected-byte vectors +laid out by hand from the specification's address columns, chosen so that a +transposition or a mis-sized row cannot pass. + +- **rust-RCP-P01 (BREAKING, ASIL-B):** the cold-/hot-start mapping in + `src/powerstate.rs` was inverted. §12.4.1 "Power-On / Wake-Up / Start-Up + behavior" (p.46) states it in one sentence — "There are two types of + start-up: a cold start (after power-on **or wake-up from sleep**) and a + hot start (**=wake-up from StandBy**)" — and §12.4 Figure 17 labels the + arrows to match. `try_cold_start` accepted only `Unpowered -> Normal`, + omitting the `Sleep -> Normal` cold start entirely; `try_hot_start` + claimed `Sleep -> Normal` and gated it behind the WakeUp handshake, when + TC18's hot start is `StandBy -> Normal` and it is the *hot*-start + procedure that §12.4.1 attaches that handshake to. Net effect on a caller + wiring this up: every wake-from-sleep was rejected until a handshake that + the specification does not require there had completed, and every + wake-from-standby was rejected outright, there being no path admitting + that origin at all. `try_cold_start` now admits both documented origins + and `try_hot_start` admits `StandBy`. +- **rust-RCP-P02 (BREAKING):** `is_power_mode_transition_defined` accepted + `StandBy <-> Sleep` as an ordinary transition. Figure 17 draws no edge of + any kind between the two low-power modes — it places `Normal`/`StandBy` + in a "Powered" box and `Sleep` in a separate "Only part of PHY powered" + box, with both low-power modes entered from and returned to `Normal` + only. It also omitted `Normal -> Sleep`, which Figure 17 *does* draw + ("Go to Sleep"). The function now returns `true` for exactly Figure 17's + two "Go to ..." edges, `Normal -> StandBy` and `Normal -> Sleep`, and + `false` for all fourteen other ordered pairs. The prior set was derived + from reading the four mode *names* as a depth ordering, which the + specification never states; that inference is what produced P01 as well, + and the module's doc comment now reproduces Figure 17's edge list + directly instead. +- **rust-RCP-P03 (BREAKING, wire):** `regmap::RequestStreamConfigEntry` + used the wrong row layout. §12.7.7 Table 22 (pp.57-58) packs eight + per-stream flags (`rx_enforce_e2e` .. `rx_wd_info_enable`) into the + single bit-addressed byte at `0x000D` — they are the only fields in the + table addressed with a `.bit` suffix — and closes each row with a 16-bit + reserved word at `0x0012` and a 32-bit reserved block at `0x0014`, the + next row's `rx_stream_id2` at `0x0018` fixing the stride at **24 bytes**. + This crate gave each flag a whole byte of its own and dropped all six + reserved bytes, for `ENCODED_LEN = 25` and a wrong offset for every field + from `0x000D` onward. `ENCODED_LEN` is now 24, the eight flag fields are + typed `bool` to match their 1-bit width (a `u8` could not round-trip + losslessly through one bit), and `FLAGS_OFFSET` plus eight `FLAG_*` mask + constants and a `flags_byte()` accessor expose the packing. +- **rust-RCP-P04 (BREAKING, wire):** `regmap::EpByteBusIdMapEntry` + transposed two fields. §12.7.8 Table 23 "EP_ID_config" (p.59) tabulates + `Request_Stream_Index` at `0x0000`, `EP_Nr` at `0x0001` and `BBID` at + `0x0002`; this crate emitted `[stream_index, BBID_hi, BBID_lo, EP_Nr]`. + The row length was coincidentally right; the middle three bytes were not. +- **rust-RCP-P05 (BREAKING, wire):** `regmap::SequencerStateEntry` modeled + one of the row's two fields. §12.7.10 Table 25 "SEQUENCER_config" (p.61) + gives each sequencer `Seq_state` at `0x0000` **and** + `Request_stream_index` at `0x0001` — the latter being the access-control + binding the section describes ("Each sequencer is dedicated to a specific + RC Client and its bound endpoints"; the field "refers the Client Nr + allowed to access this sequencer") — with `Seq_2` at `0x0002` fixing the + stride at 2 bytes. This crate carried only `seq_state` with + `ENCODED_LEN = 1`, so a multi-sequencer table read through `decode_rows` + both lost every sequencer's client binding and misaligned every row after + the first. The field is now present and `ENCODED_LEN` is 2. + +Not changed, and still to reconcile: `regmap::ResponseStreamConfigEntry`'s +layout has not been checked against §12.7.9 in this pass and remains this +crate's own inference; and `RequestStreamConfigEntry::default()` is still +all-zero, where Table 22 documents a default of `1` for +`rx_resp_stream_index`. + ## v4.0.0 (2026-07-31 TC18-conformant NTSCF/TSCF AVTPDU header) — closed **Breaking, on the wire.** `src/avtp.rs`'s NTSCF and TSCF header diff --git a/Cargo.lock b/Cargo.lock index a8de7d7..2423eb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,7 +333,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rcp" -version = "4.0.0" +version = "5.0.0" dependencies = [ "async-trait", "base64", diff --git a/Cargo.toml b/Cargo.toml index e098cf4..9249aeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rcp" -version = "4.0.0" +version = "5.0.0" edition = "2021" rust-version = "1.75" license = "MPL-2.0" diff --git a/docs/PUBLIC_API.txt b/docs/PUBLIC_API.txt index 3e261fa..0f2c6dc 100644 --- a/docs/PUBLIC_API.txt +++ b/docs/PUBLIC_API.txt @@ -2768,26 +2768,36 @@ impl core::panic::unwind_safe::RefUnwindSafe for rcp::regmap::PerEpTypeFunctiona impl core::panic::unwind_safe::UnwindSafe for rcp::regmap::PerEpTypeFunctionalConfig pub struct rcp::regmap::RequestStreamConfigEntry pub rcp::regmap::RequestStreamConfigEntry::rx_ack_stream_index: u8 -pub rcp::regmap::RequestStreamConfigEntry::rx_enforce_e2e: u8 -pub rcp::regmap::RequestStreamConfigEntry::rx_enforce_seq: u8 -pub rcp::regmap::RequestStreamConfigEntry::rx_ovrflw_safestate_enable: u8 +pub rcp::regmap::RequestStreamConfigEntry::rx_enforce_e2e: bool +pub rcp::regmap::RequestStreamConfigEntry::rx_enforce_seq: bool +pub rcp::regmap::RequestStreamConfigEntry::rx_ovrflw_safestate_enable: bool pub rcp::regmap::RequestStreamConfigEntry::rx_resp_stream_index: u8 pub rcp::regmap::RequestStreamConfigEntry::rx_safe_sequencer_state: u8 pub rcp::regmap::RequestStreamConfigEntry::rx_safestate_sequencer: u8 -pub rcp::regmap::RequestStreamConfigEntry::rx_safety_measure: u8 +pub rcp::regmap::RequestStreamConfigEntry::rx_safety_measure: bool pub rcp::regmap::RequestStreamConfigEntry::rx_secure_channel_index: u8 -pub rcp::regmap::RequestStreamConfigEntry::rx_seq_safestate_enable: u8 +pub rcp::regmap::RequestStreamConfigEntry::rx_seq_safestate_enable: bool pub rcp::regmap::RequestStreamConfigEntry::rx_stream_id: u64 pub rcp::regmap::RequestStreamConfigEntry::rx_stream_max_request_size: u16 -pub rcp::regmap::RequestStreamConfigEntry::rx_wd_enable: u8 -pub rcp::regmap::RequestStreamConfigEntry::rx_wd_info_enable: u8 -pub rcp::regmap::RequestStreamConfigEntry::rx_wd_safestate_enable: u8 +pub rcp::regmap::RequestStreamConfigEntry::rx_wd_enable: bool +pub rcp::regmap::RequestStreamConfigEntry::rx_wd_info_enable: bool +pub rcp::regmap::RequestStreamConfigEntry::rx_wd_safestate_enable: bool pub rcp::regmap::RequestStreamConfigEntry::rx_wd_timeout_interval: u16 impl rcp::regmap::RequestStreamConfigEntry pub const rcp::regmap::RequestStreamConfigEntry::CATEGORY: rcp::lifecycle::RegisterCategory pub const rcp::regmap::RequestStreamConfigEntry::ENCODED_LEN: usize +pub const rcp::regmap::RequestStreamConfigEntry::FLAGS_OFFSET: usize +pub const rcp::regmap::RequestStreamConfigEntry::FLAG_ENFORCE_E2E: u8 +pub const rcp::regmap::RequestStreamConfigEntry::FLAG_ENFORCE_SEQ: u8 +pub const rcp::regmap::RequestStreamConfigEntry::FLAG_OVRFLW_SAFESTATE_ENABLE: u8 +pub const rcp::regmap::RequestStreamConfigEntry::FLAG_SAFETY_MEASURE: u8 +pub const rcp::regmap::RequestStreamConfigEntry::FLAG_SEQ_SAFESTATE_ENABLE: u8 +pub const rcp::regmap::RequestStreamConfigEntry::FLAG_WD_ENABLE: u8 +pub const rcp::regmap::RequestStreamConfigEntry::FLAG_WD_INFO_ENABLE: u8 +pub const rcp::regmap::RequestStreamConfigEntry::FLAG_WD_SAFESTATE_ENABLE: u8 pub fn rcp::regmap::RequestStreamConfigEntry::decode(&[u8]) -> core::result::Result -pub fn rcp::regmap::RequestStreamConfigEntry::encode(&self) -> [u8; 25] +pub fn rcp::regmap::RequestStreamConfigEntry::encode(&self) -> [u8; 24] +pub fn rcp::regmap::RequestStreamConfigEntry::flags_byte(&self) -> u8 impl core::clone::Clone for rcp::regmap::RequestStreamConfigEntry pub fn rcp::regmap::RequestStreamConfigEntry::clone(&self) -> rcp::regmap::RequestStreamConfigEntry impl core::cmp::Eq for rcp::regmap::RequestStreamConfigEntry @@ -2852,12 +2862,13 @@ impl core::marker::UnsafeUnpin for rcp::regmap::ResponseStreamConfigEntry impl core::panic::unwind_safe::RefUnwindSafe for rcp::regmap::ResponseStreamConfigEntry impl core::panic::unwind_safe::UnwindSafe for rcp::regmap::ResponseStreamConfigEntry pub struct rcp::regmap::SequencerStateEntry +pub rcp::regmap::SequencerStateEntry::request_stream_index: u8 pub rcp::regmap::SequencerStateEntry::seq_state: u8 impl rcp::regmap::SequencerStateEntry pub const rcp::regmap::SequencerStateEntry::CATEGORY: rcp::lifecycle::RegisterCategory pub const rcp::regmap::SequencerStateEntry::ENCODED_LEN: usize pub fn rcp::regmap::SequencerStateEntry::decode(&[u8]) -> core::result::Result -pub fn rcp::regmap::SequencerStateEntry::encode(&self) -> [u8; 1] +pub fn rcp::regmap::SequencerStateEntry::encode(&self) -> [u8; 2] pub fn rcp::regmap::SequencerStateEntry::power_on_default() -> Self impl core::clone::Clone for rcp::regmap::SequencerStateEntry pub fn rcp::regmap::SequencerStateEntry::clone(&self) -> rcp::regmap::SequencerStateEntry diff --git a/docs/SEMVER.md b/docs/SEMVER.md index c5dd671..03c1724 100644 --- a/docs/SEMVER.md +++ b/docs/SEMVER.md @@ -27,8 +27,9 @@ and the version has moved normally ever since. one.** A change to the bytes this crate puts on the wire breaks interoperation with any peer built against a prior release just as surely as removing a `pub` item breaks compilation, so it takes a MAJOR bump even -when — as in `v3.0.0` (ACF message layout) and `v4.0.0` (NTSCF/TSCF header -layout) — the change is a *fix* and the previous bytes were nobody's +when — as in `v3.0.0` (ACF message layout), `v4.0.0` (NTSCF/TSCF header +layout) and `v5.0.0` (register-map config-table row layouts) — the change +is a *fix* and the previous bytes were nobody's correct behavior. Calling such a release a PATCH would hide, behind a version number that promises compatibility, the one thing a downstream integrator most needs told. (`v3.1.0`, which reordered the E2E CRC diff --git a/src/powerstate.rs b/src/powerstate.rs index d3156d7..6801cb0 100644 --- a/src/powerstate.rs +++ b/src/powerstate.rs @@ -25,18 +25,22 @@ //! //! - [`PowerMode`] — the four power modes themselves: [`PowerMode::Normal`], //! [`PowerMode::StandBy`], [`PowerMode::Sleep`], and -//! [`PowerMode::Unpowered`]. See "Provenance note: mode ordering and -//! `Unpowered`'s software-model semantics" below for how this crate reads -//! the four names' own relative depth and for what `Unpowered` can and -//! cannot mean for a running software model. +//! [`PowerMode::Unpowered`], each as TC18 v0.5.1_RC §12.4 defines it. See +//! "Mode reachability, per TC18 §12.4 Figure 17" below for which modes +//! are reachable from which, and "Provenance note: `Unpowered`'s +//! software-model semantics" for what `Unpowered` can and cannot mean for +//! a running software model. //! - [`is_power_mode_transition_defined`] — the coarse, state-shape check //! (mirroring [`crate::lifecycle::is_transition_defined`]'s own role) -//! naming exactly which powered-mode pairs this module implements a -//! transition between: `Normal` <-> `StandBy` and `StandBy` <-> `Sleep`. -//! `Unpowered` is deliberately excluded from this check — see -//! [`shutdown_to_unpowered`] and the cold/hot-start functions below for -//! why entering or leaving `Unpowered` is never a member of this -//! ordinary, gated transition set. +//! naming exactly which powered-mode pairs this module implements an +//! *ordinary* (non-start-up) transition between: the two "Go to" edges of +//! the specification's own state diagram, `Normal` -> `StandBy` and +//! `Normal` -> `Sleep`. The two reverse edges are start-up paths with +//! their own extra gating and their own functions ([`try_hot_start`] for +//! `StandBy` -> `Normal`, [`try_cold_start`] for `Sleep` -> `Normal`), so +//! they are deliberately not members of this ordinary set. `Unpowered` is +//! likewise excluded — see [`shutdown_to_unpowered`] and the +//! cold/hot-start functions below. //! - [`PowerModeGateInput`] / [`is_power_mode_gate_satisfied`] — the //! shared entry/exit precondition `ROADMAP.md`'s own checklist wording //! names as the reason this item is sequenced into Milestone 6 at all: @@ -57,21 +61,28 @@ //! [`crate::lifecycle::RcServerState::try_transition`]'s own //! unconditional `HW_CONFIGURED` -> `HW_UNCONFIGURED` demotion path. //! - [`StartupPath`] / [`try_cold_start`] / [`try_hot_start`] — the two -//! distinct startup paths this checklist bullet names by name: +//! distinct start-up types the specification defines by name (TC18 +//! v0.5.1_RC §12.4.1 "Power-On / Wake-Up / Start-Up behavior", p.46: +//! "There are two types of start-up: a cold start (after power-on or +//! wake-up from sleep) and a hot start (=wake-up from StandBy)"). //! [`try_cold_start`] (`StartupPath::Cold`) is the plain, no-handshake -//! `Unpowered` -> `Normal` path; [`try_hot_start`] (`StartupPath::Hot`) -//! is the `Sleep` -> `Normal` path, additionally gated by the -//! hot-start-from-Sleep WakeUp handshake below reaching completion. +//! path admitting **both** documented cold-start origins, `Unpowered` -> +//! `Normal` and `Sleep` -> `Normal`; [`try_hot_start`] +//! (`StartupPath::Hot`) is the `StandBy` -> `Normal` path, additionally +//! gated by the hot-start WakeUp handshake below reaching completion. //! Both remain further gated by [`is_power_mode_gate_satisfied`], the //! same shared precondition [`try_enter_power_mode`] uses. //! - [`WakeUpHandshakeState`] / [`send_wakeup_request`] / //! [`acknowledge_wakeup_request`] / [`is_wakeup_handshake_complete`] — -//! the hot-start-from-Sleep WakeUp handshake itself, modeled as a real -//! two-step message exchange (`Idle` -> `RequestSent` -> -//! `Acknowledged`) rather than a single flag flip, per this checklist -//! bullet's own explicit wording. See "Provenance note: the WakeUp -//! handshake's own wire encoding" below for why this stays an abstract -//! state machine rather than a concrete wire message. +//! the hot-start WakeUp handshake itself, modeled as a real two-step +//! message exchange (`Idle` -> `RequestSent` -> `Acknowledged`) rather +//! than a single flag flip. §12.4.1's "Hot-start-up procedure" is what +//! attaches this handshake to the hot-start path specifically: the RC +//! Server "will send ... a repetitive response ... with a WakeUp message +//! and the WakeUp source. The message will be repeated until a valid +//! AVTPDU from the sleep request Client is received." See "Provenance +//! note: the WakeUp handshake's own wire encoding" below for why this +//! stays an abstract state machine rather than a concrete wire message. //! //! Same "additive standalone plumbing only" discipline as every Milestone //! 1-6 entry in `src/request.rs`/`src/e2e.rs`/`src/watchdog.rs`: every item @@ -80,23 +91,48 @@ //! receives a real WakeUp message over any transport, or is called from a //! decoder, CLI, or dispatch loop. //! -//! ## Provenance note: mode ordering and `Unpowered`'s software-model -//! semantics +//! ## Mode reachability, per TC18 §12.4 Figure 17 //! -//! `ROADMAP.md`'s checklist bullet names the four modes in the order -//! "Normal / StandBy / Sleep / Unpowered" but does not itself state their -//! relative depth or which pairs are directly reachable from which. Per -//! Guiding Principle 5, this module's working interpretation — flagged -//! here rather than asserted as spec fact — reads that ordering as -//! increasing power-down depth: `Normal` (fully operational) shallower than -//! `StandBy`, `StandBy` shallower than `Sleep`, `Sleep` shallower than -//! `Unpowered`. [`is_power_mode_transition_defined`] only names the two -//! adjacent, powered-mode pairs (`Normal`<->`StandBy`, `StandBy`<->`Sleep`) -//! as ordinary transitions, and reserves the deepest hop, `Sleep` all the -//! way back to `Normal`, for the dedicated hot-start path below — this -//! module's own reading of why the checklist bullet calls out the -//! "hot-start-**from-Sleep**" WakeUp handshake specifically, rather than a -//! handshake generic to any powered mode. +//! The set of transitions below is **not** an inference from the four mode +//! names' relative "depth" — earlier revisions of this module read it that +//! way, and got the cold/hot-start mapping exactly backwards as a result. +//! TC18 v0.5.1_RC §12.4 "Power- and operation modes", Figure 17 "power and +//! operation modes" (p.46) is a labelled state diagram that names every +//! edge explicitly. It has exactly five, and this module implements exactly +//! those five: +//! +//! | Edge | Figure 17 label | Implemented by | +//! |------|-----------------|----------------| +//! | `Unpowered` -> `Normal` | "Cold start" | [`try_cold_start`] | +//! | `Sleep` -> `Normal` | "Cold start" | [`try_cold_start`] | +//! | `StandBy` -> `Normal` | "Hot start" | [`try_hot_start`] | +//! | `Normal` -> `StandBy` | "Go to StandBy" | [`try_enter_power_mode`] | +//! | `Normal` -> `Sleep` | "Go to Sleep" | [`try_enter_power_mode`] | +//! +//! Two consequences worth stating, because both contradict what this file +//! previously asserted: +//! +//! - **There is no `StandBy` <-> `Sleep` edge at all.** Figure 17 places +//! `Normal` and `StandBy` inside a "Powered" box and `Sleep` inside a +//! separate "Only part of PHY powered" box, with no arrow of any kind +//! between `StandBy` and `Sleep`. Both low-power modes are entered from, +//! and returned to, `Normal` only. `Sleep` is not "one step down from +//! `StandBy`"; the two are siblings. +//! - **`Sleep` -> `Normal` is a cold start, not a hot start**, and it is +//! therefore *not* behind the WakeUp handshake. §12.4.1 states the +//! mapping in one sentence — "a cold start (after power-on or wake-up +//! from sleep) and a hot start (=wake-up from StandBy)" — and Figure 17's +//! arrow labels agree: the `Sleep` -> `Normal` arrow reads "Cold start" +//! and the `StandBy` -> `Normal` arrow reads "Hot start". This is +//! consistent with each mode's own §12.4 definition: `StandBy` "maintains +//! ... configuration data ... alive", so resuming from it needs no +//! reconfiguration (a hot start, §12.4.1: "In hot-start the configuration +//! does not need to be redone"), whereas `Sleep` is only defined as "the +//! mode with the lowest possible power c[on]sumptio[n] still being able +//! to be woken by a dedicated WakePin or the network interface" and +//! carries no such retention guarantee. +//! +//! ## Provenance note: `Unpowered`'s software-model semantics //! //! `Unpowered` itself cannot be a state a live RC Server process is //! actually running in — by definition, unpowered hardware runs no @@ -159,12 +195,18 @@ //! //! ## Provenance note: the WakeUp handshake's own wire encoding //! -//! `ROADMAP.md`'s checklist bullet names a "hot-start-from-Sleep WakeUp -//! message handshake" but the confidential OPEN Alliance TC18 Remote -//! Control Protocol Specification v0.5.1_RC's exact wire encoding for that -//! message (its ACF/AVTPDU framing, field layout, or byte values) is out of -//! reach of this item per this crate's own licensing constraint against -//! reproducing spec text. Per Guiding Principle 5, this module does not +//! TC18 §12.4.1's "Hot-start-up procedure" describes this handshake's +//! *behavior* — the RC Server sends a repetitive WakeUp response carrying +//! the WakeUp source over the responder stream configured for the original +//! standby request, repeating "until a valid AVTPDU from the sleep request +//! Client is received" — but the specification gives no field diagram for +//! the WakeUp message itself, so its exact wire encoding (ACF/AVTPDU +//! framing, field layout, byte values) is not recoverable from that text. +//! §12.4.1 also states two distinct wake-up sources ("an internal EP signal +//! of the RC Server or the dedicated wakepin", versus "a TC14/TC10 wake-up +//! request on the network"), which differ only in whether the network +//! interface must be enabled first — a distinction with no bearing on +//! handshake *progress*. Per Guiding Principle 5, this module does not //! guess one: [`WakeUpHandshakeState`] models the handshake's *progress* — //! a request sent, then acknowledged — as an abstract state machine a //! future transport-level item can drive from real decoded messages, @@ -201,25 +243,35 @@ use crate::RcpError; /// The RC Server's power mode, per the real four-mode model this item /// replaces the legacy `Active`/`Sleep`/`Standby` model with. /// -/// See this module's doc comment "Provenance note: mode ordering and -/// `Unpowered`'s software-model semantics" for this crate's own working -/// interpretation of the four variants' relative depth, and for what +/// See this module's doc comment "Mode reachability, per TC18 §12.4 +/// Figure 17" for which variants are reachable from which, and +/// "Provenance note: `Unpowered`'s software-model semantics" for what /// [`PowerMode::Unpowered`] can and cannot mean for a running process. #[derive(Debug, Clone, Copy, PartialEq, Eq)] // fusa:req REQ-PWR-001 pub enum PowerMode { - /// Fully operational — every endpoint may be driven normally. + /// Fully operational — every endpoint may be driven normally. TC18 + /// §12.4's "Powered" mode, in the lifecycle state the RC Server is + /// configured for. Figure 17 makes this the hub: it is the only mode + /// either low-power mode is entered from or returned to. Normal, - /// A shallow power-saving mode, one step down from `Normal`. + /// TC18 §12.4: "the mode in which the system maintains the lowest + /// possible power while keeping configuration data and functional wake + /// up sources alive." Because configuration survives, resuming from + /// here is the **hot start** — see [`try_hot_start`], which gates it + /// behind the WakeUp handshake. StandBy, - /// A deep power-saving mode, one step down from `StandBy`. Resuming - /// from this mode to `Normal` is the hot-start path this module - /// specifically gates behind the WakeUp handshake — see - /// [`try_hot_start`]. + /// TC18 §12.4: "the mode with the lowest possible power c[on]sumptio[n] + /// still being able to be woken by a dedicated WakePin or the network + /// interface." Figure 17 places this outside the "Powered" box, in + /// "Only part of PHY powered". No configuration-retention guarantee is + /// stated, and §12.4.1 correspondingly classes wake-up from here as a + /// **cold start** — see [`try_cold_start`]. It is *not* one step below + /// [`PowerMode::StandBy`]: no edge joins the two. Sleep, - /// No power is applied to the RC Server hardware. See this module's - /// doc comment for the boundary this crate draws around what this - /// variant can mean for a live process. + /// TC18 §12.4: "the mode in which no sufficient power supply is + /// available." See this module's doc comment for the boundary this + /// crate draws around what this variant can mean for a live process. Unpowered, } @@ -236,23 +288,32 @@ impl PowerMode { } } -/// Whether `(from, to)` is one of the two adjacent, powered-mode pairs this -/// module implements an ordinary transition between: `Normal` <-> -/// `StandBy` and `StandBy` <-> `Sleep`. +/// Whether `(from, to)` is one of the two *ordinary* (non-start-up) +/// transitions TC18 §12.4 Figure 17 defines: its "Go to StandBy" edge +/// (`Normal` -> `StandBy`) and its "Go to Sleep" edge +/// (`Normal` -> `Sleep`). +/// +/// Every other pair is `false`. In particular: /// -/// Every other pair is `false`, including staying in the same mode, the -/// direct `Normal` <-> `Sleep` hop (reserved for the dedicated hot-start -/// path — see [`try_hot_start`]), and any pair naming -/// [`PowerMode::Unpowered`] (reserved for [`shutdown_to_unpowered`] and -/// [`try_cold_start`]). Never panics for any input. +/// - **`StandBy` <-> `Sleep` is `false` in both directions.** Figure 17 has +/// no edge joining the two low-power modes; each is reached only from, +/// and returns only to, `Normal`. (Earlier releases of this crate wrongly +/// accepted this pair.) +/// - The two reverse, wake-up directions — `StandBy` -> `Normal` and +/// `Sleep` -> `Normal` — are `false` here because they are start-ups +/// carrying extra preconditions, handled by [`try_hot_start`] and +/// [`try_cold_start`] respectively rather than by +/// [`try_enter_power_mode`]. +/// - Any pair naming [`PowerMode::Unpowered`] is `false` (reserved for +/// [`shutdown_to_unpowered`] and [`try_cold_start`]), as is staying in +/// the same mode. +/// +/// Never panics for any input. // fusa:req REQ-PWR-002 pub fn is_power_mode_transition_defined(from: PowerMode, to: PowerMode) -> bool { matches!( (from, to), - (PowerMode::Normal, PowerMode::StandBy) - | (PowerMode::StandBy, PowerMode::Normal) - | (PowerMode::StandBy, PowerMode::Sleep) - | (PowerMode::Sleep, PowerMode::StandBy) + (PowerMode::Normal, PowerMode::StandBy) | (PowerMode::Normal, PowerMode::Sleep) ) } @@ -345,29 +406,42 @@ pub fn shutdown_to_unpowered(_from: PowerMode) -> PowerMode { // ── StartupPath / cold and hot start ───────────────────────────────────────── -/// The two distinct startup paths this checklist bullet names by name. +/// The two start-up types TC18 §12.4.1 names: "There are two types of +/// start-up: a cold start (after power-on or wake-up from sleep) and a hot +/// start (=wake-up from StandBy)." #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StartupPath { - /// Powering up from [`PowerMode::Unpowered`] with no prior session to - /// resume. See [`try_cold_start`]. + /// Powering up from [`PowerMode::Unpowered`], or waking from + /// [`PowerMode::Sleep`] — §12.4.1's "after power-on **or wake-up from + /// sleep**". Neither origin guarantees retained configuration, so after + /// either the RC Server comes up "in its configured lifecycle state", + /// possibly needing further configuration. See [`try_cold_start`]. Cold, - /// Resuming from [`PowerMode::Sleep`]. See [`try_hot_start`] for the - /// additional WakeUp-handshake gate this path alone requires. + /// Waking from [`PowerMode::StandBy`] — §12.4.1's "=wake-up from + /// StandBy", where "the configuration does not need to be redone, as it + /// shall be maintained during low-power mode". See [`try_hot_start`] + /// for the additional WakeUp-handshake gate this path alone requires. Hot, } -/// Attempt cold start: [`PowerMode::Unpowered`] -> [`PowerMode::Normal`], -/// with no WakeUp handshake involved. +/// Attempt cold start, with no WakeUp handshake involved. /// -/// Returns `Ok(PowerMode::Normal)` when `from` is -/// [`PowerMode::Unpowered`] and `gate` satisfies -/// [`is_power_mode_gate_satisfied`]. Returns -/// `Err(RcpError::RequestRejected)` when `from` is any other mode, or when -/// it is [`PowerMode::Unpowered`] but `gate` is not yet satisfied. Never -/// panics for any input. +/// TC18 §12.4.1 gives the cold start **two** origins — "a cold start (after +/// power-on or wake-up from sleep)" — and Figure 17 draws both as arrows +/// labelled "Cold start". Both are accepted here: +/// +/// - [`PowerMode::Unpowered`] -> [`PowerMode::Normal`] (power-on) +/// - [`PowerMode::Sleep`] -> [`PowerMode::Normal`] (wake-up from sleep) +/// +/// Returns `Ok(PowerMode::Normal)` when `from` is either of those and +/// `gate` satisfies [`is_power_mode_gate_satisfied`]. Returns +/// `Err(RcpError::RequestRejected)` when `from` is [`PowerMode::Normal`] +/// (already started) or [`PowerMode::StandBy`] (whose resume is the hot +/// start — see [`try_hot_start`]), or when the origin is valid but `gate` +/// is not yet satisfied. Never panics for any input. // fusa:req REQ-PWRSTART-001 pub fn try_cold_start(from: PowerMode, gate: PowerModeGateInput) -> Result { - if from != PowerMode::Unpowered { + if !matches!(from, PowerMode::Unpowered | PowerMode::Sleep) { return Err(RcpError::RequestRejected); } if !is_power_mode_gate_satisfied(gate) { @@ -376,11 +450,19 @@ pub fn try_cold_start(from: PowerMode, gate: PowerModeGateInput) -> Result [`PowerMode::Normal`], gated -/// by both a completed WakeUp handshake and the shared idle/no-pending- -/// response precondition. +/// Attempt hot start: [`PowerMode::StandBy`] -> [`PowerMode::Normal`], +/// gated by both a completed WakeUp handshake and the shared idle/no- +/// pending-response precondition. +/// +/// TC18 §12.4.1 defines the hot start as exactly one origin — "a hot start +/// (=wake-up from StandBy)", drawn in Figure 17 as the single arrow +/// labelled "Hot start" — and it is that section's "Hot-start-up procedure" +/// that specifies the repeated WakeUp message awaiting "a valid AVTPDU from +/// the sleep request Client", which [`WakeUpHandshakeState`] models. The +/// handshake therefore gates *this* path, not the `Sleep` -> `Normal` cold +/// start. (Earlier releases of this crate had the two origins swapped.) /// -/// Returns `Ok(PowerMode::Normal)` when `from` is [`PowerMode::Sleep`], +/// Returns `Ok(PowerMode::Normal)` when `from` is [`PowerMode::StandBy`], /// `wakeup` has reached [`WakeUpHandshakeState::Acknowledged`] (see /// [`is_wakeup_handshake_complete`]), and `gate` satisfies /// [`is_power_mode_gate_satisfied`]. Returns @@ -392,7 +474,7 @@ pub fn try_hot_start( wakeup: WakeUpHandshakeState, gate: PowerModeGateInput, ) -> Result { - if from != PowerMode::Sleep { + if from != PowerMode::StandBy { return Err(RcpError::RequestRejected); } if !is_wakeup_handshake_complete(wakeup) { @@ -406,10 +488,12 @@ pub fn try_hot_start( // ── WakeUp handshake ────────────────────────────────────────────────────────── -/// The hot-start-from-Sleep WakeUp handshake's own progress, modeled as a +/// The hot-start-from-StandBy WakeUp handshake's own progress, modeled as a /// real two-step message exchange rather than a single flag flip: a /// WakeUp request must be sent and then acknowledged before -/// [`try_hot_start`] will admit `Normal`. +/// [`try_hot_start`] will admit `Normal`. Per TC18 §12.4.1's "Hot-start-up +/// procedure", `Acknowledged` corresponds to that section's terminating +/// condition, "a valid AVTPDU from the sleep request Client is received". /// /// See this module's doc comment "Provenance note: the WakeUp handshake's /// own wire encoding" for why this stays an abstract progress marker @@ -527,33 +611,49 @@ mod tests { // ── is_power_mode_transition_defined ───────────────────────────────── + /// TC18 §12.4 Figure 17's two "Go to ..." edges, the only ordinary + /// (non-start-up) transitions the diagram draws. Both leave `Normal`. #[test] // fusa:test REQ-PWR-002 - fn adjacent_powered_pairs_are_defined_both_directions() { + fn figure_17_go_to_edges_are_the_ordinary_transitions() { + // "Go to StandBy" assert!(is_power_mode_transition_defined( PowerMode::Normal, PowerMode::StandBy )); + // "Go to Sleep" assert!(is_power_mode_transition_defined( - PowerMode::StandBy, - PowerMode::Normal + PowerMode::Normal, + PowerMode::Sleep )); - assert!(is_power_mode_transition_defined( + } + + /// Figure 17 draws no arrow of any kind between `StandBy` and `Sleep`: + /// they sit in different boxes ("Powered" vs "Only part of PHY + /// powered") and are reached only via `Normal`. Releases before + /// v5.0.0 wrongly accepted this pair in both directions. + #[test] + // fusa:test REQ-PWR-002 + fn standby_sleep_pair_is_not_a_transition_in_either_direction() { + assert!(!is_power_mode_transition_defined( PowerMode::StandBy, PowerMode::Sleep )); - assert!(is_power_mode_transition_defined( + assert!(!is_power_mode_transition_defined( PowerMode::Sleep, PowerMode::StandBy )); } + /// Figure 17's two wake-up edges back to `Normal` are start-ups with + /// their own extra preconditions, so they are not members of the + /// ordinary set — [`try_hot_start`] and [`try_cold_start`] own them. #[test] // fusa:test REQ-PWR-002 - fn direct_normal_sleep_hop_is_not_an_ordinary_transition() { + fn wakeup_edges_back_to_normal_are_not_ordinary_transitions() { assert!(!is_power_mode_transition_defined( - PowerMode::Normal, - PowerMode::Sleep + PowerMode::StandBy, + PowerMode::Normal )); assert!(!is_power_mode_transition_defined( PowerMode::Sleep, @@ -578,6 +678,25 @@ mod tests { } } + /// Exhaustive cross-product: exactly the two Figure 17 "Go to" edges, + /// and nothing else, out of all 16 ordered pairs. + #[test] + // fusa:test REQ-PWR-002 + fn exactly_two_ordered_pairs_are_defined() { + let defined: Vec<(PowerMode, PowerMode)> = ALL_MODES + .iter() + .flat_map(|&a| ALL_MODES.iter().map(move |&b| (a, b))) + .filter(|&(a, b)| is_power_mode_transition_defined(a, b)) + .collect(); + assert_eq!( + defined, + vec![ + (PowerMode::Normal, PowerMode::StandBy), + (PowerMode::Normal, PowerMode::Sleep), + ] + ); + } + // ── PowerModeGateInput / is_power_mode_gate_satisfied ──────────────── #[test] @@ -648,7 +767,7 @@ mod tests { Ok(PowerMode::StandBy) ); assert_eq!( - try_enter_power_mode(PowerMode::StandBy, PowerMode::Sleep, gate), + try_enter_power_mode(PowerMode::Normal, PowerMode::Sleep, gate), Ok(PowerMode::Sleep) ); } @@ -660,8 +779,14 @@ mod tests { all_endpoints_idle: true, no_pending_response: true, }; + // No Figure 17 edge joins the two low-power modes. assert_eq!( - try_enter_power_mode(PowerMode::Normal, PowerMode::Sleep, gate), + try_enter_power_mode(PowerMode::StandBy, PowerMode::Sleep, gate), + Err(RcpError::RequestRejected) + ); + // Wake-up edges belong to the start-up functions, not here. + assert_eq!( + try_enter_power_mode(PowerMode::StandBy, PowerMode::Normal, gate), Err(RcpError::RequestRejected) ); assert_eq!( @@ -704,27 +829,58 @@ mod tests { // ── try_cold_start ──────────────────────────────────────────────────── + /// TC18 §12.4.1: "a cold start (after power-on **or wake-up from + /// sleep**)". Both origins, and Figure 17 labels both arrows + /// "Cold start". Releases before v5.0.0 accepted only `Unpowered`. #[test] // fusa:test REQ-PWRSTART-001 - fn cold_start_succeeds_from_unpowered_when_gated() { + fn cold_start_succeeds_from_both_documented_origins_when_gated() { let gate = PowerModeGateInput { all_endpoints_idle: true, no_pending_response: true, }; + // "after power-on" assert_eq!( try_cold_start(PowerMode::Unpowered, gate), Ok(PowerMode::Normal) ); + // "or wake-up from sleep" + assert_eq!( + try_cold_start(PowerMode::Sleep, gate), + Ok(PowerMode::Normal) + ); } + /// `Sleep` -> `Normal` is a cold start, so it takes no WakeUp + /// handshake: §12.4.1 attaches the handshake to the "Hot-start-up + /// procedure" only. Releases before v5.0.0 gated this path behind the + /// handshake, blocking every wake-from-sleep that had not run one. #[test] // fusa:test REQ-PWRSTART-001 - fn cold_start_rejected_from_a_powered_mode() { + fn cold_start_from_sleep_needs_no_wakeup_handshake() { let gate = PowerModeGateInput { all_endpoints_idle: true, no_pending_response: true, }; - for &m in &[PowerMode::Normal, PowerMode::StandBy, PowerMode::Sleep] { + // The handshake state is not even an argument to try_cold_start; + // an origin in Sleep with the handshake still Idle must succeed. + assert!(!is_wakeup_handshake_complete(WakeUpHandshakeState::Idle)); + assert_eq!( + try_cold_start(PowerMode::Sleep, gate), + Ok(PowerMode::Normal) + ); + } + + /// `StandBy` is the hot-start origin, not a cold-start one, and + /// `Normal` is already started. + #[test] + // fusa:test REQ-PWRSTART-001 + fn cold_start_rejected_from_normal_and_standby() { + let gate = PowerModeGateInput { + all_endpoints_idle: true, + no_pending_response: true, + }; + for &m in &[PowerMode::Normal, PowerMode::StandBy] { assert_eq!(try_cold_start(m, gate), Err(RcpError::RequestRejected)); } } @@ -733,35 +889,40 @@ mod tests { // fusa:test REQ-PWRSTART-001 fn cold_start_rejected_when_not_gated() { let gate = PowerModeGateInput::default(); - assert_eq!( - try_cold_start(PowerMode::Unpowered, gate), - Err(RcpError::RequestRejected) - ); + for &m in &[PowerMode::Unpowered, PowerMode::Sleep] { + assert_eq!(try_cold_start(m, gate), Err(RcpError::RequestRejected)); + } } // ── try_hot_start ───────────────────────────────────────────────────── + /// TC18 §12.4.1: "a hot start (=wake-up from StandBy)", Figure 17's + /// single "Hot start" arrow. Releases before v5.0.0 had this origin + /// as `Sleep`. #[test] // fusa:test REQ-PWRSTART-002 - fn hot_start_succeeds_from_sleep_when_acknowledged_and_gated() { + fn hot_start_succeeds_from_standby_when_acknowledged_and_gated() { let gate = PowerModeGateInput { all_endpoints_idle: true, no_pending_response: true, }; assert_eq!( - try_hot_start(PowerMode::Sleep, WakeUpHandshakeState::Acknowledged, gate), + try_hot_start(PowerMode::StandBy, WakeUpHandshakeState::Acknowledged, gate), Ok(PowerMode::Normal) ); } #[test] // fusa:test REQ-PWRSTART-002 - fn hot_start_rejected_from_a_non_sleep_mode() { + fn hot_start_rejected_from_every_non_standby_mode() { let gate = PowerModeGateInput { all_endpoints_idle: true, no_pending_response: true, }; - for &m in &[PowerMode::Normal, PowerMode::StandBy, PowerMode::Unpowered] { + // Sleep in particular: that origin is a cold start (§12.4.1), so + // routing it through the hot-start path must be rejected even with + // a fully acknowledged handshake. + for &m in &[PowerMode::Normal, PowerMode::Sleep, PowerMode::Unpowered] { assert_eq!( try_hot_start(m, WakeUpHandshakeState::Acknowledged, gate), Err(RcpError::RequestRejected) @@ -781,7 +942,7 @@ mod tests { WakeUpHandshakeState::RequestSent, ] { assert_eq!( - try_hot_start(PowerMode::Sleep, state, gate), + try_hot_start(PowerMode::StandBy, state, gate), Err(RcpError::RequestRejected) ); } @@ -792,11 +953,36 @@ mod tests { fn hot_start_rejected_when_not_gated() { let gate = PowerModeGateInput::default(); assert_eq!( - try_hot_start(PowerMode::Sleep, WakeUpHandshakeState::Acknowledged, gate), + try_hot_start(PowerMode::StandBy, WakeUpHandshakeState::Acknowledged, gate), Err(RcpError::RequestRejected) ); } + /// Cross-cutting: the cold- and hot-start origin sets are disjoint and + /// together cover exactly the three non-`Normal` modes, matching + /// Figure 17's three inbound arrows to `Normal`. + #[test] + // fusa:test REQ-PWRSTART-002 + fn cold_and_hot_start_origins_partition_the_three_inbound_edges() { + let gate = PowerModeGateInput { + all_endpoints_idle: true, + no_pending_response: true, + }; + let cold: Vec = ALL_MODES + .iter() + .copied() + .filter(|&m| try_cold_start(m, gate).is_ok()) + .collect(); + let hot: Vec = ALL_MODES + .iter() + .copied() + .filter(|&m| try_hot_start(m, WakeUpHandshakeState::Acknowledged, gate).is_ok()) + .collect(); + assert_eq!(cold, vec![PowerMode::Sleep, PowerMode::Unpowered]); + assert_eq!(hot, vec![PowerMode::StandBy]); + assert!(cold.iter().all(|m| !hot.contains(m))); + } + // ── WakeUpHandshakeState progression ────────────────────────────────── #[test] diff --git a/src/regmap.rs b/src/regmap.rs index 4ca94b8..02daf57 100644 --- a/src/regmap.rs +++ b/src/regmap.rs @@ -374,38 +374,42 @@ //! //! ### Config tables provenance note //! -//! Unlike `§3.6`'s own table, this crate's own extraction of `§3.7`-`§3.11` -//! records each table's field *names* and *purpose* in prose, but no -//! explicit per-field bit-width or byte-offset table comparable to `§3.6`'s -//! own. Every field width chosen below — including which of the sixteen -//! `§3.8` fields are modeled as a single byte-aligned flag versus a wider -//! counter/index/identifier — is therefore this crate's own placeholder -//! inference, not a spec-cited fact, flagged here per Guiding Principle 5 -//! rather than presented as settled: +//! **Corrected in v5.0.0.** Earlier revisions of this note claimed the +//! specification recorded these tables' field *names* and *purpose* only, +//! with "no explicit per-field bit-width or byte-offset table" and "no +//! textual basis for a specific bit-position assignment" — and every layout +//! below was built on that claim. It was false. TC18 v0.5.1_RC's own +//! §12.7.7 Table 22, §12.7.8 Table 23, and §12.7.10 Table 25 each carry a +//! "Relative address" column, a "Type" column giving each field's exact bit +//! width, and — for Table 22 — `0x000D.0` through `0x000D.7` bit addresses +//! naming each flag's position within a shared byte. Three of the five row +//! types were laid out wrongly as a result; see the CHANGELOG for v5.0.0. +//! Each row type's own doc comment now reproduces the relevant table's +//! address/width columns and cites its section and page. +//! +//! Row stride is likewise not inferred: each of these tables tabulates the +//! *next* row's first field, which fixes the stride exactly +//! ([`RequestStreamConfigEntry`] 24 bytes from Table 22's `rx_stream_id2` +//! at `0x0018`, [`EpByteBusIdMapEntry`] 4 bytes from Table 23's +//! `2_Request_Stream_Index` at `0x0004`, [`SequencerStateEntry`] 2 bytes +//! from Table 25's `Seq_2` at `0x0002`). +//! +//! What genuinely does remain this crate's own inference, flagged here per +//! Guiding Principle 5 rather than presented as settled: //! //! - [`HwPinMappingEntry::hw_pin_props`] is left an undecomposed raw `u8`, //! the same choice [`GeneralRegisters::svr_implemented_options`] already -//! made for a packed multi-property byte with no recorded bit-position -//! assignment. -//! - Every one of [`RequestStreamConfigEntry`]'s eight documented on/off -//! behaviors (`rx_enforce_e2e` through `rx_wd_info_enable`) is modeled as -//! its own byte-aligned `u8` field rather than packed into a shared -//! bitmask, matching [`GeneralRegisters::svr_configuration_lock`]'s own -//! "meaningfully binary but wire-width `u8`" precedent — this crate has -//! no textual basis for a specific bit-position assignment if it *were* -//! to pack them, so it does not invent one. -//! - [`EpByteBusIdMapEntry::map_byte_bus_id`] is typed `u16`, matching -//! [`crate::acf::ByteMessageInfo::byte_bus_id`]'s own existing -//! already-established field width for the same wire concept, even -//! though this table's row-level encoding of it is otherwise unrelated to -//! ACF message framing. -//! - Every row is packed sequentially with no inter-field padding, matching -//! [`GeneralRegisters`]'s own sequential-byte-packing inference (see -//! above) rather than a newly-invented convention. +//! made for a packed multi-property byte. +//! - Reserved blocks ([`RequestStreamConfigEntry`]'s `0x0012` and `0x0014`) +//! are written as zero and ignored on decode rather than carried as +//! round-tripping fields — the same treatment [`crate::avtp`]'s TSCF +//! reserved quadlets get. A row therefore round-trips its *specified* +//! fields, not arbitrary reserved content. //! -//! A real RC Server's actual per-table byte layout must be reconciled -//! against this crate's own working guesses (never against spec prose) -//! before any of these encode/decode forms are relied on for interop. +//! [`ResponseStreamConfigEntry`]'s own layout has **not** been reconciled +//! against a spec table in this pass and remains this crate's own working +//! inference; it must be checked against §12.7.9 before being relied on for +//! interop. //! //! ## `Serialize`/`Deserialize` derives (`ROADMAP.md` Milestone 9, `config` //! ## REPLACE cutover) @@ -1212,14 +1216,51 @@ impl ConfigTableRow for HwPinMappingEntry { // ── RequestStreamConfigEntry (§3.8) ────────────────────────────────────────── -/// One row of the request-stream config table (`§3.8`): the receive-side +/// One row of the request-stream config table (TC18 v0.5.1_RC §12.7.7 +/// "Request stream configuration", Table 22, pp.57-58): the receive-side /// configuration for one stream the RC Server listens on. /// +/// # Wire layout (Table 22) +/// +/// Table 22 gives each field an explicit relative address, and the address +/// of the next stream's `rx_stream_id2` (`0x0018`) fixes the row stride at +/// **24 bytes**: +/// +/// ```text +/// 0x0000 rx_stream_id 64 bit +/// 0x0008 rx_stream_max_request_size 16 bit +/// 0x000A rx_wd_timeout_intervall 16 bit +/// 0x000C rx_secure_channel_index 8 bit +/// 0x000D flags byte, bit-addressed: +/// .0 rx_enforce_e2e .4 rx_wd_safestate_enable +/// .1 rx_enforce_seq .5 rx_ovrflw_safestate_enable +/// .2 rx_seq_safestate_enable .6 rx_safety_measure +/// .3 rx_wd_enable .7 rx_wd_info_enable +/// 0x000E rx_safestate_seqencer 8 bit +/// 0x000F rx_safe_sequencer_state 8 bit +/// 0x0010 rx_ack_stream_index 8 bit +/// 0x0011 rx_resp_stream_index 8 bit +/// 0x0012 Reserved 16 bit +/// 0x0014 Reserved 32 bit +/// 0x0018 (next row) +/// ``` +/// +/// The eight `1 bit` rows are the only fields Table 22 addresses with a +/// `.bit` suffix rather than a whole-byte address, and all eight share the +/// single address `0x000D` — so they pack into one byte, LSB first, in the +/// `.0`-`.7` order the table lists them. Releases before v5.0.0 gave each +/// flag a byte of its own and dropped the six reserved bytes entirely, +/// making the row 25 bytes and misplacing every field from `0x000D` +/// onward. See [`Self::FLAGS_OFFSET`] and the per-flag mask constants. +/// +/// The two reserved blocks are not modeled as fields: they are written as +/// zero by [`Self::encode`] and ignored by [`Self::decode`], the same +/// treatment [`crate::avtp`]'s TSCF reserved quadlets get. +/// /// See this module's doc comment "Config tables" section for this table's /// row-count source ([`GeneralRegisters::svr_request_stream_cfg`]'s /// `capacity`, cross-referenced against -/// [`GeneralRegisters::svr_req_stream_max`]) and "Config tables provenance -/// note" for why each on/off behavior is its own byte-aligned field. +/// [`GeneralRegisters::svr_req_stream_max`]). #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] // fusa:req REQ-RMAP-015 pub struct RequestStreamConfigEntry { @@ -1234,31 +1275,30 @@ pub struct RequestStreamConfigEntry { /// Required MACsec secure-channel index; `0` means no security / /// uncontrolled port. pub rx_secure_channel_index: u8, - /// `0`/`1`: whether an E2E-CRC failure at one endpoint only drops that - /// bad request, versus latching the whole stream into a fault/safe - /// state until explicitly released. - pub rx_enforce_e2e: u8, - /// `0`/`1`: require strictly increasing sequence numbers before a - /// request is queued for execution at all. - pub rx_enforce_seq: u8, - /// `0`/`1`: drive every endpoint on this stream to safe-state if the - /// monotonic sequence-number check fails. - pub rx_seq_safestate_enable: u8, - /// `0`/`1`: enable this stream's watchdog. - pub rx_wd_enable: u8, - /// `0`/`1`: drive every endpoint on this stream to safe-state on - /// watchdog expiry. - pub rx_wd_safestate_enable: u8, - /// `0`/`1`: drive every endpoint on this stream to safe-state if any of - /// its endpoints' request storage overflows. - pub rx_ovrflw_safestate_enable: u8, - /// `0`/`1`: whether safe-state means forcing every I/O pin to - /// high-impedance, versus running a configured sequencer-based safety - /// request sequence. - pub rx_safety_measure: u8, - /// `0`/`1`: keep sending a repeating notification response while in the - /// safe state. - pub rx_wd_info_enable: u8, + /// Flags bit `0x000D.0`. `false`: an E2E-CRC failure at an endpoint + /// only drops that bad request. `true`: the stream is blocked until + /// released and safe state is entered. + pub rx_enforce_e2e: bool, + /// Flags bit `0x000D.1`. Requests are only filed for execution if the + /// AVTPDU sequence number is increased. + pub rx_enforce_seq: bool, + /// Flags bit `0x000D.2`. Bring all endpoints to safe state if + /// `Sequence_Nr` has no single increment. + pub rx_seq_safestate_enable: bool, + /// Flags bit `0x000D.3`. Enable the watchdog for this stream. + pub rx_wd_enable: bool, + /// Flags bit `0x000D.4`. Bring all endpoints for this stream to safe + /// state if the watchdog triggers. + pub rx_wd_safestate_enable: bool, + /// Flags bit `0x000D.5`. Bring all endpoints for this stream to safe + /// state if the request storage of one endpoint overflows. + pub rx_ovrflw_safestate_enable: bool, + /// Flags bit `0x000D.6`. `false`: as safe state, set all I/O pins to + /// high impedance. `true`: use sequencer-based safety requests. + pub rx_safety_measure: bool, + /// Flags bit `0x000D.7`. Send a repetitive notification when in safe + /// state. + pub rx_wd_info_enable: bool, /// Which sequencer number runs the safety sequence, when /// `rx_safety_measure` selects the sequencer-driven safe-state. pub rx_safestate_sequencer: u8, @@ -1272,14 +1312,70 @@ pub struct RequestStreamConfigEntry { } impl RequestStreamConfigEntry { - /// Encoded wire length in bytes. - pub const ENCODED_LEN: usize = 25; + /// Encoded wire length in bytes: Table 22's row stride, fixed by the + /// next row's `rx_stream_id2` at relative address `0x0018`. + pub const ENCODED_LEN: usize = 24; + + /// Byte offset of Table 22's single bit-addressed flags byte + /// (relative address `0x000D`). + pub const FLAGS_OFFSET: usize = 0x0D; + + /// Mask for `rx_enforce_e2e`, Table 22 bit `0x000D.0`. + pub const FLAG_ENFORCE_E2E: u8 = 1 << 0; + /// Mask for `rx_enforce_seq`, Table 22 bit `0x000D.1`. + pub const FLAG_ENFORCE_SEQ: u8 = 1 << 1; + /// Mask for `rx_seq_safestate_enable`, Table 22 bit `0x000D.2`. + pub const FLAG_SEQ_SAFESTATE_ENABLE: u8 = 1 << 2; + /// Mask for `rx_wd_enable`, Table 22 bit `0x000D.3`. + pub const FLAG_WD_ENABLE: u8 = 1 << 3; + /// Mask for `rx_wd_safestate_enable`, Table 22 bit `0x000D.4`. + pub const FLAG_WD_SAFESTATE_ENABLE: u8 = 1 << 4; + /// Mask for `rx_ovrflw_safestate_enable`, Table 22 bit `0x000D.5`. + pub const FLAG_OVRFLW_SAFESTATE_ENABLE: u8 = 1 << 5; + /// Mask for `rx_safety_measure`, Table 22 bit `0x000D.6`. + pub const FLAG_SAFETY_MEASURE: u8 = 1 << 6; + /// Mask for `rx_wd_info_enable`, Table 22 bit `0x000D.7`. + pub const FLAG_WD_INFO_ENABLE: u8 = 1 << 7; /// The [`RegisterCategory`] this table's rows belong to. pub const CATEGORY: RegisterCategory = RegisterCategory::RcpConfig; - /// Encode as a fixed-length, big-endian byte block, field order above, - /// with no padding between fields. Never panics. + /// Table 22's `0x000D` flags byte, assembled from the eight `bool` + /// fields. Never panics. + // fusa:req REQ-RMAP-016 + pub fn flags_byte(&self) -> u8 { + let mut b = 0u8; + if self.rx_enforce_e2e { + b |= Self::FLAG_ENFORCE_E2E; + } + if self.rx_enforce_seq { + b |= Self::FLAG_ENFORCE_SEQ; + } + if self.rx_seq_safestate_enable { + b |= Self::FLAG_SEQ_SAFESTATE_ENABLE; + } + if self.rx_wd_enable { + b |= Self::FLAG_WD_ENABLE; + } + if self.rx_wd_safestate_enable { + b |= Self::FLAG_WD_SAFESTATE_ENABLE; + } + if self.rx_ovrflw_safestate_enable { + b |= Self::FLAG_OVRFLW_SAFESTATE_ENABLE; + } + if self.rx_safety_measure { + b |= Self::FLAG_SAFETY_MEASURE; + } + if self.rx_wd_info_enable { + b |= Self::FLAG_WD_INFO_ENABLE; + } + b + } + + /// Encode as Table 22's fixed-length, big-endian 24-byte row: each + /// field at its tabulated relative address, the eight `1 bit` flags + /// packed into the `0x000D` byte, and both reserved blocks written as + /// zero. Never panics. // fusa:req REQ-RMAP-016 pub fn encode(&self) -> [u8; Self::ENCODED_LEN] { let mut buf = [0u8; Self::ENCODED_LEN]; @@ -1293,29 +1389,24 @@ impl RequestStreamConfigEntry { }}; } - put!(self.rx_stream_id); - put!(self.rx_stream_max_request_size); - put!(self.rx_wd_timeout_interval); - put!(self.rx_secure_channel_index); - put!(self.rx_enforce_e2e); - put!(self.rx_enforce_seq); - put!(self.rx_seq_safestate_enable); - put!(self.rx_wd_enable); - put!(self.rx_wd_safestate_enable); - put!(self.rx_ovrflw_safestate_enable); - put!(self.rx_safety_measure); - put!(self.rx_wd_info_enable); - put!(self.rx_safestate_sequencer); - put!(self.rx_safe_sequencer_state); - put!(self.rx_ack_stream_index); - put!(self.rx_resp_stream_index); - - debug_assert_eq!(off, Self::ENCODED_LEN); + put!(self.rx_stream_id); // 0x0000 + put!(self.rx_stream_max_request_size); // 0x0008 + put!(self.rx_wd_timeout_interval); // 0x000A + put!(self.rx_secure_channel_index); // 0x000C + put!(self.flags_byte()); // 0x000D + put!(self.rx_safestate_sequencer); // 0x000E + put!(self.rx_safe_sequencer_state); // 0x000F + put!(self.rx_ack_stream_index); // 0x0010 + put!(self.rx_resp_stream_index); // 0x0011 + // 0x0012 Reserved (16 bit) and 0x0014 Reserved (32 bit) stay zeroed. + debug_assert_eq!(off, Self::FLAGS_OFFSET + 5); buf } - /// Decode a fixed-length, big-endian byte block produced by - /// [`Self::encode`]. + /// Decode Table 22's fixed-length, big-endian 24-byte row. + /// + /// Reserved bytes `0x0012`-`0x0017` are ignored regardless of content, + /// so a row round-trips only its eleven specified fields. /// /// Returns `Err(RcpError::ShortFrame)` if `bytes` is shorter than /// [`Self::ENCODED_LEN`]. Trailing bytes beyond `ENCODED_LEN` are @@ -1358,37 +1449,32 @@ impl RequestStreamConfigEntry { }}; } - let rx_stream_id = take_u64!(); - let rx_stream_max_request_size = take_u16!(); - let rx_wd_timeout_interval = take_u16!(); - let rx_secure_channel_index = take_u8!(); - let rx_enforce_e2e = take_u8!(); - let rx_enforce_seq = take_u8!(); - let rx_seq_safestate_enable = take_u8!(); - let rx_wd_enable = take_u8!(); - let rx_wd_safestate_enable = take_u8!(); - let rx_ovrflw_safestate_enable = take_u8!(); - let rx_safety_measure = take_u8!(); - let rx_wd_info_enable = take_u8!(); - let rx_safestate_sequencer = take_u8!(); - let rx_safe_sequencer_state = take_u8!(); - let rx_ack_stream_index = take_u8!(); - let rx_resp_stream_index = take_u8!(); + let rx_stream_id = take_u64!(); // 0x0000 + let rx_stream_max_request_size = take_u16!(); // 0x0008 + let rx_wd_timeout_interval = take_u16!(); // 0x000A + let rx_secure_channel_index = take_u8!(); // 0x000C + debug_assert_eq!(off, Self::FLAGS_OFFSET); + let flags = take_u8!(); // 0x000D + let rx_safestate_sequencer = take_u8!(); // 0x000E + let rx_safe_sequencer_state = take_u8!(); // 0x000F + let rx_ack_stream_index = take_u8!(); // 0x0010 + let rx_resp_stream_index = take_u8!(); // 0x0011 + // 0x0012 / 0x0014 reserved: ignored. + debug_assert_eq!(off, Self::FLAGS_OFFSET + 5); - debug_assert_eq!(off, Self::ENCODED_LEN); Ok(Self { rx_stream_id, rx_stream_max_request_size, rx_wd_timeout_interval, rx_secure_channel_index, - rx_enforce_e2e, - rx_enforce_seq, - rx_seq_safestate_enable, - rx_wd_enable, - rx_wd_safestate_enable, - rx_ovrflw_safestate_enable, - rx_safety_measure, - rx_wd_info_enable, + rx_enforce_e2e: flags & Self::FLAG_ENFORCE_E2E != 0, + rx_enforce_seq: flags & Self::FLAG_ENFORCE_SEQ != 0, + rx_seq_safestate_enable: flags & Self::FLAG_SEQ_SAFESTATE_ENABLE != 0, + rx_wd_enable: flags & Self::FLAG_WD_ENABLE != 0, + rx_wd_safestate_enable: flags & Self::FLAG_WD_SAFESTATE_ENABLE != 0, + rx_ovrflw_safestate_enable: flags & Self::FLAG_OVRFLW_SAFESTATE_ENABLE != 0, + rx_safety_measure: flags & Self::FLAG_SAFETY_MEASURE != 0, + rx_wd_info_enable: flags & Self::FLAG_WD_INFO_ENABLE != 0, rx_safestate_sequencer, rx_safe_sequencer_state, rx_ack_stream_index, @@ -1411,32 +1497,50 @@ impl ConfigTableRow for RequestStreamConfigEntry { // ── EpByteBusIdMapEntry (§3.9) ─────────────────────────────────────────────── -/// One row of the EP-ID/`byte_bus_id` mapping table (`§3.9`): maps a -/// `(request_stream_index, byte_bus_id)` pair to a target endpoint number. +/// One row of the EP-ID/`byte_bus_id` mapping table (TC18 v0.5.1_RC +/// §12.7.8 "Endpoint ID and communication configuration", Table 23, +/// "EP_ID_config", p.59): maps a `(request_stream_index, byte_bus_id)` pair +/// to a target endpoint number. +/// +/// # Wire layout (Table 23) +/// +/// ```text +/// 0x0000 1_Request_Stream_Index 8 bit +/// 0x0001 1_EP_Nr 8 bit "EP addressed by 1_BBID" +/// 0x0002 1_BBID 16 bit "Byte_bus_id [11bit]" +/// 0x0004 (next row: 2_Request_Stream_Index) +/// ``` +/// +/// The row is 4 bytes, with `EP_Nr` **before** `BBID`. Releases before +/// v5.0.0 emitted the two transposed. /// /// See this module's doc comment "Config tables" section for this table's /// row-count source ([`GeneralRegisters::svr_ep_bytebus_id_map`]'s /// `capacity`) and, importantly, for why this type and this module add /// **no** row-ordering validation — that is the writing client's -/// responsibility, per `ROADMAP.md`'s own parenthetical for this checklist -/// item. +/// responsibility, per Table 23's own note ("The parameters +/// Request_Stream_Index and BBID shall occur in ascending order. This has +/// to be ensured by the instance that is sending the configuration to this +/// table"). #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] // fusa:req REQ-RMAP-018 pub struct EpByteBusIdMapEntry { - /// Which request stream this mapping applies to. + /// Table 23 `0x0000`, "Index of request stream ID". pub map_stream_index: u8, /// The `byte_bus_id` value, scoped to `map_stream_index`, that this row - /// maps. Widened to `u16` to match + /// maps (Table 23 `0x0002`, a 16-bit field carrying an 11-bit + /// `Byte_bus_id`). The `u16` width is the table's own, and matches /// [`crate::acf::ByteMessageInfo::byte_bus_id`]'s existing field width - /// for the same 11-bit wire concept. + /// for the same wire concept. pub map_byte_bus_id: u16, /// The endpoint slot number this `(stream, byte_bus_id)` pair resolves - /// to. + /// to (Table 23 `0x0001`, "EP addressed by 1_BBID"). pub map_ep_nr: u8, } impl EpByteBusIdMapEntry { - /// Encoded wire length in bytes. + /// Encoded wire length in bytes: Table 23's row stride, fixed by the + /// next row's `2_Request_Stream_Index` at relative address `0x0004`. pub const ENCODED_LEN: usize = 4; /// The [`RegisterCategory`] this table's rows belong to. @@ -1456,18 +1560,22 @@ impl EpByteBusIdMapEntry { self.map_stream_index == Self::END_OF_TABLE_STREAM_INDEX } - /// Encode as `[map_stream_index, map_byte_bus_id (big-endian), - /// map_ep_nr]`. Never panics. + /// Encode as Table 23's 4-byte row: `[Request_Stream_Index (0x0000), + /// EP_Nr (0x0001), BBID (0x0002, big-endian 16 bit)]`. + /// + /// Releases before v5.0.0 emitted `[stream_index, BBID_hi, BBID_lo, + /// EP_Nr]` — `EP_Nr` and `BBID` transposed against the table's own + /// relative addresses. // fusa:req REQ-RMAP-019 pub fn encode(&self) -> [u8; Self::ENCODED_LEN] { let mut buf = [0u8; Self::ENCODED_LEN]; - buf[0] = self.map_stream_index; - buf[1..3].copy_from_slice(&self.map_byte_bus_id.to_be_bytes()); - buf[3] = self.map_ep_nr; + buf[0] = self.map_stream_index; // 0x0000 + buf[1] = self.map_ep_nr; // 0x0001 + buf[2..4].copy_from_slice(&self.map_byte_bus_id.to_be_bytes()); // 0x0002 buf } - /// Decode from the front of `bytes`. + /// Decode Table 23's 4-byte row from the front of `bytes`. /// /// Returns `Err(RcpError::ShortFrame)` if `bytes` is shorter than /// [`Self::ENCODED_LEN`]. Trailing bytes beyond `ENCODED_LEN` are @@ -1479,8 +1587,8 @@ impl EpByteBusIdMapEntry { } Ok(Self { map_stream_index: bytes[0], - map_byte_bus_id: u16::from_be_bytes([bytes[1], bytes[2]]), - map_ep_nr: bytes[3], + map_ep_nr: bytes[1], + map_byte_bus_id: u16::from_be_bytes([bytes[2], bytes[3]]), }) } } @@ -1580,8 +1688,25 @@ impl ConfigTableRow for ResponseStreamConfigEntry { // ── SequencerStateEntry (§3.11) ────────────────────────────────────────────── -/// One row of the sequencer-state register block (`§3.11`): a single -/// persistent 8-bit state register for one sequencer. +/// One row of the `SEQUENCER_config` register block (TC18 v0.5.1_RC +/// §12.7.10 "Sequencer state registers", Table 25, p.61): one sequencer's +/// persistent state, plus the request stream authorized to reach it. +/// +/// # Wire layout (Table 25) +/// +/// ```text +/// 0x0000 Seq_state 8 bit R/W default 1 +/// 0x0001 Request_stream_index 8 bit R/W* +/// 0x0002 (next row: Seq_2's Seq_state) +/// ``` +/// +/// The row is **2 bytes**, not 1. Releases before v5.0.0 modeled only +/// `Seq_state`, dropping `Request_stream_index` — which §12.7.10 makes the +/// access-control field for the sequencer ("Each sequencer is dedicated to +/// a specific RC Client and its bound endpoints"; the field itself "refers +/// the Client Nr allowed to access this sequencer"). A row read or written +/// through the old 1-byte form both lost that binding and desynchronized +/// every subsequent row in the table. /// /// See this module's doc comment "Config tables" section for this table's /// row-count source ([`GeneralRegisters::svr_sequencers_max`] — the one @@ -1591,49 +1716,65 @@ impl ConfigTableRow for ResponseStreamConfigEntry { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] // fusa:req REQ-RMAP-024 pub struct SequencerStateEntry { - /// This sequencer's current persistent state. + /// Table 25 `0x0000`, "Recent state if sequencer no N; if manually set + /// to 0 then disabled". §12.7.10: "Upon power-on reset all sequencer + /// state values are set to '1'." pub seq_state: u8, + /// Table 25 `0x0001`, "refers the Client Nr allowed to access this + /// sequencer" — the request-stream index bound to this sequencer. + /// Table 25's Default column is blank for this field, so + /// [`Self::power_on_default`] leaves it `0` rather than inventing a + /// documented reset value. + pub request_stream_index: u8, } impl SequencerStateEntry { - /// Encoded wire length in bytes. - pub const ENCODED_LEN: usize = 1; + /// Encoded wire length in bytes: Table 25's row stride, fixed by + /// `Seq_2`'s `Seq_state` at relative address `0x0002`. + pub const ENCODED_LEN: usize = 2; /// The [`RegisterCategory`] this table's rows belong to. pub const CATEGORY: RegisterCategory = RegisterCategory::RcpConfig; - /// The documented power-on default state for a freshly reset - /// sequencer. + /// The documented power-on state for a freshly reset sequencer + /// (§12.7.10: all sequencer state values are set to "1"). + /// `request_stream_index` is left `0` — Table 25 documents no default + /// for it. // fusa:req REQ-RMAP-024 pub fn power_on_default() -> Self { - Self { seq_state: 1 } + Self { + seq_state: 1, + request_stream_index: 0, + } } - /// Encode as a single state byte. Never panics. + /// Encode as Table 25's 2-byte row, `[Seq_state, + /// Request_stream_index]`. Never panics. // fusa:req REQ-RMAP-025 pub fn encode(&self) -> [u8; Self::ENCODED_LEN] { - [self.seq_state] + [self.seq_state, self.request_stream_index] } - /// Decode from the front of `bytes`. + /// Decode Table 25's 2-byte row from the front of `bytes`. /// - /// Returns `Err(RcpError::ShortFrame)` if `bytes` is empty. Trailing - /// bytes beyond [`Self::ENCODED_LEN`] are ignored. Never panics for any - /// input. + /// Returns `Err(RcpError::ShortFrame)` if `bytes` is shorter than + /// [`Self::ENCODED_LEN`]. Trailing bytes beyond `ENCODED_LEN` are + /// ignored. Never panics for any input. // fusa:req REQ-RMAP-026 pub fn decode(bytes: &[u8]) -> Result { - if bytes.is_empty() { + if bytes.len() < Self::ENCODED_LEN { return Err(RcpError::ShortFrame); } Ok(Self { seq_state: bytes[0], + request_stream_index: bytes[1], }) } } impl Default for SequencerStateEntry { - /// Defaults to the documented power-on state (`1`), not `0` — see - /// [`Self::power_on_default`]. + /// Defaults to the documented power-on state (`seq_state == 1`), not + /// all-zero — see [`Self::power_on_default`]. fn default() -> Self { Self::power_on_default() } @@ -2415,14 +2556,14 @@ mod tests { rx_stream_max_request_size: 128, rx_wd_timeout_interval: 1000, rx_secure_channel_index: 1, - rx_enforce_e2e: 1, - rx_enforce_seq: 0, - rx_seq_safestate_enable: 1, - rx_wd_enable: 1, - rx_wd_safestate_enable: 0, - rx_ovrflw_safestate_enable: 1, - rx_safety_measure: 1, - rx_wd_info_enable: 0, + rx_enforce_e2e: true, + rx_enforce_seq: false, + rx_seq_safestate_enable: true, + rx_wd_enable: true, + rx_wd_safestate_enable: false, + rx_ovrflw_safestate_enable: true, + rx_safety_measure: true, + rx_wd_info_enable: false, rx_safestate_sequencer: 2, rx_safe_sequencer_state: 5, rx_ack_stream_index: 1, @@ -2433,14 +2574,14 @@ mod tests { rx_stream_max_request_size: u16::MAX, rx_wd_timeout_interval: u16::MAX, rx_secure_channel_index: u8::MAX, - rx_enforce_e2e: u8::MAX, - rx_enforce_seq: u8::MAX, - rx_seq_safestate_enable: u8::MAX, - rx_wd_enable: u8::MAX, - rx_wd_safestate_enable: u8::MAX, - rx_ovrflw_safestate_enable: u8::MAX, - rx_safety_measure: u8::MAX, - rx_wd_info_enable: u8::MAX, + rx_enforce_e2e: true, + rx_enforce_seq: true, + rx_seq_safestate_enable: true, + rx_wd_enable: true, + rx_wd_safestate_enable: true, + rx_ovrflw_safestate_enable: true, + rx_safety_measure: true, + rx_wd_info_enable: true, rx_safestate_sequencer: u8::MAX, rx_safe_sequencer_state: u8::MAX, rx_ack_stream_index: u8::MAX, @@ -2489,6 +2630,102 @@ mod tests { assert_eq!(RequestStreamConfigEntry::decode(&bytes), Ok(e)); } + /// TC18 v0.5.1_RC §12.7.7 Table 22 tabulates `rx_stream_id2` — the + /// *next* row's first field — at relative address `0x0018`, which fixes + /// the row stride at 24 bytes. Releases before v5.0.0 used 25. + #[test] + // fusa:test REQ-RMAP-016 + fn request_stream_config_entry_row_stride_is_table_22_next_row_address() { + assert_eq!(RequestStreamConfigEntry::ENCODED_LEN, 0x18); + assert_eq!(RequestStreamConfigEntry::FLAGS_OFFSET, 0x0D); + } + + /// Byte-for-byte against Table 22's own "Relative address" and "Type" + /// columns, laid out by hand from the table rather than from this + /// crate's encoder: + /// + /// ```text + /// 0x0000 rx_stream_id 64b 0x0011223344556677 + /// 0x0008 rx_stream_max_request_size 16b 128 = 0x0080 + /// 0x000A rx_wd_timeout_intervall 16b 1000 = 0x03E8 + /// 0x000C rx_secure_channel_index 8b 1 = 0x01 + /// 0x000D flags: .0,.2,.3,.5,.6 set; .1,.4,.7 clear + /// 0b0110_1101 = 0x6D + /// 0x000E rx_safestate_seqencer 8b 2 = 0x02 + /// 0x000F rx_safe_sequencer_state 8b 5 = 0x05 + /// 0x0010 rx_ack_stream_index 8b 1 = 0x01 + /// 0x0011 rx_resp_stream_index 8b 1 = 0x01 + /// 0x0012 Reserved 16b zero + /// 0x0014 Reserved 32b zero + /// ``` + #[test] + // fusa:test REQ-RMAP-016 + fn request_stream_config_entry_matches_table_22_literal_bytes() { + let expected: [u8; 24] = [ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, // 0x0000 + 0x00, 0x80, // 0x0008 + 0x03, 0xE8, // 0x000A + 0x01, // 0x000C + 0x6D, // 0x000D flags + 0x02, // 0x000E + 0x05, // 0x000F + 0x01, // 0x0010 + 0x01, // 0x0011 + 0x00, 0x00, // 0x0012 Reserved 16 bit + 0x00, 0x00, 0x00, 0x00, // 0x0014 Reserved 32 bit + ]; + let e = sample_request_stream_config_entries()[1]; + assert_eq!(e.encode(), expected); + assert_eq!(RequestStreamConfigEntry::decode(&expected), Ok(e)); + } + + /// Table 22 addresses the eight `1 bit` fields `0x000D.0` through + /// `0x000D.7`, in the listed order, all sharing one byte. Setting any + /// single flag must therefore light exactly one bit of `0x000D` and + /// leave all 23 other bytes zero. Releases before v5.0.0 gave each flag + /// its own byte, which this test would have caught. + #[test] + // fusa:test REQ-RMAP-016 + fn request_stream_config_entry_flag_bit_positions_match_table_22() { + /// Sets one Table 22 flag on an otherwise-default row. + type SetFlag = fn(&mut RequestStreamConfigEntry); + // (setter, Table 22 bit number) + let cases: [(SetFlag, u32); 8] = [ + (|e| e.rx_enforce_e2e = true, 0), + (|e| e.rx_enforce_seq = true, 1), + (|e| e.rx_seq_safestate_enable = true, 2), + (|e| e.rx_wd_enable = true, 3), + (|e| e.rx_wd_safestate_enable = true, 4), + (|e| e.rx_ovrflw_safestate_enable = true, 5), + (|e| e.rx_safety_measure = true, 6), + (|e| e.rx_wd_info_enable = true, 7), + ]; + for (set_flag, bit) in cases { + let mut e = RequestStreamConfigEntry::default(); + set_flag(&mut e); + let encoded = e.encode(); + let mut expected = [0u8; RequestStreamConfigEntry::ENCODED_LEN]; + expected[RequestStreamConfigEntry::FLAGS_OFFSET] = 1u8 << bit; + assert_eq!(encoded, expected, "flag at Table 22 bit 0x000D.{bit}"); + assert_eq!(RequestStreamConfigEntry::decode(&encoded), Ok(e)); + } + // All eight together fill the byte exactly, and nothing else. + let all = sample_request_stream_config_entries()[2]; + assert_eq!(all.flags_byte(), 0xFF); + } + + /// Reserved bytes `0x0012`-`0x0017` carry no modeled field: decode must + /// ignore whatever they hold, and encode must emit zero. + #[test] + // fusa:test REQ-RMAP-017 + fn request_stream_config_entry_reserved_bytes_are_ignored_and_zeroed() { + let e = sample_request_stream_config_entries()[1]; + let mut dirty = e.encode(); + dirty[0x12..0x18].copy_from_slice(&[0xFF; 6]); + assert_eq!(RequestStreamConfigEntry::decode(&dirty), Ok(e)); + assert_eq!(&e.encode()[0x12..0x18], &[0u8; 6]); + } + #[test] // fusa:test REQ-RMAP-017 fn request_stream_config_entry_decode_rejects_short_input() { @@ -2573,6 +2810,39 @@ mod tests { assert_eq!(EpByteBusIdMapEntry::decode(&bytes), Ok(e)); } + /// Byte-for-byte against TC18 v0.5.1_RC §12.7.8 Table 23's own + /// "Relative address" column, laid out by hand from the table: + /// + /// ```text + /// 0x0000 1_Request_Stream_Index 8b 3 = 0x03 + /// 0x0001 1_EP_Nr 8b 4 = 0x04 + /// 0x0002 1_BBID 16b 0x0555 = 0x05 0x55 + /// ``` + /// + /// Note `EP_Nr` precedes `BBID`. Releases before v5.0.0 emitted + /// `[0x03, 0x05, 0x55, 0x04]` — the two transposed. The literal below + /// uses distinct values in every byte so a transposition cannot pass. + #[test] + // fusa:test REQ-RMAP-019 + fn ep_bytebus_id_map_entry_matches_table_23_literal_bytes() { + let e = EpByteBusIdMapEntry { + map_stream_index: 3, + map_byte_bus_id: 0x0555, + map_ep_nr: 4, + }; + let expected: [u8; 4] = [0x03, 0x04, 0x05, 0x55]; + assert_eq!(e.encode(), expected); + assert_eq!(EpByteBusIdMapEntry::decode(&expected), Ok(e)); + } + + /// Table 23 tabulates `2_Request_Stream_Index` — the next row's first + /// field — at relative address `0x0004`, fixing the row stride. + #[test] + // fusa:test REQ-RMAP-019 + fn ep_bytebus_id_map_entry_row_stride_is_table_23_next_row_address() { + assert_eq!(EpByteBusIdMapEntry::ENCODED_LEN, 0x04); + } + #[test] // fusa:test REQ-RMAP-020 fn ep_bytebus_id_map_entry_decode_rejects_short_input() { @@ -2687,7 +2957,12 @@ mod tests { #[test] // fusa:test REQ-RMAP-024 fn sequencer_state_entry_power_on_default_is_state_one() { - assert_eq!(SequencerStateEntry::power_on_default().seq_state, 1); + // TC18 §12.7.10: "Upon power-on reset all sequencer state values + // are set to '1'." Table 25's Default column is blank for + // Request_stream_index, so it stays 0. + let d = SequencerStateEntry::power_on_default(); + assert_eq!(d.seq_state, 1); + assert_eq!(d.request_stream_index, 0); } #[test] @@ -2699,21 +2974,81 @@ mod tests { ); } + /// TC18 §12.7.10 Table 25 gives each sequencer **two** 8-bit fields, + /// and tabulates `Seq_2`'s `Seq_state` at relative address `0x0002`, + /// fixing the row stride at 2 bytes. Releases before v5.0.0 modeled + /// only `Seq_state` with a 1-byte stride, which both dropped the + /// access-control field and misaligned every row after the first. + #[test] + // fusa:test REQ-RMAP-024 + fn sequencer_state_entry_row_stride_is_table_25_next_row_address() { + assert_eq!(SequencerStateEntry::ENCODED_LEN, 0x02); + } + #[test] // fusa:test REQ-RMAP-025 fn sequencer_state_entry_encode_decode_round_trips() { for seq_state in [0u8, 1, 2, 128, u8::MAX] { - let e = SequencerStateEntry { seq_state }; - let encoded = e.encode(); - assert_eq!(encoded.len(), SequencerStateEntry::ENCODED_LEN); - assert_eq!(SequencerStateEntry::decode(&encoded), Ok(e)); + for request_stream_index in [0u8, 1, 200, u8::MAX] { + let e = SequencerStateEntry { + seq_state, + request_stream_index, + }; + let encoded = e.encode(); + assert_eq!(encoded.len(), SequencerStateEntry::ENCODED_LEN); + assert_eq!(SequencerStateEntry::decode(&encoded), Ok(e)); + } } } + /// Byte-for-byte against Table 25's own "Relative address" column, laid + /// out by hand from the table: + /// + /// ```text + /// 0x0000 Seq_state 8b 1 = 0x01 + /// 0x0001 Request_stream_index 8b 4 = 0x04 + /// ``` + #[test] + // fusa:test REQ-RMAP-025 + fn sequencer_state_entry_matches_table_25_literal_bytes() { + let e = SequencerStateEntry { + seq_state: 1, + request_stream_index: 4, + }; + let expected: [u8; 2] = [0x01, 0x04]; + assert_eq!(e.encode(), expected); + assert_eq!(SequencerStateEntry::decode(&expected), Ok(e)); + } + + /// A two-sequencer table read back through `decode_rows` must resolve + /// `Seq_2`'s state from offset `0x0002`, not `0x0001`. + #[test] + // fusa:test REQ-RMAP-025 + fn sequencer_state_table_rows_land_on_table_25_addresses() { + // Seq_1: state 1, client 9. Seq_2: state 3, client 2. + let raw: [u8; 4] = [0x01, 0x09, 0x03, 0x02]; + assert_eq!( + decode_rows::(&raw), + Ok(vec![ + SequencerStateEntry { + seq_state: 1, + request_stream_index: 9, + }, + SequencerStateEntry { + seq_state: 3, + request_stream_index: 2, + }, + ]) + ); + } + #[test] // fusa:test REQ-RMAP-025 fn sequencer_state_entry_decode_ignores_trailing_bytes() { - let e = SequencerStateEntry { seq_state: 7 }; + let e = SequencerStateEntry { + seq_state: 7, + request_stream_index: 3, + }; let mut bytes = e.encode().to_vec(); bytes.extend_from_slice(&[0xFF, 0xFF]); assert_eq!(SequencerStateEntry::decode(&bytes), Ok(e)); @@ -2721,8 +3056,14 @@ mod tests { #[test] // fusa:test REQ-RMAP-026 - fn sequencer_state_entry_decode_rejects_empty_input() { - assert_eq!(SequencerStateEntry::decode(&[]), Err(RcpError::ShortFrame)); + fn sequencer_state_entry_decode_rejects_short_input() { + for len in 0..SequencerStateEntry::ENCODED_LEN { + let bytes = vec![0u8; len]; + assert_eq!( + SequencerStateEntry::decode(&bytes), + Err(RcpError::ShortFrame) + ); + } } #[test] @@ -2781,9 +3122,12 @@ mod tests { #[test] // fusa:test REQ-RMAP-027 fn encode_rows_decode_rows_round_trip_sequencer_state_entries() { - let rows: Vec = [0u8, 1, 255] + let rows: Vec = [(0u8, 0u8), (1, 7), (255, 255)] .into_iter() - .map(|seq_state| SequencerStateEntry { seq_state }) + .map(|(seq_state, request_stream_index)| SequencerStateEntry { + seq_state, + request_stream_index, + }) .collect(); let encoded = encode_rows(&rows); assert_eq!(encoded.len(), rows.len() * SequencerStateEntry::ENCODED_LEN); diff --git a/src/wakeup.rs b/src/wakeup.rs index 60581bf..6e4fcc8 100644 --- a/src/wakeup.rs +++ b/src/wakeup.rs @@ -635,6 +635,11 @@ mod tests { ), Ok(PowerMode::StandBy) ); + // TC18 §12.4 Figure 17's other "Go to ..." edge. + assert_eq!( + request_sleep_via_sleep_cmd(SleepCmdRequest, PowerMode::Normal, PowerMode::Sleep, gate), + Ok(PowerMode::Sleep) + ); } #[test] @@ -644,8 +649,24 @@ mod tests { all_endpoints_idle: true, no_pending_response: true, }; + // Figure 17 draws no edge between the two low-power modes: both + // are entered from `Normal` only. assert_eq!( - request_sleep_via_sleep_cmd(SleepCmdRequest, PowerMode::Normal, PowerMode::Sleep, gate), + request_sleep_via_sleep_cmd( + SleepCmdRequest, + PowerMode::StandBy, + PowerMode::Sleep, + gate + ), + Err(RcpError::RequestRejected) + ); + assert_eq!( + request_sleep_via_sleep_cmd( + SleepCmdRequest, + PowerMode::Normal, + PowerMode::Unpowered, + gate + ), Err(RcpError::RequestRejected) ); }