Skip to content

feat: v2.19.0 — wire format conformance fix pass (acf/avtp/e2e) - #94

Merged
SoundMatt merged 3 commits into
mainfrom
fix/acf-avtp-wire-conformance
Jul 30, 2026
Merged

SoundMatt merged 3 commits into
mainfrom
fix/acf-avtp-wire-conformance

Conversation

@SoundMatt

Copy link
Copy Markdown
Owner

Summary

Fixes include/rcp/acf.hpp and include/rcp/avtp.hpp's self-admitted "not full bit-for-bit wire conformance" placeholder bit-packing (landed at milestone 44, v2.0.0) by re-deriving the NTSCF/TSCF AVTPDU headers and the ACF_ABB/ACF_GBB "Message Info" common header field-by-field from the OPEN Alliance TC18 Remote Control Protocol Specification v0.5.1_RC's own bit-position diagrams. Also fixes include/rcp/e2e.hpp's CRC coverage (was silently omitting ACF_GBB's message_timestamp from the covered bytes) and adds a numeric TC18 wire error code for E2eErrc::crc_error, plus an additive RELAY spec §15.5 canonical ControlFlags/Message type.

Closes #73 (cpp-RCP-04), #66 (cpp-RCP-N2-03), #76 (cpp-RCP-08), #89 (cpp-RCP-FS-06).

How the bit layout was derived

The specification's field diagrams (Figures 5-8, page 22-25) are pixel raster images with a per-bit ruler, not extractable text, so I rendered the relevant pages at 300-600 DPI (pdftoppm), cropped/zoomed with Pillow, and counted tick marks against the ruler numerically (measuring divider pixel positions and dividing by the page's own px-per-bit spacing) rather than eyeballing. Every field width below was counted this way, not assumed from generic IEEE 1722 defaults — the task brief specifically warned this protocol customizes the standard AVTP/ACF headers, and that warning turned out to be accurate (see below).

NTSCF header (12 bytes, was 14): subtype(8) sv(1) version(3, always 0) r(1, reserved) ntscf_data_length(11, split across the last 3 bits of byte1 + all of byte2) sequence_num(8) stream_id(64).

TSCF header (24 bytes, was 18): subtype(8) sv(1) version(3) mr(1) rsv(2) tv(1) sequence_num(8) reserved(7) tu(1) stream_id(64) avtp_timestamp(32) reserved(32) stream_data_length(16) reserved(16). The old 18-byte implementation was missing an entire 4-byte reserved word and had sequence_num/timestamp-valid in the wrong bit positions (tv was previously at 0x40, i.e. one bit off from its real position at 0x01).

ACF Message Info (8 bytes, was 10): acf_msg_type(7, not a whole octet) acf_msg_length(9, sharing a byte boundary with acf_msg_type) pad(2) mtv(1) rsv(2) byte_bus_id(11) evt(4) rsv(2) hs(1) cs(1) transaction_num(8) op(1) rsp(1) err(1) ms(1) read_size_or_segment_num(12).

Cross-checks performed (no independent TC18 implementation was available to interop-test against directly):

  1. Every sub-octet field boundary was independently re-derived twice: once via pixel-position measurement of Figure 5/6/7, and once by reading the text-extractable labels of a second, independently-drawn presentation of the exact same NTSCF/TSCF/Message-Info header shapes that appears later in the spec (Figures 19/20, an E2E-CRC worked example) — both agree exactly.
  2. Those same two worked examples (Figures 19/20) state concrete acf_msg_length values (0x05 for a 6-byte-payload+2-pad ACF_ABB, 0x07 for a 7-byte-payload+1-pad ACF_GBB) and explicitly label the header "8 byte Message Info." My own quadlet-counting logic (2 quadlets header + 2 quadlets padded payload + 1 quadlet CRC trailer = 5; and the GBB equivalent = 7) reproduces both stated values exactly, and the 8-byte total matches the header byte count I derived from the figures independently.
  3. I checked go-RCP (SoundMatt/go-RCP) as a possible third cross-check, per the task's nice-to-have suggestion. Its avtp/acf packages turned out to carry the identical "not a verified transcription of the published byte assignments, own reasoned encoding" disclaimer this PR removes from cpp-RCP — including a different, self-admittedly-arbitrary TSCF subtype byte (0x83, vs. the 0x05 I derived from the actual spec figure) and a fully byte-aligned (not bit-packed) message layout. So go-RCP was not usable as an independent bit-layout cross-check here; this is a negative but informative result — go-RCP's own ACF/AVTP wire layer needs the same treatment this PR gives cpp-RCP, and I did not change go-RCP (out of scope, per instructions — never edit other repos in this remediation effort). One positive cross-check did come out of it: go-RCP's ControlFlags bit values (FlagAck=0x80, etc.) match RELAY spec §15.5 exactly, same as what I implemented for [cpp-RCP-FS-06] Canonical ControlFlags type and Message frame struct are absent #89, which corroborates both were correctly transcribed from that spec text (RELAY spec §15.5, not TC18, is the authority for those specific bit values).

