Skip to content

fix: v2.20.0 — TC18 conformance fix pass II - #98

Merged
SoundMatt merged 1 commit into
mainfrom
fix/tc18-conformance
Jul 31, 2026
Merged

SoundMatt merged 1 commit into
mainfrom
fix/tc18-conformance

Conversation

@SoundMatt

Copy link
Copy Markdown
Owner

Summary

Second gap-audit pass against the OPEN Alliance TC18 Remote Control Protocol Specification v0.5.1_RC, addressing every mandatory:true item in the worklist plus two additional low-priority items. Two process/CI items were deferred (see below).

Worklist completion

ID Severity Status
cpp-RCP-01 (acf_msg_length never computed) critical, mandatory Done
cpp-RCP-04-fresh (multiple requests in one frame, §12.9.1.1) critical, mandatory Done
cpp-RCP-02 (error responses carry no error-code byte) high, mandatory Done
cpp-RCP-05-fresh (GPIO accepts over-long payload) medium, mandatory Done
cpp-RCP-03 (PWM accepts over-long payload) medium, mandatory Done (pre-verified diff applied cleanly)
cpp-RCP-A4-iseled (no ISELED wire codec) medium, mandatory Done
cpp-RCP-A4-pwmin (PWM_IN single invented trigger vs Table 44's two) low Done
cpp-RCP-A3 (control_data_length never validated) low Done
cpp-RCP-A1 (CI cpfusa analyze non-gating) low Deferred
cpp-RCP-A2 (Actions pinned to mutable tags) low Deferred

Self-verification: acf_msg_length worked examples

Per instructions, hand-verified the new acf::compute_acf_msg_length() + encode_acf_abb/encode_acf_gbb against the specification's own two fully worked numeric examples, end to end (header layout was already correct from the prior v2.19.0 pass and is untouched here — only length computation was missing). This exact scenario is now pinned as tests/test_e2e.cpp's "acf_msg_length end-to-end matches the specification's own worked examples":

ACF_ABB (6-byte payload + 2 pad bytes, caller-padded to 8 bytes total, + trailing 4-byte CRC32):

  • compute_acf_msg_length(kAcfMsgTypeAbb, 8) = 4 (header 2 quadlets + payload 2 quadlets)
  • apply_acf_length_adjustment() → 5 (+1 quadlet for the CRC trailer)
  • Encoded header bytes 0–1: frame[0] & 0x01 == 0, frame[1] == 5 → acf_msg_length = 0x05, matching the spec exactly
  • Total frame after append_crc: 8 (header) + 8 (payload) + 4 (CRC) = 20 bytes = 5 quadlets ✓

ACF_GBB (7-byte payload + 1 pad byte, caller-padded to 8 bytes total, + trailing 4-byte CRC32):

  • compute_acf_msg_length(kAcfMsgTypeGbb, 8) = 6 (header 2 + timestamp 2 + payload 2 quadlets)
  • apply_acf_length_adjustment() → 7 (+1 quadlet for the CRC trailer)
  • Encoded header bytes 0–1: frame[0] & 0x01 == 0, frame[1] == 7 → acf_msg_length = 0x07, matching the spec exactly
  • Total frame after append_crc: 8 (header) + 8 (timestamp) + 8 (payload) + 4 (CRC) = 28 bytes = 7 quadlets ✓

Ran locally:

$ tests/test_e2e "acf_msg_length end-to-end*"
All tests passed (12 assertions in 1 test case)

Design notes

  • acf_msg_length auto-fill is sentinel-based, not unconditional: encode_acf_abb/encode_acf_gbb only compute a value when the caller left AcfMessageInfo::acf_msg_length at its 0 default (true for every real production call site today — 0 is never a legitimate real wire value, since the header alone is 2 quadlets). A caller that has already set a nonzero value itself (the CRC-adjustment flow) is always respected unchanged. This preserves 100% of the existing hand-constructed-header test suite (which explicitly sets acf_msg_length in most cases) while fixing the "always literally 0" defect for every real caller — confirmed by a full local test run with zero test changes needed for the base fix itself.
  • Multi-message decode boundary-finding has a documented, honest scope limit: it trusts acf_msg_length to find message boundaries precisely, which requires each non-final message's payload to already be quadlet-aligned (true for GPIO/PWM's fixed 4-byte payloads). A non-final message with a non-aligned payload falls back to "consume the rest of the buffer" (the same convention the single-message decoder always used) rather than corrupting the parse — documented in decode_acf_messages's own comment.
  • Table 27 error codes are intentionally a separate enum from regmap::RegMapErrc, not a renumbering of it: RegMapErrc's ordinals (1–4) are this codebase's own internal std::error_code values used elsewhere and were left untouched; mock.hpp's new wire_error_code_for() explicitly translates by name to acf::WireErrorCode's correct Table 27 numeric values (3, 4, 11, 15 for the four RegMapErrc cases) rather than reusing the internal ordinal.

Build / test

