Skip to content

fix: correct PWM_OUT/ISELED/MDIO/ADC wire formats (#70, #71, #72, #77) - #91

Merged
SoundMatt merged 2 commits into
mainfrom
fix/wire-format-pwm-iseled-mdio-adc
Jul 30, 2026
Merged

SoundMatt merged 2 commits into
mainfrom
fix/wire-format-pwm-iseled-mdio-adc

Conversation

@SoundMatt

Copy link
Copy Markdown
Owner

Summary

Four endpoint-type wire-format/error-identity fixes, scoped strictly to
include/rcp/pwm.hpp, include/rcp/iseled.hpp, include/rcp/mdio.hpp,
include/rcp/adc.hpp, their matching test files, .fusa-reqs.json, and
ROADMAP.md. No other file is touched. Each fix was verified directly
against the OPEN Alliance TC18 Remote Control Protocol Specification's own
figures/tables (via pdftotext/pdftoppm on the local spec PDF), not
against the prior extraction summaries the headers were originally written
from. No spec text is quoted anywhere below or in the code/comments —
behavior is described in my own words and cited by figure/table name only.

#70 — include/rcp/pwm.hpp

  • PwmValue::period/active_duration were uint32_t (an 8-byte payload).
    Verified against the "pwmo request format" figure (§13.7.5.3, Figure 26):
    the payload is a fixed 4 bytes — PWM_Period (big-endian 16 bit)
    followed by PWM_active (big-endian 16 bit). The existing field
    order (period, then active) was already correct
    — I re-checked this
    against the figure myself before touching anything, per the task's
    request to verify rather than trust the issue text, and confirmed no
    swap was needed. Both fields are now uint16_t, and I added
    encode_pwm_payload/decode_pwm_payload (there was no wire codec at
    all before) implementing the exact 4-byte big-endian layout, reusing
    rcp/avtp.hpp's existing put_u16/get_u16 helpers the same way
    rcp/gpio.hpp's payload codec already reuses put_u32/get_u32.
  • Also removed PWM_OUT's reuse of GPIO's 8-way bitmask/saturating-write
    combinator (rcp::endpoint::apply_bitmask_write), which I was able to
    do cleanly rather than leaving it in place: the spec's PWM_OUT
    request-handling section describes only direct application of a
    request's period/active values (a request with PWM_Period of 0 stops
    generation) — there's no described combination against the endpoint's
    previous state the way GPIO's OR/AND/XOR/saturating-add/subtract make
    sense for independent pin bits. PwmOutEndpoint::handle_write now only
    honors WriteSemantics::Replace; every other semantics is rejected via
    the existing EndpointErrc::non_combinable_write_semantics sentinel.

