Skip to content

fix: v5.0.0 — TC18-conformant power-mode model + register-map config tables (BREAKING) - #135

Merged
SoundMatt merged 1 commit into
mainfrom
fix/tc18-powerstate-and-regmap-tables
Aug 1, 2026
Merged

fix: v5.0.0 — TC18-conformant power-mode model + register-map config tables (BREAKING)#135
SoundMatt merged 1 commit into
mainfrom
fix/tc18-powerstate-and-regmap-tables

Conversation

@SoundMatt

Copy link
Copy Markdown
Owner

Four independently-confirmed conformance findings against the OPEN Alliance TC18 Remote Control Specification v0.5.1_RC. Each was verified against primary source before any code changed — and where the question was one of bit packing or table structure, against a 300/600 dpi render of the relevant PDF page rather than extracted text, since text extraction preserves field order but not bit widths.

v5.0.0 (MAJOR) per docs/SEMVER.md: three of the four are wire-format changes, and that doc makes wire-format correctness a MAJOR concern even when the change is a fix.

The three 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 yet), so interop urgency is lower than v3.0.0/v4.0.0. P01/P02 are a real behavioural bug in code callable today.


rust-RCP-P01 — power-state cold/hot-start mapping was inverted (HIGH, ASIL-B)

Source: §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)." Corroborated by §12.4 Figure 17's own arrow labels (see P02).

before after (TC18)
try_cold_start Unpowered -> Normal only Unpowered -> Normal and Sleep -> Normal
try_hot_start Sleep -> Normal, behind WakeUp handshake StandBy -> Normal, behind WakeUp handshake

Both origins were wrong, and the handshake was attached to the wrong path — §12.4.1 introduces it under "Hot-start-up procedure". Net effect on a caller wiring this up: every wake-from-sleep was rejected until a handshake the spec does not require there had completed, and every wake-from-standby was rejected outright, there being no path admitting that origin at all.

rust-RCP-P02 — undefined StandBy <-> Sleep transition, missing Normal -> Sleep

Source: §12.4 Figure 17 "power and operation modes", p.46 — a labelled state diagram with exactly five edges:

Edge Figure 17 label
Unpowered -> Normal "Cold start"
Sleep -> Normal "Cold start"
StandBy -> Normal "Hot start"
Normal -> StandBy "Go to StandBy"
Normal -> Sleep "Go to Sleep"

is_power_mode_transition_defined accepted StandBy <-> Sleep — an edge the figure does not draw at all; it places Normal/StandBy in a "Powered" box and Sleep in a separate "Only part of PHY powered" box, both low-power modes reached from and returning to Normal only — and omitted Normal -> Sleep, which it does draw. Now exactly the two "Go to ..." edges, with the two wake-up edges owned by the start-up functions.

rust-RCP-P03 — RequestStreamConfigEntry wrong layout (wire)

Source: §12.7.7 Table 22 "Request stream configuration", pp.57-58.

Table 22 addresses eight per-stream flags as 0x000D.0 through 0x000D.7 — the only fields in the table given a .bit suffix rather than a whole-byte address — so all eight share one byte. It then closes each row with a 16-bit reserved word at 0x0012 and a 32-bit reserved block at 0x0014, with 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 25 → 24
  • the eight flag fields u8bool (a u8 cannot round-trip losslessly through one bit)
  • new FLAGS_OFFSET, eight FLAG_* masks, and a flags_byte() accessor

rust-RCP-P04 — EpByteBusIdMapEntry transposed EP_Nr/BBID (wire)

Source: §12.7.8 Table 23 "EP_ID_config", p.59 — Request_Stream_Index at 0x0000, EP_Nr at 0x0001, BBID at 0x0002 (16-bit).

Encoder emitted [stream_index, BBID_hi, BBID_lo, EP_Nr]. Row length was coincidentally right; the middle three bytes were not.

rust-RCP-P05 — SequencerStateEntry modeled 1 of 2 fields (wire)

Source: §12.7.10 Table 25 "SEQUENCER_config", p.61 — each sequencer is Seq_state at 0x0000 and Request_stream_index at 0x0001, with Seq_2 at 0x0002 fixing a 2-byte stride.

The missing field is the sequencer's access-control binding: §12.7.10 states "Each sequencer is dedicated to a specific RC Client and its bound endpoints", and the field itself "refers the Client Nr allowed to access this sequencer". With ENCODED_LEN = 1, a multi-sequencer table read through decode_rows both lost every sequencer's client binding and misaligned every row after the first.


Root causes addressed, not just symptoms

False-scarcity provenance notes. src/regmap.rs's "Config tables provenance note" asserted the specification recorded these tables' field names and purpose in prose only, with "no explicit per-field bit-width or byte-offset table" and "no textual basis for a specific bit-position assignment". That is false — Tables 22, 23 and 25 each carry explicit "Relative address" and "Type" columns, and Table 22 additionally gives 0x000D.0-0x000D.7. Three of the five row types were laid out wrongly on the strength of that claim. The note is corrected, and each row type's doc comment now reproduces its table's address/width columns with a section and page citation. src/powerstate.rs had the equivalent problem: it derived its transition set by reading the four mode names as a depth ordering, which the spec never states, and that inference is what produced P01 and P02 both.