Full local build (C++17, all targets, fresh out-of-tree build dir) and ctest: 52/52 suites pass, including all new/updated tests (test_acf, test_e2e, test_udp, test_mock, test_gpio, test_pwm, test_iseled).

cpfusa (the cpp-FuSa safety-tooling CLI CI also gates on) could not be run locally — no cpp-FuSa checkout or built cpfusa binary is available in this environment, and building it from source was out of scope for this pass's time budget. This is the same tooling cpp-RCP-A1 concerns; deferred alongside it.

Deferred (honest reasons)

  • cpp-RCP-A1 (CI's cpfusa analyze step runs non-gating via || true): changing what CI gates on is a policy call this pass can't safely make blind — with no local cpfusa build available to see what findings would newly gate the build, flipping this could surprise-fail CI in ways I have no way to verify here.
  • cpp-RCP-A2 (GitHub Actions pinned to mutable major-version tags, not commit SHAs): a supply-chain hardening change with no way to verify pinned SHAs still resolve correctly against the hosted Actions runners from this environment.

Both are low-severity, explicitly non-wire-conformance items per the worklist itself, and are reasonable to pick up in a dedicated follow-up pass with the right tooling/verification available.

Second gap-audit pass against the specification's own worked numeric
examples and per-endpoint request-handling text. Fixes, in priority order:

- acf_msg_length is now actually computed (cpp-RCP-01, critical):
  AcfMessageInfo::acf_msg_length defaulted to 0 and nothing filled it
  in, so every frame this library emitted carried acf_msg_length=0
  despite the header *bit layout* already being correct. New
  acf::compute_acf_msg_length() computes the real quadlet count;
  encode_acf_abb/encode_acf_gbb call it automatically whenever a
  caller leaves acf_msg_length at its 0 default, while still
  respecting a caller-computed nonzero value (e.g. e2e.hpp's
  apply_acf_length_adjustment baking in a CRC trailer's +1 quadlet).
  Verified end to end against the spec's own two worked examples
  (ACF_ABB -> 0x05 for a 20-byte frame, ACF_GBB -> 0x07 for a 28-byte
  frame) in tests/test_e2e.cpp.

- Multiple ACF requests packed into one AVTPDU are now handled
  (cpp-RCP-04-fresh, critical): §12.9.1.1 mandates it. New
  acf::decode_acf_messages/AcfEntry walk a buffer that may hold more
  than one ACF_ABB/ACF_GBB message; udp.hpp gains a parallel
  MultiFrame/encode_multi_frame/decode_multi_frame envelope, and
  udp::Server::serve() now dispatches and answers every request in an
  inbound datagram individually instead of only the first one.

- Error responses now carry a Table 27 error-code byte_msg_payload
  (cpp-RCP-02, high): acf::WireErrorCode fixes Table 27's 17 numeric
  values (deliberately independent of regmap.hpp's own RegMapErrc
  ordinals); mock.hpp gains wire_error_code_for() and routes every
  ErrorResponse through one set_error_response() helper.

- GPIO's payload decoder now rejects a mis-sized buffer
  (cpp-RCP-05-fresh, medium), matching PWM's exact-four-bytes fix
  (cpp-RCP-03, applied from a pre-verified diff in this same pass).

- ISELED gains a real ACF payload codec (cpp-RCP-A4-iseled, medium):
  encode/decode for both the Instruction/Address/Data request shape
  (Figure 40) and the Address/Data[11:0] response shape (Figure 41) —
  previously only validated in-memory structs existed, with no way to
  actually build or parse an ISELED request/response for the wire.

- PWM_IN's trigger signals now match Table 44 (cpp-RCP-A4-pwmin, low):
  RisingEdge/FallingEdge replace the single invented MidPulse signal.

- control_data_length is now validated against the buffer it actually
  arrived in (cpp-RCP-A3, low), via the already-defined-but-unused
  avtp::AvtpErrc::length_mismatch.

Deferred: cpp-RCP-A1 (CI's cpfusa analyze step is non-gating) and
cpp-RCP-A2 (GitHub Actions pinned to mutable tags, not SHAs) — both
process/CI items with no TC18 wire-conformance content, deferred
because this environment has no cpfusa binary/build to verify an
analyze-gating change against and no way to test a SHA-pin bump's
effect on hosted Actions runners from here.

Wire-format-adjacent, non-breaking-at-the-API-level: every emitted ACF
frame's acf_msg_length bytes and every error response's payload bytes
now differ from what v2.19.0 emitted (0 -> correct value; empty -> one
octet). No C++ function signature changed. kVersion/CMakeLists.txt
bump 2.19.0 -> 2.20.0 accordingly.

Full local build (C++17, all targets) and ctest (52/52 suites) pass.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
@SoundMatt
SoundMatt merged commit 3eacb1d into main Jul 31, 2026
21 checks passed
@SoundMatt
SoundMatt deleted the fix/tc18-conformance branch July 31, 2026 12:47
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