Honest confidence level

High confidence, not independently interop-verified. Every bit position was counted (not eyeballed) from the specification's own diagrams and cross-checked against a second in-spec presentation of the same header shapes plus two fully worked numeric examples, all of which agree. I do not claim "full bit-for-bit wire conformance against other TC18 implementations" as an established fact — there is no independent TC18 implementation in this environment to round-trip a frame against, and go-RCP (the one candidate) turned out to have the same unverified-encoding problem, not a working reference. If a real interoperability test against another vendor's TC18 stack ever surfaces a mismatch, the two places most likely to be wrong are (a) something in the two-per-week-old sub-octet fields I had no second textual confirmation for at all — mr/tu/r/the four reserved-word fields, none of which have documented RCP-specific behavior, so I left them un-exposed and always-zero rather than guessing a meaning — and (b) evt's exact interpretation in the "standard request" context, where the spec text itself says usage is "endpoint specific, see section 13.5" (a section I did not need to read for this task's scope).

Verification performed

  • cmake -B build -DCMAKE_BUILD_TYPE=Debug -G Ninja && cmake --build build --parallel — clean build, zero warnings, across the whole tree (GCC 16 locally; also spot-checked with AppleClang, which caught one thing GCC didn't — see below).
  • ctest --test-dir build --output-on-failure — 53/53 test binaries pass, including new round-trip and hand-computed-vector tests added to tests/test_acf.cpp, tests/test_avtp.cpp, tests/test_e2e.cpp.
  • New tests include, per message-header type: encode→decode→re-encode round-trip tests (already existed, kept), and fully hand-computed expected-byte-sequence tests — every byte independently computed from the field values against my own derived layout (shown in the test's own comments), not copied from anywhere, for the NTSCF header, the TSCF header, ACF_ABB, and ACF_GBB.
  • clang-tidy (Homebrew LLVM 22, clang-diagnostic-*,clang-analyzer-*) run against test_acf.cpp/test_avtp.cpp/test_e2e.cpp with a --header-filter scoped to my three touched headers: 0 warnings after two fixes (see below). Note: this repo has no .clang-tidy config file and CI's own clang-tidy job is soft-fail (|| true), so this was a best-effort local check, not a repo-standard gate.
  • AppleClang caught one thing GCC's -Wall -Wextra didn't: a pre-existing test line in test_avtp.cpp set sequence_num = 4242 for what is now (correctly) an 8-bit field — fixed to 242.
  • clang-analyzer-optin.core.EnumCastOutOfRange flagged the new ControlFlags bitmask operator|/operator& (an OR of two single-bit flags legitimately produces a combined value with no enumerator of its own) — this is a known false-positive pattern for bitmask enums; NOLINT'd with an explanatory comment rather than silently ignored.

Files touched outside acf.hpp/avtp.hpp/e2e.hpp, and why

Per the task brief's explicit allowance to make minimal mechanical fixes at call sites when a wire-format fix changes behavior:

  • include/rcp/udp.hpp (decode_frame) and include/rcp/record.hpp (read_msg): both compared a raw wire byte directly against acf::kAcfMsgTypeAbb/kAcfMsgTypeGbb to decide which decoder to call. That comparison was only ever correct because the old (wrong) layout happened to put acf_msg_type in a whole octet; now that acf_msg_type is a real 7-bit field sharing byte0 with acf_msg_length's MSB, that raw comparison would essentially never match. Fixed both to go through a new acf::peek_acf_msg_type() helper instead. This is a real, would-have-shipped-broken bug the layout fix exposed, not a style change.
  • tests/test_udp.cpp: one line building a std::vector<uint8_t> from a now-uint16_t byte_bus_id via a braced-init-list triggered a new (GCC-silent, AppleClang-loud) narrowing warning; added an explicit static_cast<uint8_t> with a one-line comment. Purely mechanical, no behavior change (the endpoint IDs involved were always < 256).
  • .fusa-reqs.json / ROADMAP.md: added REQ-WIRE-015 for the new additive ControlFlags/Message types ([cpp-RCP-FS-06] Canonical ControlFlags type and Message frame struct are absent #89) and a new milestone 63 (v2.19.0) ahead of the existing "TC18 RCP — General Availability" milestone (renumbered 63→64), per CONTRIBUTING.md's checklist.

Left unresolved / explicitly out of scope

  • mr, tu, the NTSCF r bit, and the two 32-bit reserved TSCF words have no documented RCP-specific behavior found anywhere in the spec text I read for this task; they're encoded as always-zero and not exposed as struct fields (same "don't invent semantics" posture used elsewhere in this codebase, e.g. the IEEE1722 sv bit).
  • Section 13.5 ("endpoint specific" evt usage for standard requests) was out of this task's scope; evt_ack/evt_op are kept as the existing split (now correctly bit-packed as a real 4-bit evt nibble) rather than redesigned, to avoid a large, unnecessary blast radius across mock.hpp/cli.hpp/adapt.hpp, none of which needed a source change for this fix.
  • Fragmentation, conditional-request payload semantics, and everything else already flagged out-of-scope by this repo's own acf.hpp/avtp.hpp header comments remain out of scope here too.

Branch: fix/acf-avtp-wire-conformance
Confidence in bit-layout correctness: High (derivation + independent in-spec cross-check + two worked numeric examples all agree); not independently interop-tested against another TC18 implementation — none was available.

… 63)

Re-derives rcp/avtp.hpp's NTSCF/TSCF header bit-packing and rcp/acf.hpp's
ACF_ABB/ACF_GBB Message Info bit-packing field by field from the OPEN
Alliance TC18 Remote Control Protocol Specification v0.5.1_RC's own
bit-position diagrams, replacing the self-admitted "not full bit-for-bit
wire conformance" placeholder encoding landed at milestone 44 (v2.0.0).

- avtp.hpp: NTSCF is 12 bytes (was 14), TSCF is 24 bytes (was 18);
  sequence_num is an 8-bit rolling counter (was 16-bit); ntscf_data_length
  is an 11-bit sub-octet field; TSCF's stream_data_length lives in a
  dedicated 4-byte word after a previously-missing 4-byte reserved word.
  ByteBusId widens uint8_t -> uint16_t (byte_bus_id is an 11-bit wire
  field).
- acf.hpp: the Message Info header is 8 bytes (was 10), matching the
  specification's own two worked CRC examples that label it "8 byte
  Message Info"; acf_msg_type/acf_msg_length share a byte (7+9 bits),
  byte_bus_id is 11 bits, evt is a 4-bit field, read_size_or_segment_num is
  12 bits. Fixes two real dispatch bugs this exposed elsewhere in the tree
  (rcp/udp.hpp, rcp/record.hpp compared a raw wire byte against
  kAcfMsgTypeAbb/kAcfMsgTypeGbb directly, which almost never matches once
  acf_msg_type shares byte0 with acf_msg_length's MSB) via a new
  peek_acf_msg_type() helper. Also corrects make_response/response_kind_of:
  every response (including Acknowledge) carries rsp=1 per the
  specification's own response table; Acknowledge is distinguished by
  evt[3:0] == 0xF, not by rsp being clear.
- e2e.hpp: coverage_buffer/compute_crc/verify_crc gain a message_timestamp
  parameter and include it, at the correct byte offset, in an ACF_GBB
  message's CRC coverage (previously omitted entirely). E2eErrc::crc_error
  gains a numeric TC18 wire error code accessor (POCI_FAILURE = 12).
- acf.hpp gains ControlFlags/Message, an additive, documented thin view
  over AcfMessageInfo matching RELAY spec §15.5's canonical decoded-frame
  shape.

Closes #73, #66, #76, #89.

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

Merges origin/main (which now includes the retired-model-removal and
wire-format-endpoint-fix PRs) into this branch. ROADMAP.md conflicted
because both this branch and the retired-model-removal PR appended a
milestone entry after #62; resolved by keeping both narratively in
order (dated cleanup entry, then '63. Wire Format Conformance Fix
Pass', then '64. TC18 RCP GA', renumbered from '63').

Two integration-only defects surfaced by combining this branch's
avtp::ByteBusId widening (uint8_t -> uint16_t, an 11-bit wire field)
with code neither this branch nor the retired-model-removal branch
touched on its own:

- tests/test_rcp.cpp (added by the retired-model-removal PR, which
  predates this branch's ByteBusId widening) asserted
  ByteBusId == uint8_t via static_assert; updated to assert the type
  is wide enough for the real 11-bit range instead.
- include/rcp/discovery.hpp's encode_discovery_request took
  sequence_num as uint16_t and assigned it into NtscfHeader::sequence_num,
  which this branch narrowed to uint8_t (sequence_num is an 8-bit wire
  field, not 16) as part of the same header fix. Neither this file nor
  its test was in this branch's original diff, so the mismatch only
  appeared once merged; narrowed the parameter type to match.

Full local build (C++17, Ninja) and ctest (52/52 tests) pass on the
merged tree.

Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com>
test_capi.cpp's mock_server_request_fn assigns AcfMessageInfo::byte_bus_id
(now avtp::ByteBusId = uint16_t, an 11-bit wire field) into
rcp_response_t::info.byte_bus_id (rcp_byte_bus_id_t = uint8_t, a
pre-existing, narrower C-ABI type unrelated to and out of scope for this
fix pass). GCC/Clang silently truncate; MSVC's warnings-as-errors build
fails on the implicit narrowing (C4244 -> C2220). Add an explicit cast
with a comment explaining the pre-existing ABI width gap, rather than
changing capi.h's public C ABI.

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