#71 — include/rcp/iseled.hpp

  • IseledResponse::address was a full uint16_t, ::data was an open
    std::vector<uint8_t>, and the header carried an invented 8-bit CRC
    (polynomial 0x07) its own prior comment admitted wasn't from the
    ISELED standard. Verified against the "iseled request format" (Figure
    40) and "iseled response format" (Figure 41) figures, §13.7.12.3: the
    address field is 12 bits (in both request and response), and a
    response's data is a single 12-bit value (Data[11:0]), not an open
    byte vector. kIseledFieldMask/kIseledInstructionMask plus
    validate_request/validate_response now enforce those widths.
    IseledRequest::data is left as a byte vector — Figure 40 shows the
    request's data as variable-length (Data1/Data2/Data3/...+padding), so a
    vector remains correct there; only the response's fixed-width fields
    were wrong.
  • The invented CRC-8 is dropped outright, per the task's stated
    preference: I don't have a verified real ISELED-standard CRC-4
    algorithm to source (the spec explicitly defers the ISELED CRC's actual
    definition to the separate ISELED standard, which isn't in this repo),
    so inventing a differently-sized polynomial would repeat the same
    mistake. IseledErrc::field_out_of_range (a direct, non-invented
    consequence of the width fix) takes the error category's place;
    IseledSignal::NativeCrcError is removed, leaving TransferComplete as
    the endpoint's only trigger signal.

#72 — include/rcp/mdio.hpp

  • Replaced the invented IEEE 802.3 Clause 22/Clause 45 PHY-addressing
    model (5-bit PHY address, Clause22/45 enum, device/register split) with
    the spec's actual model, verified against the "mdio request format"
    figure (Figure 42) and Table 57, §13.7.13.3: a 2-bit mdio_mode
    selector, an mdio_address field ("as per IEEE & OA SPI spec" — opaque
    to this header, not decomposed further), and an mdio_payload whose
    width depends on mode.
  • mdio_mode values: I found that Table 57 itself prints 01b for
    both "MMD, single word access" and "MMD, multiple byte access" —
    this can't be right for a 2-bit field where the other two rows (10b
    MMS single, 11b MMS multi) are each printed once. Rather than guess, I
    resolved it by elimination: since 3 of the 4 two-bit values are
    unambiguous (01b explicit on the second MMD row, 10b, 11b), the
    remaining unclaimed value 00b must be MMD single word access. This
    reasoning is documented at length in the header comment so it's
    auditable, not asserted.
  • mdio_payload width, per Table 57's own payload-width row: 16 bits for
    MMD (either sub-mode) and MMS single-word access; 32 bits for MMS
    multi-(double-)word access specifically when the addressed MMS device is
    MMS0 or MMS1 (16 bits for any other MMS device). Since which MMS
    device number mdio_address encodes is itself deferred to the external
    IEEE/OA SPI addressing scheme (not decoded by this header, consistent
    with how mdio_address is treated as opaque throughout), I added an
    explicit MdioRequest::mms_is_0_or_1 field the caller supplies, with
    payload_width_bits(mode, mms_is_0_or_1) and validate_request
    implementing the exact rule.

#77 — include/rcp/adc.hpp

  • Narrowed sample/result values from uint32_t to uint16_t throughout
    AdcEndpoint/compute_average, matching the spec's 16-bit ADC
    resolution ceiling (§13.7.9.1) and "16 bit ADC value" response-frame
    fields (Figure 32/34). Summation still widens internally to a 64-bit
    accumulator to avoid overflow; only the stored/returned values are
    16-bit. Added a boundary test averaging three 0xFFFF samples to
    exercise this without truncation.
  • Removed the invented "ADC_NO_SIGNAL" string from the no-signal
    diagnostic message. Checked Table 27 ("Error codes in responses",
    §12.9.6) myself: there is no ADC-specific no-signal entry anywhere in
    it — the only *_NO_SIGNAL code in the whole table is
    PWM_IN_NO_SIGNAL (value 9), which belongs to PWM_IN, a different
    endpoint type. Since nothing in Table 27 actually fits an ADC no-signal
    condition, AdcErrc::no_signal's message is now documented and rendered
    as a plain internal library condition with no claimed TC18 error-code
    identity (kept conceptually — not nominally — parallel to PWM_IN's
    no-signal handling).

Other changes

What's not resolved / flagged explicitly

  • ISELED's real CRC algorithm is dropped, not replaced — see above. If a
    verified ISELED-standard CRC-4 algorithm becomes available, it should
    replace the current "no CRC support" state rather than staying dropped
    forever.
  • clang-tidy currently reports "no checks enabled" on this repo (no
    .clang-tidy config file exists), and CI's own clang-tidy job runs it
    wrapped in || true, so it's a non-blocking no-op today regardless of
    code content — this predates this PR and I didn't add a .clang-tidy
    config since that's out of scope for a wire-format fix. I additionally
    ran clang-tidy locally with an explicit --checks list
    (clang-analyzer-*,bugprone-*,performance-*) against both my changed
    headers and an untouched sibling header (rcp/gpio.hpp) for comparison
    — the same findings (enum base-type size suggestions, the standard
    is_error_code_enum "std namespace modification" idiom every
    rcp::*Errc type in this codebase already uses) appear identically in
    both, confirming nothing new was introduced.
  • cpfusa (the FuSa CLI referenced by ROADMAP.md/CI) isn't installed in
    this environment, so I couldn't run cpfusa check/lint/cyber/trace
    locally; I updated .fusa-reqs.json by hand for the requirements whose
    described behavior changed.