Tautological tests. Every affected encoder's tests asserted only decode(encode(x)) == x. A round trip through one's own encoder cannot detect a wrong layout — which is why all five findings survived. Each fix adds literal expected-byte vectors laid out by hand from the specification's address columns, not copied back from encoder output, with values chosen so a transposition or mis-sized row cannot pass:

  • Table 22: full 24-byte literal, plus a per-flag test asserting each flag alone lights exactly one bit of 0x000D and leaves all 23 other bytes zero (this alone would have caught P03), plus a reserved-bytes-ignored test
  • Table 23: [0x03, 0x04, 0x05, 0x55] — distinct value in every byte, so a transposition cannot pass
  • Table 25: 2-byte literal, plus a decode_rows test asserting Seq_2's state resolves at offset 0x0002
  • Figure 17: an exhaustive 16-pair cross-product pinning the defined set, and a test asserting the cold- and hot-start origin sets are disjoint and cover exactly the three inbound edges

Requirements. .fusa-reqs.json REQ-PWR-002, REQ-PWRSTART-001/002, REQ-RMAP-015/016/017/018/019/024/025/026 all baked the wrong behavior in as correct. Every one is rewritten against the real spec source, with section, table and page cited inline.


Verification

  • cargo test --all-features1092 pass, 0 fail (1060 lib + 32 bin)
  • cargo build --release — clean
  • cargo clippy --all-targets --all-features -- -D warnings — clean
  • cargo fmt --check — clean
  • scripts/fusa-gap-check.sh — 564/564 requirements traced, no gaps
  • scripts/hara_asil_check.py — 10 hazards match ISO 26262-3:2018 Table 4

docs/PUBLIC_API.txt was updated by hand (no nightly toolchain available locally, so cargo public-api could not regenerate it); please watch the api-stability job in particular. Public surface delta: RequestStreamConfigEntry's 8 flag fields u8 -> bool, encode -> [u8; 24], 10 new consts + flags_byte(); SequencerStateEntry gains request_stream_index, encode -> [u8; 2].

Left for a follow-up (deliberately out of scope)

  • ResponseStreamConfigEntry's layout has not been checked against §12.7.9 in this pass and remains this crate's own inference — flagged as such in the module doc.
  • RequestStreamConfigEntry::default() is still all-zero, where Table 22 documents a default of 1 for rx_resp_stream_index. Noted in the CHANGELOG rather than changed, as it is a distinct finding from the four here.

…tables (BREAKING)

Four independently-confirmed findings, each verified against the OPEN
Alliance TC18 v0.5.1_RC specification's own tables and figures — rendered
at 300/600 dpi where bit packing or table structure was in question, since
text extraction preserves field order but not bit widths.

rust-RCP-P01 (BREAKING, ASIL-B): cold-/hot-start mapping was inverted.
§12.4.1 (p.46): "a cold start (after power-on or wake-up from sleep) and a
hot start (=wake-up from StandBy)". try_cold_start accepted only
Unpowered->Normal, omitting the Sleep->Normal cold start; try_hot_start
claimed Sleep->Normal 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.

rust-RCP-P02 (BREAKING): is_power_mode_transition_defined accepted
StandBy<->Sleep, an edge Figure 17 does not draw, and omitted Normal->Sleep
("Go to Sleep"), which it does. Now exactly Figure 17's two "Go to" edges.

rust-RCP-P03 (BREAKING, wire): RequestStreamConfigEntry used the wrong row
layout. §12.7.7 Table 22 packs eight per-stream flags into the single
bit-addressed byte at 0x000D and closes each row with reserved blocks at
0x0012/0x0014, the next row's rx_stream_id2 at 0x0018 fixing the stride at
24 bytes. This crate gave each flag its own byte and dropped all six
reserved bytes (ENCODED_LEN 25), misplacing every field from 0x000D on.
The eight flags are now typed bool to match their 1-bit width.

rust-RCP-P04 (BREAKING, wire): EpByteBusIdMapEntry transposed EP_Nr and
BBID against §12.7.8 Table 23's own relative addresses.

rust-RCP-P05 (BREAKING, wire): SequencerStateEntry modeled one of the row's
two fields, dropping §12.7.10 Table 25's Request_stream_index — the
sequencer's access-control binding — and misaligning every row after the
first (ENCODED_LEN 1, should be 2).

Root causes addressed, not just symptoms: this module's provenance notes
asserted the specification gave "no explicit per-field bit-width or
byte-offset table" and "no textual basis for a specific bit-position
assignment" for these tables, which is false — Tables 22/23/25 each carry
explicit Relative address and Type columns. Those notes are corrected.
Separately, every affected encoder's tests asserted only
decode(encode(x)) == x, which cannot detect a wrong layout; each fix adds
literal expected-byte vectors laid out by hand from the spec's address
columns, chosen so a transposition or mis-sized row cannot pass.

.fusa-reqs.json REQ-PWR-002, REQ-PWRSTART-001/002 and REQ-RMAP-015..019,
024..026 baked the wrong behavior in as correct; all are rewritten with
citations to the real spec source.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
@SoundMatt
SoundMatt merged commit 6630317 into main Aug 1, 2026
18 checks passed
@SoundMatt
SoundMatt deleted the fix/tc18-powerstate-and-regmap-tables branch August 1, 2026 05:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant