diff --git a/include/rcp/acf.hpp b/include/rcp/acf.hpp index 3c8e971..986b8fc 100644 --- a/include/rcp/acf.hpp +++ b/include/rcp/acf.hpp @@ -29,58 +29,68 @@ // `read_size_or_segment_num`) already exist below so later milestones can be // layered on without reshaping this header. // -// Field names and behavior below implement TC18's *behavior* as described in -// an internal structured extraction of the specification named above; no -// text from that document is reproduced here. Wire conformance note -// (v2.19.0, issue cpp-RCP-04): the ACF Message Info bit-packing below was -// re-derived field by field from the specification's own header diagram -// (its bit-position figure, not prose) and cross-checked two ways: (1) -// against a second, independent diagram of the same header shape used for a -// different message subtype elsewhere in the specification, and (2) against -// two fully worked numeric examples elsewhere in the specification (one -// ACF_ABB, one ACF_GBB) that state concrete byte counts and an -// acf_msg_length value for a chosen payload/padding. +// Field names and behavior below implement TC18's *behavior*, ported from +// c-RCP's acf.h/acf.c (Phase 17, cpp-RCP issue #129), which is this +// project's RC5-spec-conformant reference implementation for this wire +// layer; no text from the specification is reproduced here. // -// acf_msg_length conformance note (v2.20.0, issue cpp-RCP-01): the bit -// layout above was already correct, but until this pass nothing in this -// codec ever *computed* acf_msg_length from a real message's actual size — -// AcfMessageInfo::acf_msg_length defaulted to 0 and stayed 0 unless a -// caller (only rcp/e2e.hpp's own unit tests did) set it by hand, so every -// frame this library actually emitted over rcp/udp.hpp carried -// acf_msg_length=0. compute_acf_msg_length() below now does that -// computation (quadlets over the shared header, the ACF_GBB timestamp when -// present, and the payload as given — see its own comment for the exact -// worked-example derivation), and encode_acf_abb()/encode_acf_gbb() call it -// automatically whenever the caller leaves AcfMessageInfo::acf_msg_length at -// its 0 default, which is what every real request/response builder in this -// tree does today (rcp::acf::make_standard_request, rcp::acf::make_response, -// rcp/mock.hpp's dispatch_*, rcp/discovery.hpp, rcp/record.hpp). A caller -// that has already computed a specific value itself — e.g. rcp/e2e.hpp's -// apply_acf_length_adjustment(), which must bake a trailing CRC's +1 -// quadlet into the header *before* that header is serialized for CRC -// coverage — sets AcfMessageInfo::acf_msg_length to a nonzero value first, -// which this codec always takes as an explicit override and never -// recomputes out from under it. See compute_acf_msg_length()'s own comment -// for this fix's known scope limit (it counts quadlets from the payload -// length exactly as given, since this codec has always left padding a -// caller-owned concern — see the "ACF shared header" section above — rather -// than silently appending real pad octets a caller did not ask for). +// byte_message_info bit-packing (encode_acf_message_info/ +// decode_acf_message_info below) was already correct as of this codec's +// v2.19.0 pass and is unchanged by the Phase 17 port — verified bit-for-bit +// against c-RCP's own pack/unpack golden-vector tests (test_acf.c's +// test_pack_header_bit_positions/test_unpack_header_bit_positions_from_ +// raw_bytes), which this file's own equivalent tests below now also pin. // -// ACF_GBB timestamp-position conformance note (v2.22.0, issue -// cpp-RCP-GBB-TS): both notes above concern the shared header's *bit* -// packing and its length field; neither noticed that the ACF_GBB message's -// *byte* geometry was also wrong. encode_acf_gbb/decode_acf_gbb placed the -// 64-bit message_timestamp after the complete 8-byte shared header (wire -// octet 8), when the specification splices it between that header's two -// quadlets (wire octet 4), pushing evt/rsv/hs/cs/transaction_num/op/rsp/ -// err/ms/read_size_or_segment_num from octet 8 to octet 12. Every ACF_GBB -// message this library emitted before v2.22.0 was therefore unreadable by -// a conformant peer (and vice versa), and every ACF_GBB E2E CRC was -// computed over the wrong byte sequence. Fixed below; see the -// kAcfGbbTimestampOffset constant block for the point-by-point -// verification against the specification's own figures. This is a -// wire-format-breaking change against every prior cpp-RCP release — -// ACF_ABB is entirely unaffected, since it has no timestamp field. +// acf_msg_length auto-fill (v2.20.0, issue cpp-RCP-01, retained by the +// Phase 17 port): AcfMessageInfo::acf_msg_length defaults to 0; +// encode_acf_abb()/encode_acf_gbb() compute it from the caller's actual +// payload size (compute_acf_msg_length()) whenever the caller leaves it at +// that 0 default, and take a nonzero value as an explicit override, +// re-serializing it unchanged instead. This is a deliberate divergence from +// c-RCP's own rcp_acf_encode_abb()/_gbb(), which always recompute +// acf_msg_length (and always auto-pad the payload to a quadlet boundary via +// rcp_acf_pad_len(), overwriting whatever the caller's own hdr->pad held) — +// this codec's "0 = auto, nonzero = override" contract, and its +// caller-owns-padding convention, are load-bearing for rcp/e2e.hpp's +// apply_acf_length_adjustment(), which must bake a trailing CRC32 trailer's +// +1 quadlet into the header *before* it is serialized for CRC coverage, +// and are exercised by rcp/e2e.hpp's own tests (tests/test_e2e.cpp). Phase +// 17 (this pass) is scoped to acf.hpp/avtp.hpp only and does not touch +// rcp/e2e.hpp, so this codec deliberately keeps both conventions rather +// than adopting c-RCP's always-recompute/always-pad behavior, which would +// silently break that contract. See pad_len() below (new, additive — +// ported from rcp_acf_pad_len() as a pure utility any caller MAY use) and +// this file's own "// TODO(phase1-followup)" markers for the specific +// c-RCP behaviors this pass intentionally left unported for that reason. +// +// ACF_GBB Message Info geometry (Phase 17 wire-format fix, issue +// cpp-RCP-GBB-TS): a prior pass (v2.22.0) placed the 64-bit +// message_timestamp *spliced between* the shared header's two quadlets +// (wire octet 4, pushing evt/hs/cs/transaction_num/op/rsp/err/ms/ +// read_size_or_segment_num to octet 12). c-RCP's acf.h/acf.c — this +// project's RC5-conformant reference for this module — is unambiguous that +// the real layout is CONTIGUOUS instead: the complete 8-byte +// byte_message_info header at octets 0..7, the 8-byte message_timestamp +// immediately after it at octets 8..15, then byte_msg_payload at octet 16 +// onward (RCP_ACF_GBB_HEADER_LEN == RCP_ACF_ABB_HEADER_LEN + 8, per c-RCP's +// acf.h file comment, its rcp_acf_encode_gbb()/_decode_gbb() implementation +// in acf.c, its own test_peek_gbb_request_type() pinning byte 8 as the +// first byte after a *contiguous* 8-byte header, and its +// .fusa-reqs.json REQ-ACF-044 citation "ACF_GBB's additional 8-byte +// message_timestamp, e.g. TC18.txt L1447-1451"). The v2.22.0 splice was +// therefore a regression, not a fix; it is reverted below. Every real +// caller in this tree (rcp/e2e.hpp, rcp/l2.hpp, rcp/udp.hpp, +// rcp/record.hpp, rcp/request.hpp) reaches the Message Info block only +// through encode_acf_gbb_message_info()/decode_acf_gbb_message_info()/ +// encode_acf_gbb()/decode_acf_gbb() below, never through a raw byte offset +// of its own, so this fix changes wire *content* for those callers but not +// their source. The one exception is tests/test_e2e.cpp, which pins the +// old (spliced) layout via hardcoded byte literals — those assertions are +// now wrong and need a follow-up fix in a later phase that touches +// rcp/e2e.hpp; see this pass's own PR description for the specific test +// cases affected. This is a wire-format-breaking change against every +// cpp-RCP release since v2.22.0 — ACF_ABB is entirely unaffected, since it +// has no timestamp field. #pragma once #include @@ -209,51 +219,32 @@ struct AcfMessageInfo { constexpr size_t kAcfCommonHeaderLen = 8; constexpr size_t kAcfGbbTimestampLen = 8; -// ── ACF_GBB Message Info wire geometry (v2.22.0 fix) ────────────────────────── -// The 8 shared-header bytes above are *contiguous* on the wire only for -// ACF_ABB. For ACF_GBB the specification splices the 64-bit -// message_timestamp **between** the shared header's two quadlets, not after -// both of them — so an ACF_GBB Message Info block is 16 bytes laid out as: +// ── ACF_GBB Message Info wire geometry ──────────────────────────────────────── +// For ACF_GBB, the 64-bit message_timestamp sits immediately AFTER the +// complete, contiguous 8-byte byte_message_info header — not spliced +// between its two quadlets. An ACF_GBB Message Info block is therefore 16 +// bytes laid out as: // -// offset 0..3 shared-header quadlet 0 -// (acf_msg_type / acf_msg_length / pad / mtv / rsv / byte_bus_id) -// offset 4..11 message_timestamp, 64-bit big-endian -// offset 12..15 shared-header quadlet 1 -// (evt / rsv / hs / cs / transaction_num / op / rsp / err / ms / -// read_size_or_segment_num) +// offset 0..7 the complete byte_message_info header, same contiguous +// 8-octet layout ACF_ABB uses (acf_msg_type / +// acf_msg_length / pad / mtv / rsv / byte_bus_id / evt / +// rsv / hs / cs / transaction_num / op / rsp / err / ms / +// read_size_or_segment_num) +// offset 8..15 message_timestamp, 64-bit big-endian // offset 16.. byte_msg_payload // -// Independently verified against the specification's own figures, not -// inferred from this codec's prior behavior (which had the timestamp after -// *both* header quadlets, at offset 8, pushing quadlet 1 to offset 8 -// instead of 12 — the bug this constant block exists to prevent -// recurring): -// (a) The single-ACF_GBB CRC-coverage figure draws one "Byte Message -// Info" group of three rows in this exact order: the -// acf_msg_type/acf_msg_length/pad/mtv/rsv/byte_bus_id quadlet, then -// message_time_stamp rendered as a double-height 64-bit block (the -// same way that figure's own 64-bit stream_id is drawn), then the -// evt/rsv/hs/cs/transaction_num/op/rsp/err/ms/read_size quadlet. -// (b) That figure's own stated acf_msg_length (7 quadlets = 28 octets) -// is only reproducible with a 64-bit timestamp inside the Message -// Info block: quadlet0(4) + timestamp(8) + quadlet1(4) + payload -// (7 real + 1 pad = 8) + CRC32(4) = 28. Its ACF_ABB counterpart, with -// no timestamp at all, states 5 quadlets = 20 octets: 4 + 4 + (6 real -// + 2 pad = 8) + 4 = 20. Both check out exactly. -// (c) The compound-request figure (an ACF_GBB with mtv=0) shows the -// mtv=0 repurposing fields — request_type/cmp_start_state/ -// cmp_next_state/cmp_sequencer, then cmp_exec_delay/cmp_repetitions — -// occupying exactly the two quadlets *between* the same two header -// quadlets, i.e. octets 4..11, which is only consistent with the -// timestamp slot they repurpose living there too. -// (d) The response-field table lists the Message Info fields in wire -// order and places message_timestamp ("Present in ACF_GBB, omitted in -// ACF_ABB") between byte_bus_id (quadlet 0's last field) and evt -// (quadlet 1's first field). -constexpr size_t kAcfHeaderQuadletLen = 4; -constexpr size_t kAcfGbbTimestampOffset = 4; // == kAcfHeaderQuadletLen -constexpr size_t kAcfGbbSecondQuadletOffset = 12; // == kAcfHeaderQuadletLen + kAcfGbbTimestampLen -constexpr size_t kAcfGbbMessageInfoLen = 16; // == kAcfCommonHeaderLen + kAcfGbbTimestampLen +// This is ported directly from c-RCP's acf.h/acf.c (this project's +// RC5-conformant reference for this module — see this file's own header +// comment for the full derivation and the specific byte-geometry bug this +// reverts): RCP_ACF_GBB_HEADER_LEN there is defined as exactly +// RCP_ACF_ABB_HEADER_LEN + 8, rcp_acf_encode_gbb() packs the full 8-byte +// header contiguously via rcp_acf_pack_header() and then writes +// message_timestamp at that fixed offset 8, and its own +// test_peek_gbb_request_type() pins byte 8 as the first byte immediately +// following a *contiguous* 8-byte header (the message_timestamp region's +// own leading octet, repurposed by the conditional-request modules). +constexpr size_t kAcfGbbTimestampOffset = kAcfCommonHeaderLen; // 8: right after the contiguous header +constexpr size_t kAcfGbbMessageInfoLen = kAcfCommonHeaderLen + kAcfGbbTimestampLen; // 16 // compute_acf_msg_length computes the wire acf_msg_length value (quadlets, // counted over the *entire* ACF message: the 8-byte shared header, the @@ -358,42 +349,33 @@ inline void decode_acf_message_info(const uint8_t* in8, AcfMessageInfo& out) noe out.read_size_or_segment_num = static_cast(((in8[6] & 0x0F) << 8) | in8[7]); } -// ── ACF_GBB Message Info: the spliced 16-byte form ──────────────────────────── +// ── ACF_GBB Message Info: header + timestamp, contiguous ───────────────────── // encode_acf_gbb_message_info/decode_acf_gbb_message_info are the ACF_GBB // analogues of encode_acf_message_info/decode_acf_message_info above. They // deliberately reuse those two functions for the bit-level packing (so the -// bit layout has exactly one definition in this file) and only own the -// ACF_GBB-specific *byte geometry*: quadlet 0, then the 64-bit -// message_timestamp, then quadlet 1 — see the kAcfGbbTimestampOffset / -// kAcfGbbSecondQuadletOffset comment block above for the verification of -// that ordering against the specification's own figures. `out16`/`in16` -// must point at kAcfGbbMessageInfoLen (16) accessible bytes. +// bit layout has exactly one definition in this file) and only add the +// ACF_GBB-specific byte geometry: the complete 8-byte header, then the +// 64-bit message_timestamp immediately after it — see the +// kAcfGbbTimestampOffset comment block above for the c-RCP-derived +// verification of that ordering. `out16`/`in16` must point at +// kAcfGbbMessageInfoLen (16) accessible bytes. inline void encode_acf_gbb_message_info(const AcfMessageInfo& info, uint64_t message_timestamp, uint8_t* out16) noexcept { - uint8_t hdr[kAcfCommonHeaderLen]; - encode_acf_message_info(info, hdr); - std::copy(hdr, hdr + kAcfHeaderQuadletLen, out16); + encode_acf_message_info(info, out16); avtp::detail::put_u64(out16 + kAcfGbbTimestampOffset, message_timestamp); - std::copy(hdr + kAcfHeaderQuadletLen, hdr + kAcfCommonHeaderLen, - out16 + kAcfGbbSecondQuadletOffset); } inline void decode_acf_gbb_message_info(const uint8_t* in16, AcfMessageInfo& out_info, uint64_t& out_message_timestamp) noexcept { - uint8_t hdr[kAcfCommonHeaderLen]; - std::copy(in16, in16 + kAcfHeaderQuadletLen, hdr); - std::copy(in16 + kAcfGbbSecondQuadletOffset, - in16 + kAcfGbbSecondQuadletOffset + kAcfHeaderQuadletLen, - hdr + kAcfHeaderQuadletLen); - decode_acf_message_info(hdr, out_info); + decode_acf_message_info(in16, out_info); out_message_timestamp = avtp::detail::get_u64(in16 + kAcfGbbTimestampOffset); } // ── ACF_ABB / ACF_GBB message encode / decode ───────────────────────────────── // ACF_ABB carries no timestamp field at all; ACF_GBB always reserves a -// 64-bit message_timestamp slot inside the Message Info block (spliced -// between the block's two header quadlets, see above), regardless of -// whether `mtv` marks it valid (extraction §2.3, §2.7). +// 64-bit message_timestamp slot immediately after the Message Info header +// (contiguous, see above), regardless of whether `mtv` marks it valid +// (extraction §2.3, §2.7). // peek_acf_msg_type reads the 7-bit acf_msg_type field out of a raw buffer's // first octet without decoding the rest of the header. Needed because @@ -407,6 +389,42 @@ inline uint8_t peek_acf_msg_type(const uint8_t* b) noexcept { return static_cast(b[0] >> 1); } +// peek_msg_type is peek_acf_msg_type's checked, whole-buffer-validating +// counterpart (ported from c-RCP's rcp_acf_peek_msg_type()): it also +// validates `len >= 1` itself, rather than requiring the caller to do so +// before calling peek_acf_msg_type() directly. +inline std::error_code peek_msg_type(const uint8_t* b, size_t len, uint8_t& out_msg_type) noexcept { + if (len < 1) return avtp::make_error_code(avtp::AvtpErrc::short_buffer); + out_msg_type = peek_acf_msg_type(b); + return {}; +} + +// pad_len (ported from c-RCP's rcp_acf_pad_len()) returns the number of +// zero pad octets (0-3) needed to bring `unpadded_len` octets of +// header(+timestamp)+payload up to a whole number of quadlets — the unit +// acf_msg_length is expressed in. A pure, additive utility: encode_acf_abb() +// /encode_acf_gbb() below do NOT call this automatically (see this file's +// own header comment for why — rcp/e2e.hpp's caller-owns-padding contract), +// but a caller that wants c-RCP's own auto-pad accounting for a payload it +// is about to hand to encode_acf_abb()/_gbb() can compute it here first. +inline uint8_t pad_len(size_t unpadded_len) noexcept { + return static_cast((4u - (unpadded_len % 4u)) % 4u); +} + +// acf_msg_length is a 9-bit quadlet count (Table 4) — the largest ACF +// message (header/timestamp + payload + pad) this codec can represent. +// Ported from c-RCP's RCP_ACF_MAX_QUADLETS/RCP_ACF_ABB_MAX_PAYLOAD/ +// RCP_ACF_GBB_MAX_PAYLOAD. Informational only: encode_acf_abb()/ +// encode_acf_gbb() below mask acf_msg_length to 9 bits on encode (see +// detail::kMsgLengthMask) rather than rejecting an oversized payload +// outright, matching this codec's existing "always returns bytes, never an +// error code" contract for those two functions (see this file's header +// comment) — a caller that must not silently wrap can check a payload's +// size against these bounds itself before encoding. +constexpr uint16_t kAcfMaxQuadlets = 0x1FFu; +constexpr size_t kAcfAbbMaxPayload = static_cast(kAcfMaxQuadlets) * 4u - kAcfCommonHeaderLen; +constexpr size_t kAcfGbbMaxPayload = static_cast(kAcfMaxQuadlets) * 4u - kAcfGbbMessageInfoLen; + inline std::vector encode_acf_abb(AcfMessageInfo info, const std::vector& payload) { info.acf_msg_type = kAcfMsgTypeAbb; @@ -428,6 +446,29 @@ inline std::vector encode_acf_abb(AcfMessageInfo info, return buf; } +// TODO(phase1-followup): c-RCP's rcp_acf_decode_abb()/_decode_gbb() are +// stricter than this pair: they treat the decoded acf_msg_length*4 as the +// message's authoritative byte length (rejecting a buffer shorter than +// that declared length, RCP_ACF_ERR_SHORT_FRAME) and trim `pad` trailing +// octets off of *out_payload_len so a caller never sees pad bytes as +// payload. This codec deliberately keeps its existing, more lenient +// contract instead — payload is simply "everything from the header to the +// end of the buffer given" — because it is load-bearing for callers +// outside this pass's Phase 17 scope (acf.hpp/avtp.hpp only): every real +// payload builder in this tree (rcp/gpio.hpp, rcp/pwm.hpp, rcp/spi.hpp, +// etc., dispatched through rcp/mock.hpp/rcp/l2.hpp/rcp/udp.hpp) hands +// encode_acf_abb()/_gbb() an arbitrary-length payload without pre-padding +// it to a quadlet boundary or setting AcfMessageInfo::pad, so +// compute_acf_msg_length()'s ceiling-rounded acf_msg_length is not +// generally byte-exact for those callers' frames — enforcing it strictly +// on decode would reject frames this library's own encoders legitimately +// produce today. tests/test_e2e.cpp also asserts today that decode does +// NOT trim pad (its "ACF_GBB full-message layout..." test checks +// `decoded_payload == payload` where `payload` still includes the literal +// trailing pad byte). Revisit this once a later phase makes every payload +// builder in this tree pre-pad via pad_len() (added above) and sets `pad` +// accordingly, at which point c-RCP's stricter decode can be adopted +// without rejecting frames this library itself still emits. inline std::error_code decode_acf_abb(const uint8_t* b, size_t len, AcfMessageInfo& out_info, std::vector& out_payload) { @@ -524,9 +565,10 @@ inline std::error_code decode_acf_messages(const uint8_t* b, size_t len, std::ve break; } - // For ACF_GBB the fixed part is the whole 16-byte spliced Message - // Info block (quadlet0 + timestamp + quadlet1); for ACF_ABB it is - // the contiguous 8-byte header. + // For ACF_GBB the fixed part is the whole 16-byte Message Info + // block (8-byte header + 8-byte message_timestamp, contiguous — + // see the "ACF_GBB Message Info wire geometry" section above); for + // ACF_ABB it is just the 8-byte header. const size_t fixed_len = is_gbb ? kAcfGbbMessageInfoLen : kAcfCommonHeaderLen; if (remaining < fixed_len) { if (out.empty()) return avtp::make_error_code(avtp::AvtpErrc::short_buffer); @@ -674,6 +716,282 @@ inline std::vector encode_error_payload(WireErrorCode code) { return {static_cast(code)}; } +// ── Response builders (ported from c-RCP's rcp_acf_build_error_response()/ +// _build_acknowledge_response()/_build_acknowledge_rejected_response()) ────── +// These build a complete, ready-to-send ACF_ABB frame from just +// (byte_bus_id, transaction_num[, error code]) — the minimum a caller +// answering a request already knows — rather than requiring the caller to +// first assemble an AcfMessageInfo (make_response() above still exists for +// that lower-level use). All three encode as ACF_ABB (no timestamp), +// matching c-RCP's own ABB/GBB-split convention; a caller needing a +// timestamped variant builds its own ACF_GBB header with these same field +// values and calls encode_acf_gbb() directly. + +// build_error_response builds a TC18 §12.9.6 Error Response: "The error +// response shall contain the byte_bus_id and transaction number of the +// request. The error response shall contain a byte_msg_payload with an +// error code." evt = 0 (any value other than kEvtAcknowledge classifies the +// same way once err is set — see response_kind_of()'s own logic above), err +// = true, rsp = true. op is set true (c-RCP's RCP_ACF_OP_NONE, which always +// wire-encodes as the write/no-data-response bit — see AcfMessageInfo::op's +// own doc comment) to match c-RCP's rcp_acf_build_error_response() bit for +// bit, even though op does not affect response_kind_of()'s classification +// once err is set. +inline std::vector build_error_response(avtp::ByteBusId byte_bus_id, + uint8_t transaction_num, + WireErrorCode error_code) { + AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.transaction_num = transaction_num; + info.op = true; + info.rsp = true; + info.err = true; + return encode_acf_abb(info, encode_error_payload(error_code)); +} + +// evt_requests_acknowledge: TC18 §13.5's own opening statement, before its +// per-endpoint-type evt[2:0] table: "evt[3] is used to request an +// acknowledge. I.e. evt[3]=1 requests acknowledge." Universal across every +// endpoint type (unlike evt[2:0], which is per-endpoint-type — see +// evt_row2_is_plain() below). +inline bool evt_requests_acknowledge(uint8_t evt) noexcept { + return (evt & 0x08u) != 0u; +} + +// build_acknowledge_response builds a genuine Acknowledge +// (ResponseKind::Acknowledge, evt[3:0] == kEvtAcknowledge) for a request +// whose own evt[3] asked for one (evt_requests_acknowledge()) and that was +// accepted into request storage — err = false, per TC18 §12.3.1.3 ("if +// requested an acknowledge is sent after storing the request"). +inline std::vector build_acknowledge_response(avtp::ByteBusId byte_bus_id, + uint8_t transaction_num) { + AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.transaction_num = transaction_num; + info.evt_ack = true; + info.evt_op = detail::kEvtOpMask; // together, evt[3:0] == 0xF == kEvtAcknowledge + info.op = true; // c-RCP's RCP_ACF_OP_NONE — see build_error_response()'s doc comment + info.rsp = true; + return encode_acf_abb(info, {}); +} + +// build_acknowledge_rejected_response builds TC18 §11.3.1's OTHER +// Acknowledge shape, distinct from build_acknowledge_response() above: same +// evt[3:0] == kEvtAcknowledge, but for a request that was never filed into +// request storage at all — "err = 1 indicates that the request has been +// rejected. The byte_msg_payload contains an error code." This is NOT the +// same wire shape as build_error_response()'s §11.3.4 Error Response +// (evt[3:0] < 0x9, err = 1): that shape is for a request already accepted +// whose later execution fails; this one is for admission itself refusing to +// file the request (e.g. request-store full, a malformed opcode). Both +// shapes decode with err=1, but only this function's evt=0xF makes +// response_kind_of() classify the response as Acknowledge rather than +// ErrorResponse. +inline std::vector build_acknowledge_rejected_response(avtp::ByteBusId byte_bus_id, + uint8_t transaction_num, + WireErrorCode error_code) { + AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.transaction_num = transaction_num; + info.evt_ack = true; + info.evt_op = detail::kEvtOpMask; // together, evt[3:0] == 0xF == kEvtAcknowledge + info.op = true; // c-RCP's RCP_ACF_OP_NONE — see build_error_response()'s doc comment + info.rsp = true; + info.err = true; + return encode_acf_abb(info, encode_error_payload(error_code)); +} + +// ── Request-side header validation (ported from c-RCP's +// rcp_acf_request_header_constraints_valid()/_header_is_request()) ─────────── + +// header_is_request: TC18's own rsp field description (Table 4) states +// rsp=1b identifies a response; a decoded message with rsp=1 must not be +// admitted as a request. A caller decoding an inbound frame it intends to +// treat as a request should call this before admission and refuse the +// frame if it returns false. +inline bool header_is_request(const AcfMessageInfo& hdr) noexcept { + return !hdr.rsp; +} + +// request_header_constraints_valid: true iff hdr's hs/rsp/err fields are +// the fixed value TC18 requires on an encoded REQUEST: hs=false, rsp=false, +// err=false unconditionally, and cs=false UNLESS cs_has_meaning is true — +// compound-wait (TC18 §11.2.2.3 Table 8) and chained (§11.2.2.6 Table 11) +// are the only two request kinds that assign cs a meaning of its own, so a +// caller building one of those two kinds passes true; every other request +// kind passes false. A pure, directly-testable validator, not an +// encode_acf_abb()/_gbb()-time enforcement — those two functions are shared +// by request AND response encoding (e.g. build_error_response() above +// deliberately sets rsp=err=true), so they cannot force these fields to +// their request-only values unconditionally. +inline bool request_header_constraints_valid(const AcfMessageInfo& hdr, bool cs_has_meaning) noexcept { + if (hdr.hs) return false; + if (hdr.rsp) return false; + if (hdr.err) return false; + if (!cs_has_meaning && hdr.cs) return false; + return true; +} + +// ── TC18 §13.5 Table 33's shared evt[2:0] rule for the {ADC, PWM_IN, I2C, +// LIN, CAN, UART, ISELED, MDIO} endpoint-type row ───────────────────────────── +// evt[2:0] = 000b is the only value a plain (non-configuration) request in +// this row may carry — every other value is either reserved (001b-110b, +// request shall be rejected with error code UNSUPPORTED_CMD) or selects an +// entirely different, configuration-write-shaped request (111b, TC18 +// §12.7.1 Figure 18) that a plain read/write decoder should never accept. +// Not meaningful for SPI or GPIO/PWM_OUT, which have their own dedicated +// Table 33 rows with their own distinct rules. +inline bool evt_row2_is_plain(uint8_t evt) noexcept { + return (evt & 0x7u) == 0u; +} + +// ── TC18 §13.5.1: compound-wait's own, endpoint-type-independent evt[2:0] +// rule ───────────────────────────────────────────────────────────────────── +// compound-wait gives evt[2:0] an entirely different meaning than Table 33 +// gives it for a Standard request: it selects one of eight ways to compare +// that request's own byte_msg_payload against the addressed endpoint's +// current status, and this rule is the SAME across every endpoint type — +// unlike Table 33, there is no per-endpoint-type row. + +namespace detail { +constexpr uint8_t kCompoundWaitModeExact = 0x0u; +constexpr uint8_t kCompoundWaitModeAndOnes = 0x1u; +constexpr uint8_t kCompoundWaitModeAndZeros = 0x2u; +constexpr uint8_t kCompoundWaitModeReserved = 0x3u; +constexpr uint8_t kCompoundWaitModeHiGe = 0x4u; +constexpr uint8_t kCompoundWaitModeHiLe = 0x5u; +constexpr uint8_t kCompoundWaitModeLoGe = 0x6u; +constexpr uint8_t kCompoundWaitModeLoLe = 0x7u; + +inline uint16_t be16(const uint8_t* p) noexcept { + return static_cast((static_cast(p[0]) << 8) | p[1]); +} +} // namespace detail + +// compound_wait_evt_valid: true iff (evt & 0x7) != 0x3 — every value except +// the reserved 011b, which callers must reject with error code +// UNSUPPORTED_CMD rather than passing to compound_wait_match() below (that +// function's return value for a reserved evt is not a meaningful +// "never matches" result — see its own doc comment). +inline bool compound_wait_evt_valid(uint8_t evt) noexcept { + return (evt & 0x7u) != detail::kCompoundWaitModeReserved; +} + +// compound_wait_match evaluates whether payload[0..payload_len) matches +// status[0..status_len) under the comparison mode evt[2:0] selects. Callers +// must call compound_wait_evt_valid(evt) first and reject a false result +// (UNSUPPORTED_CMD) rather than calling this function — its own return +// value for evt[2:0] == 011b is unconditionally false, not a meaningful +// "reserved" signal distinct from a real non-match. +// +// Length rule (applies before any mode-specific comparison, per the +// specification's own wording and its own SPI example — "only the first +// four out of 20 received bytes will be checked when the byte_msg_payload +// in the compound wait has only four bytes"): if status_len < payload_len +// the condition never matches (false, regardless of mode and buffer +// contents); otherwise status is compared only against its own first +// payload_len bytes. +// +// Modes (evt[2:0]): +// 000b exact match: payload[0..n) == status[0..n), byte for byte. +// 001b AND-with-1s-mask: for every byte i, (payload[i] & status[i]) | +// ~payload[i] == 0xFF — every payload bit that +// is 1 must also be 1 in status. +// 010b AND-with-0s-mask: for every byte i, payload[i] & status[i] == +// 0x00 — every payload bit that is 1 must be 0 +// in status. +// 100b/101b: the first two bytes of payload's own leading quadlet, read +// big-endian, are >= (100b) or <= (101b) the same two bytes of +// status. Returns false (never reads OOB) if payload_len < 4. +// 110b/111b: same as 100b/101b, but the LAST two bytes of the leading +// quadlet (payload[2..4)), same payload_len < 4 fail-safe. +// +// status/payload may be nullptr iff their respective length is 0. +inline bool compound_wait_match(uint8_t evt, const uint8_t* payload, size_t payload_len, + const uint8_t* status, size_t status_len) noexcept { + const uint8_t mode = static_cast(evt & 0x7u); + + if (status_len < payload_len) return false; + + switch (mode) { + case detail::kCompoundWaitModeExact: + if (payload_len == 0u) return true; + return std::equal(payload, payload + payload_len, status); + + case detail::kCompoundWaitModeAndOnes: + for (size_t i = 0; i < payload_len; ++i) { + const uint8_t v = static_cast((payload[i] & status[i]) | static_cast(~payload[i])); + if (v != 0xFFu) return false; + } + return true; + + case detail::kCompoundWaitModeAndZeros: + for (size_t i = 0; i < payload_len; ++i) { + if (static_cast(payload[i] & status[i]) != 0x00u) return false; + } + return true; + + case detail::kCompoundWaitModeHiGe: + case detail::kCompoundWaitModeHiLe: + if (payload_len < 4u) return false; + return (mode == detail::kCompoundWaitModeHiGe) ? (detail::be16(payload) >= detail::be16(status)) + : (detail::be16(payload) <= detail::be16(status)); + + case detail::kCompoundWaitModeLoGe: + case detail::kCompoundWaitModeLoLe: + if (payload_len < 4u) return false; + return (mode == detail::kCompoundWaitModeLoGe) ? (detail::be16(&payload[2]) >= detail::be16(&status[2])) + : (detail::be16(&payload[2]) <= detail::be16(&status[2])); + + case detail::kCompoundWaitModeReserved: + default: + return false; + } +} + +// ── REQ-RMAP-069 (TC18 §13.7.1.2): EP0 register-write effective length ────── +// "Effective number of bytes to be written to register map = (acf_msg_length +// - 3) x 4 - pad - 2." acf_msg_length/pad are the decoded header fields of +// the same name; this function does no decoding of its own. FIXED per c-RCP +// (spec rebaseline to TC18 0.5.1_RC5, 2026-08-11): the 0.5.1_RC baseline's +// formula omitted the trailing "- 2" term (the 2-octet register start +// address that leads the byte payload); RC5 corrects it. Returns 0, never +// underflowing to a huge size_t, if acf_msg_length is too small to contain +// the fixed 3-quadlet region at all (< 3), or if pad plus the 2-octet +// address exceeds what remains after subtracting it — both describe a +// malformed or adversarial frame, and 0 effective data octets is this +// function's own fail-safe reading of that, not an out-of-band error code. +inline size_t reg_write_len(uint16_t acf_msg_length, uint8_t pad) noexcept { + if (acf_msg_length < 3u) return 0; + const size_t total_octets = static_cast(acf_msg_length - 3u) * 4u; + const size_t overhead = static_cast(pad) + 2u; + if (overhead > total_octets) return 0; + return total_octets - overhead; +} + +// ── Peeking a GBB frame's own request_type without a full kind-specific +// decode ───────────────────────────────────────────────────────────────────── +// Every conditional-request module (compound/triggered/chained/timed) +// places its own request_type opcode at the SAME fixed offset: octet 0 of +// the 8-byte message_timestamp region (frame offset kAcfCommonHeaderLen, +// i.e. 8 — see the "ACF_GBB Message Info wire geometry" section above), +// repurposed identically by every one of those modules. Returns true and +// sets `out_request_type` to frame[8] iff frame_len >= 9 and the header's +// own acf_msg_type is kAcfMsgTypeGbb; returns false (`out_request_type` +// left unchanged) for an ACF_ABB frame (no request_type concept exists on +// that wire shape) or a frame too short to hold byte_message_info(8) + +// request_type(1). Does NOT itself validate that the returned byte is one +// of the currently-defined request_type values. +inline bool peek_gbb_request_type(const uint8_t* frame, size_t frame_len, + uint8_t& out_request_type) noexcept { + if (frame_len < 9u) return false; + AcfMessageInfo hdr; + decode_acf_message_info(frame, hdr); + if (hdr.acf_msg_type != kAcfMsgTypeGbb) return false; + out_request_type = frame[kAcfCommonHeaderLen]; + return true; +} + // ── Timestamp fallback rules ────────────────────────────────────────────────── // avtp_timestamp (TSCF-only, 32-bit, rcp/avtp.hpp) and message_timestamp // (ACF_GBB-only, 64-bit, valid only when `mtv` is set) are two independent, diff --git a/include/rcp/avtp.hpp b/include/rcp/avtp.hpp index 96da7c2..cf5fb51 100644 --- a/include/rcp/avtp.hpp +++ b/include/rcp/avtp.hpp @@ -242,18 +242,47 @@ using ByteBusId = uint16_t; // bytes20-21 stream_data_length(octets) — 16 bits; this is // TscfHeader::control_data_length's wire position // bytes22-23 reserved, always 0 +// sv/version below are additive fields (Phase 17, ported from c-RCP's +// rcp_avtp_ntscf_header_t/rcp_avtp_tscf_header_t, which round-trip both +// through encode/decode rather than hardcoding them). Their defaults (true +// / 0) reproduce this codec's own pre-existing hardcoded encode behavior +// exactly, so no existing caller that leaves them at their defaults sees +// any wire change. struct NtscfHeader { + bool sv = true; // stream_id valid; TC18 always sets this for NTSCF + uint8_t version = 0; // AVTP version; always 0 in this spec revision StreamId stream_id{}; uint8_t sequence_num = 0; // per-stream_id AVTPDU counter, 8-bit rolling uint16_t control_data_length = 0; // byte length of the ACF message that follows; wire field is 11 bits (0–2047) }; +// mr/tu/reserved0/reserved1 below are additive fields (Phase 17, ported +// from c-RCP's rcp_avtp_tscf_header_t). mr and tv/tu are round-tripped +// through encode/decode; reserved0/reserved1 are decode-only (encode always +// zero-fills bytes 16-19/22-23 on the wire regardless of what a caller sets +// here, matching c-RCP's rcp_avtp_encode_tscf() and this struct's own +// pre-existing control_data_length/ntscf_data_length "derived, never +// trusted, on encode" convention) — a genuinely nonzero reserved0/reserved1 +// after decode can only come from a real (possibly non-conformant, or +// future-revision) wire frame; see tscf_reserved_all_zero() below for TC18 +// §13.3's own rule built on top of them. struct TscfHeader { + bool sv = true; // stream_id valid; always 1 for TC18 use + uint8_t version = 0; // AVTP version; always 0 in this spec revision + bool mr = false; // media clock restart; round-tripped, no RCP-specific behavior StreamId stream_id{}; uint8_t sequence_num = 0; // per-stream_id AVTPDU counter, 8-bit rolling uint16_t control_data_length = 0; // byte length of the ACF message that follows; wire field is a full 16 bits bool timestamp_valid = false; // "tv" — whether avtp_timestamp below is meaningful uint32_t avtp_timestamp = 0; // 32-bit; TSCF-only (extraction §2.6) + // "tu" — avtp_timestamp uncertain. TC18 §13.3's third rule: "In case + // the time stamp is uncertain (i.e. tu = 1), then this shall be + // executed as if tu = 0" — this codec does not itself branch on tu + // anywhere; it is decoded purely so a caller can still inspect the + // wire value (diagnostics, a future revision that does need it). + bool tu = false; + uint32_t reserved0 = 0; // bytes 16-19; decode-only, see struct comment above + uint16_t reserved1 = 0; // bytes 22-23; decode-only, see struct comment above }; constexpr size_t kNtscfHeaderLen = 12; // 1 subtype + 1 flags/length-hi + 1 length-lo + 1 seq + 8 stream_id @@ -262,6 +291,8 @@ constexpr size_t kTscfHeaderLen = 24; // kNtscfHeaderLen + 4 avtp_timestamp + 4 namespace detail { constexpr uint8_t kFlagStreamValid = 0x80; // "sv" — always set for these control formats, byte1 bit7 in both headers constexpr uint8_t kFlagTimestampValid = 0x01; // "tv" — TSCF only, byte1 bit0 (NOT bit6 — see header comment above) +constexpr uint8_t kFlagMediaRestart = 0x08; // "mr" — TSCF only, byte1 bit3 +constexpr uint8_t kFlagTimestampUncertain = 0x01; // "tu" — TSCF only, byte3 bit0 constexpr uint16_t kNtscfDataLengthMask = 0x07FF; // ntscf_data_length is 11 bits wide } // namespace detail @@ -269,7 +300,9 @@ inline std::vector encode_ntscf_header(const NtscfHeader& h) { std::vector buf(kNtscfHeaderLen, 0); const uint16_t data_len = static_cast(h.control_data_length & detail::kNtscfDataLengthMask); buf[0] = kSubtypeNtscf; - buf[1] = static_cast(detail::kFlagStreamValid | ((data_len >> 8) & 0x07)); + buf[1] = static_cast((h.sv ? detail::kFlagStreamValid : 0) | + ((h.version & 0x07) << 4) | + ((data_len >> 8) & 0x07)); buf[2] = static_cast(data_len & 0xFF); buf[3] = h.sequence_num; detail::put_u64(&buf[4], h.stream_id.to_u64()); @@ -280,6 +313,8 @@ inline std::error_code decode_ntscf_header(const uint8_t* b, size_t len, NtscfHe if (len < 1) return make_error_code(AvtpErrc::short_buffer); if (b[0] != kSubtypeNtscf) return make_error_code(AvtpErrc::bad_subtype); if (len < kNtscfHeaderLen) return make_error_code(AvtpErrc::short_buffer); + out.sv = (b[1] & detail::kFlagStreamValid) != 0; + out.version = static_cast((b[1] >> 4) & 0x07); out.control_data_length = static_cast(((b[1] & 0x07) << 8) | b[2]); out.sequence_num = b[3]; out.stream_id = StreamId::from_u64(detail::get_u64(&b[4])); @@ -289,15 +324,18 @@ inline std::error_code decode_ntscf_header(const uint8_t* b, size_t len, NtscfHe inline std::vector encode_tscf_header(const TscfHeader& h) { std::vector buf(kTscfHeaderLen, 0); buf[0] = kSubtypeTscf; - buf[1] = static_cast(detail::kFlagStreamValid | + buf[1] = static_cast((h.sv ? detail::kFlagStreamValid : 0) | + ((h.version & 0x07) << 4) | + (h.mr ? detail::kFlagMediaRestart : 0) | (h.timestamp_valid ? detail::kFlagTimestampValid : 0)); buf[2] = h.sequence_num; - // buf[3] (reserved + tu) stays 0. + buf[3] = static_cast(h.tu ? detail::kFlagTimestampUncertain : 0); detail::put_u64(&buf[4], h.stream_id.to_u64()); detail::put_u32(&buf[12], h.avtp_timestamp); - // buf[16..19] (reserved) stays 0. + // buf[16..19] (reserved0) stays 0 — always zero-filled on encode, + // regardless of h.reserved0 (decode-only field, see struct comment). detail::put_u16(&buf[20], h.control_data_length); - // buf[22..23] (reserved) stays 0. + // buf[22..23] (reserved1) stays 0 — same rule as reserved0 above. return buf; } @@ -305,13 +343,117 @@ inline std::error_code decode_tscf_header(const uint8_t* b, size_t len, TscfHead if (len < 1) return make_error_code(AvtpErrc::short_buffer); if (b[0] != kSubtypeTscf) return make_error_code(AvtpErrc::bad_subtype); if (len < kTscfHeaderLen) return make_error_code(AvtpErrc::short_buffer); + out.sv = (b[1] & detail::kFlagStreamValid) != 0; + out.version = static_cast((b[1] >> 4) & 0x07); + out.mr = (b[1] & detail::kFlagMediaRestart) != 0; out.timestamp_valid = (b[1] & detail::kFlagTimestampValid) != 0; out.sequence_num = b[2]; + out.tu = (b[3] & detail::kFlagTimestampUncertain) != 0; out.stream_id = StreamId::from_u64(detail::get_u64(&b[4])); out.avtp_timestamp = detail::get_u32(&b[12]); + out.reserved0 = detail::get_u32(&b[16]); out.control_data_length = detail::get_u16(&b[20]); + out.reserved1 = detail::get_u16(&b[22]); return {}; } +// ── REQ-TIMED-012, TC18 §11.2/§11.2.1: 48-bit gPTP-domain reconstruction of +// a TSCF avtp_timestamp ──────────────────────────────────────────────────── +// "If received under TSCF header, [a request's] execution is postponed +// until the presentation time has occurred" — a rule that applies to every +// request kind, not just a timed-request-specific one. Evaluating it means +// comparing avtp_timestamp (this header's own 32-bit, nanoseconds-modulo- +// 2^32 IEEE 1722 field) against a 48-bit gPTP-domain clock — but a 32-bit +// field cannot itself carry which of the (2^48 / 2^32) possible 48-bit +// instants congruent to it mod 2^32 was actually intended, and IEEE 1722 +// leaves that reconstruction to the receiver. +// +// extend_timestamp resolves that ambiguity the same way every real +// AVTP/gPTP receiver does (standard IEEE 1722 presentation-time +// reconstruction, ported from c-RCP's rcp_avtp_extend_timestamp()): of the +// several 48-bit instants congruent to wire_ts modulo 2^32, it returns +// whichever is CLOSEST to reference_now. Naively zero-extending wire_ts (OR +// -ing it onto reference_now's own high bits, unadjusted) is wrong whenever +// wire_ts's low bits happen to be numerically smaller than reference_now's +// — that reads a request meant for ~100ms in the future as ~4.29 seconds +// (2^32 ns) in the past instead. +// +// The result is intended to be computed ONCE, at admission time +// (reference_now = the current gPTP-domain clock at that moment), then +// compared on every later tick against that same fixed result — +// reference_now is a resolution anchor, not something the caller +// re-supplies per tick. +inline uint64_t extend_timestamp(uint32_t wire_ts, uint64_t reference_now) noexcept { + constexpr uint64_t period = uint64_t{1} << 32; + constexpr uint64_t half = period / 2; + const uint64_t base = reference_now & ~(period - 1); + uint64_t candidate = base | uint64_t{wire_ts}; + + if (candidate > reference_now && (candidate - reference_now) > half) { + // candidate is more than half a period ahead of reference_now — + // the instant one period earlier is the closer match. + candidate -= period; + } else if (candidate < reference_now && (reference_now - candidate) > half) { + // Symmetric case: one period later is closer. + candidate += period; + } + return candidate; +} + +// ── Subtype dispatch & the TSCF-without-time-sync drop rule ──────────────── +// Ported from c-RCP's rcp_avtp_should_drop_tscf()/_tscf_reserved_all_zero(), +// both governed by TC18 §13.3's own configurable disposition for a +// TSCF-headed AVTPDU an RC Server cannot (or chooses not to) honor the +// ordinary way. + +// peek_subtype reads just the subtype byte (offset 0) from a received +// AVTPDU, so a caller can decide which of decode_ntscf_header()/ +// decode_tscf_header() to invoke without a full decode attempt first. +inline std::error_code peek_subtype(const uint8_t* b, size_t len, uint8_t& out_subtype) noexcept { + if (len < 1) return make_error_code(AvtpErrc::short_buffer); + out_subtype = b[0]; + return {}; +} + +// TscfFallback selects between TC18 §11.1's unconditional wording ("AVTPDUs +// having a TSCF header are dropped, and no response send") and §13.3's own +// alternative, more specific, explicitly configurable rule ("...or dropped, +// depending on the configuration of the RC Server") for a TSCF-headed +// AVTPDU an RC Server cannot honor the ordinary way — shared by +// should_drop_tscf()'s unsupported-time-sync rule and +// tscf_reserved_all_zero()'s reserved-bytes rule below, since both of +// §13.3's own sentences describing them have the identical "...or dropped, +// depending on the configuration..." shape. Drop (0) reproduces this +// codec's own original, unconditional-drop disposition — a caller that +// does not opt in sees no behavior change. +enum class TscfFallback : uint8_t { + Drop = 0, // drop the frame outright (this codec's default disposition) + Ignore = 1, // ignore the TSCF-specific semantics that could not be + // honored and process the request as if no presentation + // time were included +}; + +// REQ-AVTP-014/021: true iff `subtype` is kSubtypeTscf, time sync is not +// supported, and `unsupported_time_sync_policy` is TscfFallback::Drop. +// Returns false for every subtype other than kSubtypeTscf, regardless of +// server_time_sync_supported or unsupported_time_sync_policy — this rule is +// TSCF-only. +inline bool should_drop_tscf(bool server_time_sync_supported, uint8_t subtype, + TscfFallback unsupported_time_sync_policy) noexcept { + if (subtype != kSubtypeTscf) return false; + if (!server_time_sync_supported) return unsupported_time_sync_policy == TscfFallback::Drop; + return false; +} + +// REQ-AVTP-022, TC18 §13.3's second configurable rule: "If the reserved +// bytes in the header are all zero, then the request shall be queued as if +// the header was in NTSCF format or dropped, depending on configuration." +// Returns true iff hdr's own reserved0/reserved1 are both zero — callers +// combine this with a caller-owned TscfFallback exactly the way +// should_drop_tscf()'s own unsupported_time_sync_policy parameter is used. +inline bool tscf_reserved_all_zero(const TscfHeader& hdr) noexcept { + return hdr.reserved0 == 0 && hdr.reserved1 == 0; +} + } // namespace avtp } // namespace rcp diff --git a/tests/test_acf.cpp b/tests/test_acf.cpp index 9a70e83..6def525 100644 --- a/tests/test_acf.cpp +++ b/tests/test_acf.cpp @@ -92,54 +92,33 @@ TEST_CASE("ACF_GBB round-trips a 64-bit message_timestamp alongside the shared h } // ── Hand-computed expected-byte-sequence vectors ────────────────────────────── -// Every byte below is computed by hand from the field values and this file's -// own derived bit layout (see acf.hpp's "ACF shared header" comment) — not -// copied from anywhere, and in particular not read back out of the +// Every byte below is computed by hand from the field values and this +// file's own derived bit layout (see acf.hpp's "ACF shared header" comment) +// — not copied from anywhere, and in particular not read back out of the // encoder's own output (which would only prove the encoder agrees with // itself). Byte-by-byte derivation (MSB-first bit numbering, bit0 = MSB of -// byte0, matching the specification's own diagrams): +// byte0): // -// Shared-header quadlet 0 (wire octets 0..3 of every ACF message): +// byte_message_info, octets 0..7 (identical layout for ACF_ABB and +// ACF_GBB — see acf.hpp's "ACF shared header" comment): // byte0 = (acf_msg_type[6:0] << 1) | acf_msg_length[8] // byte1 = acf_msg_length[7:0] // byte2 = (pad[1:0] << 6) | (mtv << 5) | (rsv=00 << 3) | byte_bus_id[10:8] // byte3 = byte_bus_id[7:0] -// Shared-header quadlet 1: -// byte0 = (evt[3:0] << 4) | (rsv=00 << 2) | (hs << 1) | cs +// byte4 = (evt[3:0] << 4) | (rsv=00 << 2) | (hs << 1) | cs // where evt[3:0] = (evt_ack << 3) | evt_op[2:0] -// byte1 = transaction_num -// byte2 = (op << 7) | (rsp << 6) | (err << 5) | (ms << 4) | read_size[11:8] -// byte3 = read_size[7:0] +// byte5 = transaction_num +// byte6 = (op << 7) | (rsp << 6) | (err << 5) | (ms << 4) | read_size[11:8] +// byte7 = read_size[7:0] // -// The two quadlets' *positions* differ by message type, and this is the -// one thing that must be pinned from the specification rather than from -// this codec (see acf.hpp's kAcfGbbTimestampOffset comment block for the -// full verification, summarized here): -// -// ACF_ABB — no message_timestamp field exists at all: -// octets 0..3 quadlet 0 -// octets 4..7 quadlet 1 -// octets 8.. byte_msg_payload -// -// ACF_GBB — the specification's single-ACF_GBB CRC-coverage figure draws -// one "Byte Message Info" group whose three rows are, in order, quadlet -// 0, then message_time_stamp as a double-height 64-bit block, then -// quadlet 1; its compound-request figure (an mtv=0 ACF_GBB) likewise -// puts the fields that repurpose the timestamp slot between the same two -// quadlets; and its response-field table lists message_timestamp -// ("Present in ACF_GBB, omitted in ACF_ABB") between byte_bus_id -// (quadlet 0's last field) and evt (quadlet 1's first field): -// octets 0..3 quadlet 0 -// octets 4..11 message_timestamp, big-endian -// octets 12..15 quadlet 1 -// octets 16.. byte_msg_payload -// Arithmetic cross-check from the same figure: it states -// acf_msg_length = 0x07 quadlets = 28 octets for a 7-real-byte, -// 1-pad-byte payload with a CRC32 trailer, and 4 + 8 + 4 + 8 + 4 = 28 -// only works out with the timestamp inside the Message Info block. (Its -// ACF_ABB counterpart states 0x05 = 20 octets: 4 + 4 + 8 + 4 = 20.) -// -// remaining bytes = payload, unchanged +// ACF_ABB has no message_timestamp field at all — byte_msg_payload begins +// immediately at octet 8. ACF_GBB appends the 64-bit message_timestamp +// immediately AFTER this same 8-byte header (octets 8..15), then +// byte_msg_payload at octet 16 — ported from c-RCP's acf.h/acf.c, this +// project's RC5-conformant reference for this module (see acf.hpp's own +// "ACF_GBB Message Info wire geometry" comment for the full derivation, +// including c-RCP's own test_peek_gbb_request_type() and its +// .fusa-reqs.json REQ-ACF-044 citation). TEST_CASE("ACF_ABB hand-computed expected byte sequence", "[acf][REQ-WIRE-004][REQ-WIRE-006]") { AcfMessageInfo info; @@ -201,57 +180,49 @@ TEST_CASE("ACF_GBB hand-computed expected byte sequence", "[acf][REQ-WIRE-005][R info.ms = true; info.read_size_or_segment_num = 4095; // 12 bits, max value: 0xFFF - // Quadlet 0 (wire octets 0..3): + // byte_message_info, octets 0..7 (contiguous — see this file's own + // layout comment above): // byte0 = (0x0D << 1) | (7 >> 8 & 1) = 0x1A | 0 = 0x1A // byte1 = 7 & 0xFF = 0x07 // byte2 = (1 << 6) | (1 << 5) | (300 >> 8 & 7) = 0x40 | 0x20 | 0x01 = 0x61 // byte3 = 300 & 0xFF = 0x2C - // Quadlet 1 (wire octets 12..15 for ACF_GBB — see this file's layout - // comment above; these are octets 4..7 only for ACF_ABB): - // byte0 = (0x3 << 4) | (0 << 1) | 1 = 0x30 | 0x01 = 0x31 - // byte1 = 200 = 0xC8 - // byte2 = 0 | (1<<6) | (1<<5) | (1<<4) | (4095 >> 8 & 0xF) = 0x40|0x20|0x10|0x0F = 0x7F - // byte3 = 4095 & 0xFF = 0xFF - const std::vector expected_q0 = {0x1A, 0x07, 0x61, 0x2C}; - const std::vector expected_q1 = {0x31, 0xC8, 0x7F, 0xFF}; + // byte4 = (0x3 << 4) | (0 << 1) | 1 = 0x30 | 0x01 = 0x31 + // byte5 = 200 = 0xC8 + // byte6 = 0 | (1<<6) | (1<<5) | (1<<4) | (4095 >> 8 & 0xF) = 0x40|0x20|0x10|0x0F = 0x7F + // byte7 = 4095 & 0xFF = 0xFF + const std::vector expected_header = {0x1A, 0x07, 0x61, 0x2C, 0x31, 0xC8, 0x7F, 0xFF}; const uint64_t ts = 0x1122334455667788ULL; const std::vector expected_ts = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}; const std::vector payload = {0xAA, 0xBB, 0xCC, 0xDD}; // The whole expected ACF_GBB message, written out as one literal at the // spec-derived octet positions rather than assembled from the encoder's - // own constants: quadlet 0 || message_timestamp || quadlet 1 || payload. + // own constants: byte_message_info || message_timestamp || payload. const std::vector expected_frame = { - // octets 0..3 — quadlet 0 - 0x1A, 0x07, 0x61, 0x2C, - // octets 4..11 — message_timestamp, big-endian (spliced *between* - // the two header quadlets, per the specification's - // single-ACF_GBB CRC-coverage figure and its - // compound-request figure) + // octets 0..7 — byte_message_info (contiguous, same layout ACF_ABB uses) + 0x1A, 0x07, 0x61, 0x2C, 0x31, 0xC8, 0x7F, 0xFF, + // octets 8..15 — message_timestamp, big-endian 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, - // octets 12..15 — quadlet 1 - 0x31, 0xC8, 0x7F, 0xFF, // octets 16..19 — byte_msg_payload 0xAA, 0xBB, 0xCC, 0xDD, }; auto frame = encode_acf_gbb(info, ts, payload); - REQUIRE(frame.size() == 20); // 4 + 8 + 4 + 4 + REQUIRE(frame.size() == 20); // 8 + 8 + 4 REQUIRE(frame == expected_frame); // Same assertion again, sliced field by field at literal offsets, so a // failure names which field moved rather than just "the buffer differs". - REQUIRE(std::vector(frame.begin() + 0, frame.begin() + 4) == expected_q0); - REQUIRE(std::vector(frame.begin() + 4, frame.begin() + 12) == expected_ts); - REQUIRE(std::vector(frame.begin() + 12, frame.begin() + 16) == expected_q1); + REQUIRE(std::vector(frame.begin() + 0, frame.begin() + 8) == expected_header); + REQUIRE(std::vector(frame.begin() + 8, frame.begin() + 16) == expected_ts); REQUIRE(std::vector(frame.begin() + 16, frame.end()) == payload); - // Regression guard for the pre-v2.22.0 layout specifically: back then - // octet 4 was transaction-info (quadlet 1's evt/hs/cs byte) and octet 8 - // was the timestamp's first byte. Those two octets are the cheapest - // possible discriminator between the two layouts. - REQUIRE(frame[4] == 0x11); // timestamp MSB, not 0x31 (quadlet 1 byte0) - REQUIRE(frame[8] == 0x55); // still inside the timestamp, not 0x31 either + // Regression guard for the spliced-layout bug this pass reverts: under + // that (wrong) layout, octet 4 held quadlet 1's evt/hs/cs byte (0x31) + // and octet 8 held the timestamp's first byte (0x11). Under the correct + // contiguous layout it's the other way around. + REQUIRE(frame[4] == 0x31); // still inside the contiguous header, not the timestamp + REQUIRE(frame[8] == 0x11); // timestamp MSB, right after the 8-byte header // Decoding the hand-written literal (not the encoder's output) must // recover every field — this is the direction that proves the decoder @@ -625,3 +596,403 @@ TEST_CASE("to_message carries message_timestamp only for ACF_GBB", "[acf][relay- auto gbb_msg = to_message(gbb_info, /*message_timestamp=*/0xDEADBEEF, {}); REQUIRE(gbb_msg.timestamp == 0xDEADBEEF); } + +// ── Phase 17 (c-RCP port): message-type constants & GBB/ABB header-length relation ── + +TEST_CASE("ACF_ABB/ACF_GBB message type wire values", "[acf]") { + REQUIRE(kAcfMsgTypeAbb == 0x0E); + REQUIRE(kAcfMsgTypeGbb == 0x0D); +} + +TEST_CASE("kAcfGbbMessageInfoLen is exactly kAcfCommonHeaderLen + 8", "[acf]") { + // The presence/absence of message_timestamp is the only structural + // difference between the two variants. + REQUIRE(kAcfCommonHeaderLen + 8 == kAcfGbbMessageInfoLen); +} + +// ── pad_len (ported from c-RCP's rcp_acf_pad_len()) ─────────────────────────── + +TEST_CASE("pad_len computes the octets needed to reach the next quadlet boundary", "[acf]") { + REQUIRE(pad_len(8) == 0); + REQUIRE(pad_len(9) == 3); + REQUIRE(pad_len(10) == 2); + REQUIRE(pad_len(11) == 1); + REQUIRE(pad_len(12) == 0); +} + +// ── acf_msg_length / payload bounds (ported from c-RCP's RCP_ACF_MAX_QUADLETS +// / RCP_ACF_ABB_MAX_PAYLOAD / RCP_ACF_GBB_MAX_PAYLOAD) ──────────────────────── + +TEST_CASE("kAcfAbbMaxPayload/kAcfGbbMaxPayload are derived from the 9-bit acf_msg_length field", "[acf]") { + REQUIRE(kAcfMaxQuadlets == 0x1FF); + REQUIRE(kAcfAbbMaxPayload == static_cast(kAcfMaxQuadlets) * 4 - kAcfCommonHeaderLen); + REQUIRE(kAcfGbbMaxPayload == static_cast(kAcfMaxQuadlets) * 4 - kAcfGbbMessageInfoLen); + REQUIRE(kAcfGbbMaxPayload < kAcfAbbMaxPayload); // GBB's fixed region is 8 bytes larger +} + +// ── peek_msg_type (ported from c-RCP's rcp_acf_peek_msg_type()) ────────────── + +TEST_CASE("peek_msg_type reads the first byte's acf_msg_type", "[acf]") { + AcfMessageInfo info; + auto frame = encode_acf_abb(info, {}); + uint8_t msg_type = 0; + REQUIRE_FALSE(peek_msg_type(frame.data(), frame.size(), msg_type)); + REQUIRE(msg_type == kAcfMsgTypeAbb); +} + +TEST_CASE("peek_msg_type rejects an empty buffer", "[acf]") { + uint8_t msg_type = 0; + REQUIRE(peek_msg_type(nullptr, 0, msg_type)); +} + +// ── header_is_request / request_header_constraints_valid (ported from c-RCP's +// rcp_acf_header_is_request()/_request_header_constraints_valid()) ─────────── + +TEST_CASE("header_is_request is true for rsp=0 and false for rsp=1", "[acf]") { + AcfMessageInfo hdr; + REQUIRE(header_is_request(hdr)); // rsp=0: a request + + hdr.rsp = true; + REQUIRE_FALSE(header_is_request(hdr)); // rsp=1: a response +} + +TEST_CASE("request_header_constraints_valid accepts a fresh, unmodified request header", "[acf]") { + AcfMessageInfo hdr; + REQUIRE(request_header_constraints_valid(hdr, /*cs_has_meaning=*/false)); +} + +TEST_CASE("request_header_constraints_valid rejects hs/rsp/err set on a request", "[acf]") { + AcfMessageInfo hs_hdr; + hs_hdr.hs = true; + REQUIRE_FALSE(request_header_constraints_valid(hs_hdr, false)); + + AcfMessageInfo rsp_hdr; + rsp_hdr.rsp = true; + REQUIRE_FALSE(request_header_constraints_valid(rsp_hdr, false)); + + AcfMessageInfo err_hdr; + err_hdr.err = true; + REQUIRE_FALSE(request_header_constraints_valid(err_hdr, false)); +} + +TEST_CASE("request_header_constraints_valid rejects cs=1 unless cs_has_meaning", "[acf]") { + AcfMessageInfo hdr; + hdr.cs = true; + REQUIRE_FALSE(request_header_constraints_valid(hdr, /*cs_has_meaning=*/false)); + REQUIRE(request_header_constraints_valid(hdr, /*cs_has_meaning=*/true)); +} + +// ── evt_row2_is_plain (TC18 §13.5 Table 33's ADC/PWM_IN/I2C/LIN/CAN/UART/ +// ISELED/MDIO row) ───────────────────────────────────────────────────────── + +TEST_CASE("evt_row2_is_plain is true only for evt[2:0] == 0", "[acf]") { + REQUIRE(evt_row2_is_plain(0x0)); + for (uint8_t v = 1; v <= 6; ++v) { + REQUIRE_FALSE(evt_row2_is_plain(v)); + } + REQUIRE_FALSE(evt_row2_is_plain(0x7)); // reserved config-write selector +} + +TEST_CASE("evt_row2_is_plain ignores evt[3] (the ack-request bit)", "[acf]") { + // evt[3] is outside evt[2:0]'s 3-bit scope — a request with evt[3] set + // but evt[2:0] = 000b is still plain. + REQUIRE(evt_row2_is_plain(0x8)); +} + +// ── evt_requests_acknowledge (TC18 §13.5: "evt[3] is used to request an +// acknowledge") ─────────────────────────────────────────────────────────── + +TEST_CASE("evt_requests_acknowledge reflects evt[3] regardless of evt[2:0]", "[acf]") { + REQUIRE_FALSE(evt_requests_acknowledge(0x00)); + REQUIRE_FALSE(evt_requests_acknowledge(0x07)); + REQUIRE(evt_requests_acknowledge(0x08)); + REQUIRE(evt_requests_acknowledge(0x0F)); +} + +// ── TC18 §13.5.1 compound-wait evt[2:0] comparison rule (ported from c-RCP's +// rcp_acf_compound_wait_evt_valid()/_compound_wait_match()) ────────────────── + +TEST_CASE("compound_wait_evt_valid is true for every mode but the reserved one", "[acf]") { + REQUIRE(compound_wait_evt_valid(0x0)); + REQUIRE(compound_wait_evt_valid(0x1)); + REQUIRE(compound_wait_evt_valid(0x2)); + REQUIRE(compound_wait_evt_valid(0x4)); + REQUIRE(compound_wait_evt_valid(0x5)); + REQUIRE(compound_wait_evt_valid(0x6)); + REQUIRE(compound_wait_evt_valid(0x7)); +} + +TEST_CASE("compound_wait_evt_valid is false for evt[2:0] == 011b regardless of the upper bits", "[acf]") { + REQUIRE_FALSE(compound_wait_evt_valid(0x3)); + REQUIRE_FALSE(compound_wait_evt_valid(0xB)); // 1011b + REQUIRE_FALSE(compound_wait_evt_valid(0xFB & 0x0F)); +} + +TEST_CASE("compound_wait_match: status shorter than payload never matches", "[acf]") { + const uint8_t payload[4] = {0x01, 0x02, 0x03, 0x04}; + const uint8_t status[3] = {0x01, 0x02, 0x03}; + + // Exact match on the shared 3-byte prefix would otherwise succeed — the + // length rule must short-circuit before any mode-specific comparison. + REQUIRE_FALSE(compound_wait_match(0x0, payload, sizeof(payload), status, sizeof(status))); +} + +TEST_CASE("compound_wait_match caps status to payload_length (the specification's own SPI example)", "[acf]") { + const uint8_t payload[4] = {0x00, 0x00, 0x00, 0x02}; + uint8_t status[20]; + std::fill(std::begin(status), std::end(status), uint8_t{0xAA}); // tail bytes: never read + status[0] = 0x00; status[1] = 0x00; status[2] = 0x00; status[3] = 0x02; + + REQUIRE(compound_wait_match(0x0, payload, sizeof(payload), status, sizeof(status))); + + // Changing a byte within the compared prefix must still be seen. + status[3] = 0x03; + REQUIRE_FALSE(compound_wait_match(0x0, payload, sizeof(payload), status, sizeof(status))); +} + +TEST_CASE("compound_wait_match exact-match mode (evt[2:0] = 000b)", "[acf]") { + const uint8_t payload[2] = {0x01, 0x02}; + const uint8_t equal[2] = {0x01, 0x02}; + const uint8_t differs[2] = {0x01, 0x03}; + + REQUIRE(compound_wait_match(0x0, payload, 2, equal, 2)); + REQUIRE_FALSE(compound_wait_match(0x0, payload, 2, differs, 2)); + REQUIRE(compound_wait_match(0x0, nullptr, 0, nullptr, 0)); +} + +TEST_CASE("compound_wait_match AND-with-1s-mask mode (evt[2:0] = 001b)", "[acf]") { + // The specification's own example: byte_msg_payload = 0x00000002 checks + // whether the second IO pin (bit 1) is asserted. + const uint8_t payload[4] = {0x00, 0x00, 0x00, 0x02}; + const uint8_t bit_set[4] = {0x00, 0x00, 0x00, 0x02}; + const uint8_t bit_clear[4] = {0x00, 0x00, 0x00, 0x00}; + const uint8_t other_bits[4] = {0xFF, 0xFF, 0xFF, 0xFF}; + + REQUIRE(compound_wait_match(0x1, payload, 4, bit_set, 4)); + REQUIRE_FALSE(compound_wait_match(0x1, payload, 4, bit_clear, 4)); + // Payload's own 0-bits are don't-care: status's other set bits (which + // correspond to payload 0-bits) must not affect the outcome. + REQUIRE(compound_wait_match(0x1, payload, 4, other_bits, 4)); +} + +TEST_CASE("compound_wait_match AND-with-0s-mask mode (evt[2:0] = 010b)", "[acf]") { + const uint8_t payload[4] = {0x00, 0x00, 0x00, 0x02}; + const uint8_t bit_clear[4] = {0x00, 0x00, 0x00, 0x00}; + const uint8_t bit_set[4] = {0x00, 0x00, 0x00, 0x02}; + const uint8_t other_bits_only[4] = {0xFF, 0xFF, 0xFF, 0xFD}; + + REQUIRE(compound_wait_match(0x2, payload, 4, bit_clear, 4)); + REQUIRE_FALSE(compound_wait_match(0x2, payload, 4, bit_set, 4)); + REQUIRE(compound_wait_match(0x2, payload, 4, other_bits_only, 4)); +} + +TEST_CASE("compound_wait_match leading-quadlet hi-word >= mode (evt[2:0] = 100b)", "[acf]") { + const uint8_t payload[4] = {0x00, 0x0A, 0x00, 0x00}; // hi word = 10 + const uint8_t lower[4] = {0x00, 0x05, 0x00, 0x00}; // hi word = 5 + const uint8_t higher[4] = {0x00, 0x0F, 0x00, 0x00}; // hi word = 15 + const uint8_t equal[4] = {0x00, 0x0A, 0x00, 0x00}; + + REQUIRE(compound_wait_match(0x4, payload, 4, lower, 4)); // 10>=5 + REQUIRE_FALSE(compound_wait_match(0x4, payload, 4, higher, 4)); // 10>=15 + REQUIRE(compound_wait_match(0x4, payload, 4, equal, 4)); // 10>=10 +} + +TEST_CASE("compound_wait_match leading-quadlet hi-word <= mode (evt[2:0] = 101b)", "[acf]") { + const uint8_t payload[4] = {0x00, 0x0A, 0x00, 0x00}; // hi word = 10 + const uint8_t lower[4] = {0x00, 0x05, 0x00, 0x00}; + const uint8_t higher[4] = {0x00, 0x0F, 0x00, 0x00}; + const uint8_t equal[4] = {0x00, 0x0A, 0x00, 0x00}; + + REQUIRE_FALSE(compound_wait_match(0x5, payload, 4, lower, 4)); // 10<=5 + REQUIRE(compound_wait_match(0x5, payload, 4, higher, 4)); // 10<=15 + REQUIRE(compound_wait_match(0x5, payload, 4, equal, 4)); // 10<=10 +} + +TEST_CASE("compound_wait_match leading-quadlet lo-word >= mode (evt[2:0] = 110b)", "[acf]") { + const uint8_t payload[4] = {0xFF, 0xFF, 0x00, 0x0A}; // lo word = 10 + const uint8_t lower[4] = {0xFF, 0xFF, 0x00, 0x05}; // lo word = 5 + const uint8_t higher[4] = {0xFF, 0xFF, 0x00, 0x0F}; // lo word = 15 + + REQUIRE(compound_wait_match(0x6, payload, 4, lower, 4)); // 10>=5 + REQUIRE_FALSE(compound_wait_match(0x6, payload, 4, higher, 4)); // 10>=15 +} + +TEST_CASE("compound_wait_match leading-quadlet lo-word <= mode (evt[2:0] = 111b)", "[acf]") { + const uint8_t payload[4] = {0xFF, 0xFF, 0x00, 0x0A}; // lo word = 10 + const uint8_t lower[4] = {0xFF, 0xFF, 0x00, 0x05}; + const uint8_t higher[4] = {0xFF, 0xFF, 0x00, 0x0F}; + + REQUIRE_FALSE(compound_wait_match(0x7, payload, 4, lower, 4)); // 10<=5 + REQUIRE(compound_wait_match(0x7, payload, 4, higher, 4)); // 10<=15 +} + +TEST_CASE("compound_wait_match ge/le modes reject a payload shorter than one quadlet", "[acf]") { + const uint8_t payload[3] = {0x00, 0x0A, 0x00}; + const uint8_t status[3] = {0x00, 0x00, 0x00}; + + REQUIRE_FALSE(compound_wait_match(0x4, payload, 3, status, 3)); + REQUIRE_FALSE(compound_wait_match(0x5, payload, 3, status, 3)); + REQUIRE_FALSE(compound_wait_match(0x6, payload, 3, status, 3)); + REQUIRE_FALSE(compound_wait_match(0x7, payload, 3, status, 3)); +} + +TEST_CASE("compound_wait_match reserved mode (evt[2:0] = 011b) always returns false", "[acf]") { + // Callers must gate on compound_wait_evt_valid() first; this pins the + // function's own defined (always-false) behavior if they don't. + const uint8_t payload[2] = {0x01, 0x02}; + REQUIRE_FALSE(compound_wait_match(0x3, payload, 2, payload, 2)); +} + +// ── reg_write_len (TC18 §13.7.1.2, RC5-corrected formula) ──────────────────── + +TEST_CASE("reg_write_len computes the EP0 register-write effective length", "[acf]") { + // (acf_msg_length - 3) * 4 - pad - 2. + REQUIRE(reg_write_len(5, 0) == (5 - 3) * 4 - 0 - 2); + REQUIRE(reg_write_len(7, 1) == (7 - 3) * 4 - 1 - 2); +} + +TEST_CASE("reg_write_len fails safe to 0 rather than underflowing", "[acf]") { + REQUIRE(reg_write_len(0, 0) == 0); + REQUIRE(reg_write_len(2, 0) == 0); // < 3 quadlets: no room for the fixed region at all + REQUIRE(reg_write_len(3, 255) == 0); // pad + address overhead exceeds what's left +} + +// ── peek_gbb_request_type (conditional-request modules' shared repurposed- +// timestamp-region accessor) ────────────────────────────────────────────── + +TEST_CASE("peek_gbb_request_type reads frame[8] for a genuine GBB frame", "[acf]") { + AcfMessageInfo info; + auto frame = encode_acf_gbb(info, /*message_timestamp=*/0, {}); + // Repurpose the timestamp region's first octet, as the conditional- + // request modules (compound/triggered/chained/timed) do when mtv=0. + REQUIRE(frame.size() > kAcfCommonHeaderLen); + frame[kAcfCommonHeaderLen] = 0x0F; + + uint8_t request_type = 0xFF; + REQUIRE(peek_gbb_request_type(frame.data(), frame.size(), request_type)); + REQUIRE(request_type == 0x0F); +} + +TEST_CASE("peek_gbb_request_type rejects an ACF_ABB frame outright", "[acf]") { + AcfMessageInfo info; + std::vector frame = encode_acf_abb(info, {0x0F}); + + uint8_t request_type = 0xFF; + REQUIRE_FALSE(peek_gbb_request_type(frame.data(), frame.size(), request_type)); + REQUIRE(request_type == 0xFF); // left unchanged +} + +TEST_CASE("peek_gbb_request_type rejects a GBB frame too short to hold the request_type octet", "[acf]") { + AcfMessageInfo info; + auto frame = encode_acf_gbb(info, 0, {}); + frame.resize(kAcfCommonHeaderLen); // exactly the 8-byte header, no request_type octet + + uint8_t request_type = 0xFF; + REQUIRE_FALSE(peek_gbb_request_type(frame.data(), frame.size(), request_type)); + REQUIRE(request_type == 0xFF); +} + +// ── Response builders (ported from c-RCP's rcp_acf_build_error_response()/ +// _build_acknowledge_response()/_build_acknowledge_rejected_response()) ────── + +TEST_CASE("build_error_response carries byte_bus_id, transaction_num, and the error code", "[acf]") { + auto resp = build_error_response(7, 200, WireErrorCode::ReqStorageOverflow); + + AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(decode_acf_abb(resp.data(), resp.size(), hdr, payload)); + REQUIRE(response_kind_of(hdr) == ResponseKind::ErrorResponse); + REQUIRE(hdr.err == true); + REQUIRE(hdr.rsp == true); + REQUIRE(hdr.byte_bus_id == 7); + REQUIRE(hdr.transaction_num == 200); + REQUIRE(payload.size() == 1); + REQUIRE(payload[0] == static_cast(WireErrorCode::ReqStorageOverflow)); +} + +TEST_CASE("build_error_response never classifies as Acknowledge", "[acf]") { + auto resp = build_error_response(1, 1, WireErrorCode::UnsupportedCmd); + + AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(decode_acf_abb(resp.data(), resp.size(), hdr, payload)); + uint8_t evt = static_cast((hdr.evt_ack ? 0x08 : 0) | (hdr.evt_op & 0x07)); + REQUIRE(evt != kEvtAcknowledge); + REQUIRE(response_kind_of(hdr) == ResponseKind::ErrorResponse); +} + +TEST_CASE("build_acknowledge_rejected_response carries byte_bus_id, transaction_num, and the error code", + "[acf]") { + auto resp = build_acknowledge_rejected_response(7, 200, WireErrorCode::ReqStorageOverflow); + + AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(decode_acf_abb(resp.data(), resp.size(), hdr, payload)); + REQUIRE(response_kind_of(hdr) == ResponseKind::Acknowledge); + uint8_t evt = static_cast((hdr.evt_ack ? 0x08 : 0) | (hdr.evt_op & 0x07)); + REQUIRE(evt == kEvtAcknowledge); + REQUIRE(hdr.err == true); + REQUIRE(hdr.rsp == true); + REQUIRE(hdr.byte_bus_id == 7); + REQUIRE(hdr.transaction_num == 200); + REQUIRE(payload.size() == 1); + REQUIRE(payload[0] == static_cast(WireErrorCode::ReqStorageOverflow)); +} + +TEST_CASE("build_acknowledge_rejected_response differs from build_error_response only in evt", + "[acf]") { + // Pins the distinction directly: same transaction_num/error code, but + // the two builders' own responses must classify differently + // (Acknowledge vs Error), and only the rejected-acknowledge shape's own + // evt is 0xF. + auto ack_resp = build_acknowledge_rejected_response(3, 55, WireErrorCode::UnsupportedCmd); + auto err_resp = build_error_response(3, 55, WireErrorCode::UnsupportedCmd); + + AcfMessageInfo ack_hdr, err_hdr; + std::vector ack_payload, err_payload; + REQUIRE_FALSE(decode_acf_abb(ack_resp.data(), ack_resp.size(), ack_hdr, ack_payload)); + REQUIRE_FALSE(decode_acf_abb(err_resp.data(), err_resp.size(), err_hdr, err_payload)); + + REQUIRE(response_kind_of(ack_hdr) == ResponseKind::Acknowledge); + REQUIRE(response_kind_of(err_hdr) == ResponseKind::ErrorResponse); + // Both carry err=1 and the same payload octet — only evt tells them apart. + REQUIRE(ack_hdr.err == true); + REQUIRE(err_hdr.err == true); + REQUIRE(ack_payload[0] == err_payload[0]); +} + +TEST_CASE("build_acknowledge_response builds a genuine Acknowledge with no payload", "[acf]") { + auto resp = build_acknowledge_response(5, 9); + + AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(decode_acf_abb(resp.data(), resp.size(), hdr, payload)); + REQUIRE(response_kind_of(hdr) == ResponseKind::Acknowledge); + REQUIRE(hdr.err == false); + REQUIRE(hdr.rsp == true); + REQUIRE(hdr.byte_bus_id == 5); + REQUIRE(hdr.transaction_num == 9); + REQUIRE(payload.empty()); +} + +// ── GBB worked-example pin, adapted to this codec's caller-owns-padding +// convention (GBB counterpart of the existing ABB +// "acf_msg_length/pad wire bits round-trip..." test above) ─────────────────── + +TEST_CASE("ACF_GBB acf_msg_length/pad wire bits round-trip the specification's own worked-example " + "values", + "[acf][REQ-WIRE-005]") { + AcfMessageInfo info; + info.acf_msg_length = 7; // Message Info+timestamp (4 quadlets) + padded payload (2) + CRC trailer (1) + info.pad = 1; // 7 payload bytes + 1 pad byte = 8 bytes = 2 quadlets + info.mtv = true; + std::vector payload(8, 0); // 7 "real" + 1 padding byte, caller-padded per convention + + auto frame = encode_acf_gbb(info, /*message_timestamp=*/0x1122334455667788ULL, payload); + AcfMessageInfo out; + uint64_t out_ts = 0; + std::vector out_payload; + REQUIRE_FALSE(decode_acf_gbb(frame.data(), frame.size(), out, out_ts, out_payload)); + REQUIRE(out.acf_msg_length == 7); + REQUIRE(out.pad == 1); +} diff --git a/tests/test_avtp.cpp b/tests/test_avtp.cpp index b1e9177..f1584d4 100644 --- a/tests/test_avtp.cpp +++ b/tests/test_avtp.cpp @@ -225,3 +225,208 @@ TEST_CASE("decode_tscf_header rejects a buffer shorter than the fixed header", " std::vector too_short(kTscfHeaderLen - 1, 0); REQUIRE(decode_tscf_header(too_short.data(), too_short.size(), tscf_out)); } + +// ── Phase 17 (c-RCP port): sv/version/mr/tu/reserved0/reserved1 ────────────── +// Additive fields ported from c-RCP's rcp_avtp_ntscf_header_t/ +// rcp_avtp_tscf_header_t; defaults reproduce this codec's own pre-existing +// hardcoded encode behavior (sv=1, version=0, mr=0) exactly. + +TEST_CASE("NTSCF header round-trips sv/version", "[avtp]") { + NtscfHeader hdr; + hdr.sv = true; + hdr.version = 3; // TC18 fixes this at 0 for this spec revision, but the wire + // codec itself must round-trip whatever value it is given. + hdr.stream_id = make_stream_id(0x02, 1); + + auto buf = encode_ntscf_header(hdr); + NtscfHeader out; + REQUIRE_FALSE(decode_ntscf_header(buf.data(), buf.size(), out)); + REQUIRE(out.sv == hdr.sv); + REQUIRE(out.version == hdr.version); +} + +TEST_CASE("TSCF header round-trips sv/version/mr/tu", "[avtp]") { + TscfHeader hdr; + hdr.sv = true; + hdr.version = 3; // same round-trip-whatever-given rationale as the NTSCF test above + hdr.timestamp_valid = true; + hdr.tu = false; + hdr.mr = true; + hdr.sequence_num = 0x99; + hdr.stream_id = make_stream_id(0x02, 2); + hdr.avtp_timestamp = 0x12345678; + + auto buf = encode_tscf_header(hdr); + TscfHeader out; + REQUIRE_FALSE(decode_tscf_header(buf.data(), buf.size(), out)); + REQUIRE(out.sv == hdr.sv); + REQUIRE(out.version == hdr.version); + REQUIRE(out.mr == hdr.mr); + REQUIRE(out.timestamp_valid == hdr.timestamp_valid); + REQUIRE(out.tu == hdr.tu); + REQUIRE(out.sequence_num == hdr.sequence_num); + REQUIRE(out.avtp_timestamp == hdr.avtp_timestamp); + REQUIRE(out.stream_id == hdr.stream_id); +} + +// ── §13.3 tu=1/tu=0 equivalence (REQ-AVTP-023) ──────────────────────────────── + +TEST_CASE("TSCF header decode reports tu=1 and tu=0 faithfully", "[avtp]") { + TscfHeader hdr; + hdr.stream_id = make_stream_id(0x02, 1); + hdr.tu = true; + + auto buf = encode_tscf_header(hdr); + TscfHeader out; + REQUIRE_FALSE(decode_tscf_header(buf.data(), buf.size(), out)); + REQUIRE(out.tu == true); + + hdr.tu = false; + buf = encode_tscf_header(hdr); + REQUIRE_FALSE(decode_tscf_header(buf.data(), buf.size(), out)); + REQUIRE(out.tu == false); +} + +// ── REQ-TIMED-012: TSCF avtp_timestamp -> gPTP-domain reconstruction +// (ported from c-RCP's rcp_avtp_extend_timestamp()) ─────────────────────────── + +TEST_CASE("extend_timestamp: wire_ts's low bits already equal reference_now's own", "[avtp]") { + uint64_t now = 0x0000123456789ABCull; + REQUIRE(extend_timestamp(static_cast(now), now) == now); +} + +TEST_CASE("extend_timestamp: near-future value within half a period needs no wraparound", "[avtp]") { + uint64_t now = 0x0000000100000000ull; // low 32 bits == 0 + uint32_t wire_ts = 1000u; + REQUIRE(extend_timestamp(wire_ts, now) == now + 1000u); +} + +TEST_CASE("extend_timestamp: near-past value within half a period needs no wraparound", "[avtp]") { + uint64_t now = 0x0000000100001000ull; // low 32 bits == 0x1000 + uint32_t wire_ts = 0x1000u - 500u; + REQUIRE(extend_timestamp(wire_ts, now) == now - 500u); +} + +TEST_CASE("extend_timestamp wraps forward when wire_ts is just past a 2^32 boundary", "[avtp]") { + // reference_now sits just below a 2^32 boundary; wire_ts's own low bits + // are numerically small (just above 0), which naive zero-extension + // would misread as ~4.29 seconds in the past. The correct + // reconstruction recognizes wire_ts is actually ~100ns in the FUTURE, + // one period up from the naive candidate. + uint64_t now = (uint64_t{1} << 32) - 100u; // 100ns before the boundary + uint32_t wire_ts = 0u; // the boundary itself, i.e. now + 100 + REQUIRE(extend_timestamp(wire_ts, now) == now + 100u); +} + +TEST_CASE("extend_timestamp wraps backward when wire_ts is just before a 2^32 boundary", "[avtp]") { + // Symmetric case: reference_now sits just above a 2^32 boundary; + // wire_ts's own low bits are numerically large (near 2^32-1), which + // naive zero-extension would misread as ~4.29 seconds in the future. + uint64_t now = (uint64_t{1} << 32) + 100u; // 100ns after the boundary + uint32_t wire_ts = 0xFFFFFFFFu; // the boundary minus 1, i.e. now - 101 + REQUIRE(extend_timestamp(wire_ts, now) == now - 101u); +} + +TEST_CASE("extend_timestamp: exactly half a period ahead prefers the un-wrapped candidate", "[avtp]") { + // The tie-break condition is "> half", not ">=", so exactly half stays + // with the un-wrapped (forward) candidate. + uint64_t now = 0x0000000200000000ull; // low 32 bits == 0 + uint32_t wire_ts = static_cast((uint64_t{1} << 32) / 2); // 2^31 + REQUIRE(extend_timestamp(wire_ts, now) == now + (uint64_t{1} << 31)); +} + +TEST_CASE("extend_timestamp: exactly half a period behind prefers the un-wrapped candidate", "[avtp]") { + uint64_t now = 0x0000000300000000ull | (uint64_t{1} << 31); // low 32 bits == 2^31 + uint32_t wire_ts = 0u; + REQUIRE(extend_timestamp(wire_ts, now) == now - (uint64_t{1} << 31)); +} + +// ── Subtype dispatch & the TSCF-without-time-sync drop rule (ported from +// c-RCP's rcp_avtp_peek_subtype()/_should_drop_tscf()/_tscf_reserved_all_zero()) ── + +TEST_CASE("peek_subtype reads the first byte", "[avtp]") { + NtscfHeader hdr; + hdr.stream_id = make_stream_id(0x02, 1); + auto buf = encode_ntscf_header(hdr); + + uint8_t subtype = 0; + REQUIRE_FALSE(peek_subtype(buf.data(), buf.size(), subtype)); + REQUIRE(subtype == kSubtypeNtscf); +} + +TEST_CASE("peek_subtype rejects an empty buffer", "[avtp]") { + uint8_t subtype = 0; + REQUIRE(peek_subtype(nullptr, 0, subtype)); +} + +TEST_CASE("should_drop_tscf drops a TSCF frame when time sync is unsupported and the policy is Drop", + "[avtp]") { + // TC18 §11.1's own unconditional wording, and this codec's original + // (still default) disposition. + REQUIRE(should_drop_tscf(false, kSubtypeTscf, TscfFallback::Drop)); +} + +TEST_CASE("should_drop_tscf never drops a TSCF frame when time sync is supported", "[avtp]") { + REQUIRE_FALSE(should_drop_tscf(true, kSubtypeTscf, TscfFallback::Drop)); +} + +TEST_CASE("should_drop_tscf never drops an NTSCF frame regardless of policy or time sync", "[avtp]") { + REQUIRE_FALSE(should_drop_tscf(false, kSubtypeNtscf, TscfFallback::Drop)); + REQUIRE_FALSE(should_drop_tscf(true, kSubtypeNtscf, TscfFallback::Drop)); + REQUIRE_FALSE(should_drop_tscf(false, kSubtypeNtscf, TscfFallback::Ignore)); + REQUIRE_FALSE(should_drop_tscf(true, kSubtypeNtscf, TscfFallback::Ignore)); +} + +TEST_CASE("should_drop_tscf with TscfFallback::Ignore does not drop an unsupported-time-sync TSCF frame", + "[avtp]") { + // TC18 §13.3's own configurable alternative to §11.1's unconditional + // wording — same inputs as the Drop-policy test above, only the policy + // differs, isolating this behavior from every other case. + REQUIRE_FALSE(should_drop_tscf(false, kSubtypeTscf, TscfFallback::Ignore)); +} + +TEST_CASE("should_drop_tscf: Ignore policy is irrelevant once time sync is supported", "[avtp]") { + REQUIRE_FALSE(should_drop_tscf(true, kSubtypeTscf, TscfFallback::Ignore)); +} + +TEST_CASE("tscf_reserved_all_zero is true for a freshly decoded conformant header", "[avtp]") { + TscfHeader hdr; + hdr.stream_id = make_stream_id(0x02, 1); + auto buf = encode_tscf_header(hdr); + + // encode_tscf_header always zero-fills the reserved octets regardless + // of hdr's own (here default, but irrelevant) reserved0/reserved1 — see + // TscfHeader's own doc comment — so a conformant round trip always + // decodes all-zero. + TscfHeader decoded; + REQUIRE_FALSE(decode_tscf_header(buf.data(), buf.size(), decoded)); + REQUIRE(tscf_reserved_all_zero(decoded)); +} + +TEST_CASE("tscf_reserved_all_zero is false when reserved0 or reserved1 is nonzero", "[avtp]") { + TscfHeader hdr; + hdr.reserved0 = 1; + REQUIRE_FALSE(tscf_reserved_all_zero(hdr)); + + TscfHeader hdr2; + hdr2.reserved1 = 1; + REQUIRE_FALSE(tscf_reserved_all_zero(hdr2)); +} + +TEST_CASE("decode_tscf_header reads nonzero reserved bytes off the wire", "[avtp]") { + // Decoding a hand-built wire frame whose own bytes 16-19 are nonzero + // (simulating a non-conformant or future-revision sender) proves decode + // actually reads the reserved octets off the wire, not merely that a + // hand-set struct field round-trips. + std::vector b(kTscfHeaderLen, 0); + b[0] = kSubtypeTscf; + b[1] = static_cast(1u << 7); // sv=1 + b[16] = 0xDE; b[17] = 0xAD; b[18] = 0xBE; b[19] = 0xEF; // reserved0 + b[22] = 0xAA; b[23] = 0xBB; // reserved1 + + TscfHeader decoded; + REQUIRE_FALSE(decode_tscf_header(b.data(), b.size(), decoded)); + REQUIRE(decoded.reserved0 == 0xDEADBEEFu); + REQUIRE(decoded.reserved1 == 0xAABBu); + REQUIRE_FALSE(tscf_reserved_all_zero(decoded)); +} diff --git a/tests/test_e2e.cpp b/tests/test_e2e.cpp index e0fdffd..d83a4b0 100644 --- a/tests/test_e2e.cpp +++ b/tests/test_e2e.cpp @@ -105,19 +105,22 @@ TEST_CASE("coverage_buffer layout is stream_id + avtp_timestamp + ACF header + p } // cpp-RCP-N2-03 / cpp-RCP-GBB-TS: for ACF_GBB the wire carries an 8-byte -// message_timestamp *inside* the Message Info block, spliced between its -// two header quadlets — the specification's single-ACF_GBB CRC-coverage -// figure draws one "Byte Message Info" group whose rows are, in order, -// the acf_msg_type/acf_msg_length/pad/mtv/rsv/byte_bus_id quadlet, then -// message_time_stamp as a double-height 64-bit block, then the -// evt/rsv/hs/cs/transaction_num/op/rsp/err/ms/read_size quadlet. The CRC -// coverage buffer must reproduce that byte order exactly, because the CRC -// has to cover the bytes actually transmitted. Before v2.22.0 this test -// pinned the wrong layout (timestamp after *both* quadlets, at coverage -// offset 20); the offsets below are derived from the figure, not from -// coverage_buffer's own output. -TEST_CASE("coverage_buffer splices the 8-byte message_timestamp between the ACF header's two " - "quadlets for ACF_GBB", +// message_timestamp *inside* the Message Info block, immediately after the +// complete 8-byte header (contiguous, not spliced between the header's two +// quadlets) — the specification's single-ACF_GBB CRC-coverage figure draws +// one "Byte Message Info" group whose rows are, in order, the +// acf_msg_type/acf_msg_length/pad/mtv/rsv/byte_bus_id quadlet, then the +// evt/rsv/hs/cs/transaction_num/op/rsp/err/ms/read_size quadlet, then +// message_time_stamp as a double-height 64-bit block. The CRC coverage +// buffer must reproduce that byte order exactly, because the CRC has to +// cover the bytes actually transmitted. A prior pass (v2.22.0) spliced the +// timestamp between the two quadlets instead; that was a regression against +// c-RCP's confirmed-RC5-conformant `rcp_acf_encode_gbb()`, reverted during +// the Phase 17 rewrite (cpp-RCP issue #129) — the offsets below are derived +// from acf.hpp's actual `kAcfGbbTimestampOffset` (contiguous, after the full +// header), not from a hand-derived figure. +TEST_CASE("coverage_buffer places the 8-byte message_timestamp contiguously after the ACF " + "header's two quadlets for ACF_GBB", "[e2e][REQ-E2E-002]") { auto sid = make_stream_id(0x02, 0x0003); AcfMessageInfo info; @@ -137,8 +140,8 @@ TEST_CASE("coverage_buffer splices the 8-byte message_timestamp between the ACF // Fixed prefix: stream_id (8, big-endian) + avtp_timestamp (4). // Message Info block therefore begins at coverage offset 12: // 12..15 quadlet 0 - // 16..23 message_timestamp, big-endian - // 24..27 quadlet 1 + // 16..19 quadlet 1 + // 20..27 message_timestamp, big-endian // 28.. payload const size_t mi_off = 8 + 4; // Quadlet 0: byte0 = (0x0D << 1) | 0 = 0x1A; byte2 has mtv (bit5) set @@ -146,13 +149,14 @@ TEST_CASE("coverage_buffer splices the 8-byte message_timestamp between the ACF REQUIRE(buf[mi_off + 0] == 0x1A); REQUIRE(buf[mi_off + 2] == 0x20); REQUIRE(buf[mi_off + 3] == 9); - // message_timestamp at block offset 4 (coverage offset 16), big-endian. - REQUIRE(buf[mi_off + 4] == 0x01); - REQUIRE(buf[mi_off + 11] == 0x08); - // Quadlet 1 at block offset 12 (coverage offset 24): byte1 is + // Quadlet 1 at block offset 4 (coverage offset 16): byte1 is // transaction_num, which is the cheapest positive proof the second - // quadlet really moved from offset 4 to offset 12. - REQUIRE(buf[mi_off + 13] == 0x5A); + // quadlet stayed right after the first, not after the timestamp. + REQUIRE(buf[mi_off + 5] == 0x5A); + // message_timestamp at block offset 8 (coverage offset 20), big-endian, + // immediately after the complete header. + REQUIRE(buf[mi_off + 8] == 0x01); + REQUIRE(buf[mi_off + 15] == 0x08); // Payload follows the complete 16-byte block, unchanged. REQUIRE(buf[mi_off + 16] == 0x11); REQUIRE(buf[mi_off + 17] == 0x22); @@ -285,8 +289,8 @@ TEST_CASE("acf_msg_length end-to-end matches the specification's own worked exam REQUIRE(info.acf_msg_length == 7); // 0x07 auto frame = encode_acf_gbb(info, ts, payload); - // 16-byte Message Info block (quadlet0 + 8-byte message_timestamp + - // quadlet1) + 8-byte payload = 24. + // 16-byte Message Info block (quadlet0 + quadlet1 + 8-byte + // message_timestamp, contiguous) + 8-byte payload = 24. REQUIRE(frame.size() == rcp::acf::kAcfGbbMessageInfoLen + payload.size()); // 16+8=24 auto sid = make_stream_id(0x03, 1); @@ -297,15 +301,15 @@ TEST_CASE("acf_msg_length end-to-end matches the specification's own worked exam REQUIRE((frame[0] & 0x01) == 0); REQUIRE(frame[1] == 7); - // The timestamp occupies octets 4..11 of the message, not 8..15 — - // this is the arithmetic the 0x07 figure only closes with the - // timestamp inside the Message Info block (4 + 8 + 4 + 8 + 4 = 28). - REQUIRE(frame[4] == 0x11); // message_timestamp MSB - REQUIRE(frame[11] == 0x88); // message_timestamp LSB - // Quadlet 1 begins at octet 12; with every field left at its - // default in this fixture, all four of its bytes are 0. - REQUIRE(frame[12] == 0x00); - REQUIRE(frame[15] == 0x00); + // Quadlet 1 occupies octets 4..7, immediately after quadlet 0; with + // every field left at its default in this fixture, all four of its + // bytes are 0. + REQUIRE(frame[4] == 0x00); + REQUIRE(frame[7] == 0x00); + // The timestamp occupies octets 8..15 — contiguous, right after the + // complete 8-byte header, not spliced between the two quadlets. + REQUIRE(frame[8] == 0x11); // message_timestamp MSB + REQUIRE(frame[15] == 0x88); // message_timestamp LSB } } @@ -313,8 +317,11 @@ TEST_CASE("acf_msg_length end-to-end matches the specification's own worked exam // single-ACF_GBB CRC-coverage figure field for field: acf_msg_type = 0x0D, // acf_msg_length = 0x07, pad = 1, a 7-real-byte payload padded to 8, and a // trailing CRC32 — 28 octets total. Every octet position below comes from -// that figure's row structure (quadlet 0 || 64-bit message_time_stamp || -// quadlet 1 || byte_msg_payload || CRC32), not from this codec's output. +// that figure's row structure (quadlet 0 || quadlet 1 || 64-bit +// message_time_stamp || byte_msg_payload || CRC32) — the timestamp is +// contiguous right after the complete 8-byte header, not spliced between +// the two quadlets — cross-checked against c-RCP's confirmed-RC5-conformant +// `rcp_acf_encode_gbb()`, not derived from this codec's own output. TEST_CASE("ACF_GBB full-message layout matches the specification's CRC-coverage figure " "octet for octet", "[e2e][acf][REQ-WIRE-005][REQ-E2E-002]") { @@ -340,16 +347,17 @@ TEST_CASE("ACF_GBB full-message layout matches the specification's CRC-coverage // [1] = 0x07 & 0xFF = 0x07 // [2] = (1 << 6) | (mtv << 5) | (0x123>>8) = 0x40|0x20|0x01 = 0x61 // [3] = 0x123 & 0xFF = 0x23 - // [4..11] = message_timestamp, big-endian - // [12] = (evt=0 << 4) | (hs=0 << 1) | cs=0 = 0x00 - // [13] = transaction_num = 0x42 - // [14] = (op << 7) | (0x0AB >> 8) = 0x80|0x00 = 0x80 - // [15] = 0x0AB & 0xFF = 0xAB + // [4] = (evt=0 << 4) | (hs=0 << 1) | cs=0 = 0x00 + // [5] = transaction_num = 0x42 + // [6] = (op << 7) | (0x0AB >> 8) = 0x80|0x00 = 0x80 + // [7] = 0x0AB & 0xFF = 0xAB + // [8..15] = message_timestamp, big-endian, immediately after the + // complete 8-byte header // [16..23] = byte_msg_payload (7 real + 1 pad) const std::vector expected{ 0x1A, 0x07, 0x61, 0x23, - 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xF0, 0x0D, 0x00, 0x42, 0x80, 0xAB, + 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xF0, 0x0D, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0x00, };