Verification

  • cmake -B build -DCMAKE_BUILD_TYPE=Debug -G Ninja && cmake --build build --parallel — succeeds, no warnings from the touched files.
  • ctest --test-dir build --output-on-failure — 53/53 tests pass,
    including all updated/new PWM, ISELED, MDIO, and ADC cases.
  • git status/git diff --stat — only the 10 in-scope files changed:
    .fusa-reqs.json, ROADMAP.md, the 4 headers, and their 4 test files.

🤖 Generated with Claude Code

…, #77)

Four endpoint-type conformance fixes, each verified against the OPEN
Alliance TC18 Remote Control Protocol Specification's own figures/tables
rather than against prior extraction summaries:

- rcp/pwm.hpp (#70): PWM_OUT/PWM_IN's period/active_duration were uint32_t
  (8-byte payload); the spec's pwmo request format figure shows a fixed
  4-byte payload (two big-endian 16-bit words, period then active — the
  existing field order was already correct). Narrowed both fields to
  uint16_t and added encode_pwm_payload/decode_pwm_payload for the exact
  4-byte wire codec. Also removed PWM_OUT's reuse of GPIO's 8-way
  bitmask/saturating-write combinator: the spec's PWM_OUT request handling
  only ever applies a request's values directly, so handle_write now only
  honors WriteSemantics::Replace and rejects everything else as
  non-combinable.
- rcp/iseled.hpp (#71): IseledResponse::address was a full uint16_t,
  ::data was an open vector<uint8_t>, and the header carried an invented
  8-bit CRC (poly 0x07) its own comment admitted wasn't from the ISELED
  standard. The response format figure specifies a 12-bit address, a
  12-bit data value, and an optional CRC whose real algorithm/width the
  ISELED standard (not TC18) defines. Repacked address/data to 12-bit
  range-validated fields and dropped the invented CRC outright rather than
  inventing a differently-sized one.
- rcp/mdio.hpp (#72): replaced the invented IEEE 802.3 Clause 22/45
  PHY-addressing model with the spec's actual mdio_mode/mdio_address/
  mdio_payload model (Figure 42 + Table 57): a 2-bit mode selector, an
  opaque address field, and a payload that's 16 bits except for MMS
  multi-word access to MMS0/MMS1, which is 32 bits.
- rcp/adc.hpp (#77): narrowed sample/result values from uint32_t to
  uint16_t throughout AdcEndpoint/compute_average, matching the spec's
  16-bit ADC resolution ceiling and response-frame figures. Also removed
  the invented "ADC_NO_SIGNAL" identifier from the no-signal diagnostic
  message — Table 27 defines no ADC-specific no-signal error code (the
  only *_NO_SIGNAL entry, PWM_IN_NO_SIGNAL, belongs to PWM_IN).

Updated .fusa-reqs.json requirement text for the affected REQ-PWM-002/003,
REQ-ISELED-002/003, and REQ-MDIO-002/003/004 entries, and added a
post-hoc fix-pass note to ROADMAP.md. All 53 ctest cases pass.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
…wing error

test_adc.cpp constructed std::optional<uint16_t> from bare int/unsigned
literals in several fixtures (added by the uint32_t->uint16_t ADC width
fix). GCC/Clang accept the implicit narrowing conversion silently, but
MSVC's warnings-as-errors build (C4244 -> C2220) fails on it. Wrap the
literals in explicit uint16_t{...} so the conversion is exact everywhere.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
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