diff --git a/include/rcp/can.hpp b/include/rcp/can.hpp index 66dfdd4..781d364 100644 --- a/include/rcp/can.hpp +++ b/include/rcp/can.hpp @@ -12,70 +12,159 @@ // Control Protocol Specification v0.5.1_RC's Classical/FD/XL frame-format // selection, data-frame-only transfer model, CAN XL's extra header region // and extended payload ceiling, the three per-phase bit-timing register -// sets, and CAN-XL-specific acceptance/receive filtering (extraction §5.11, -// §7). +// sets, CAN-XL-specific acceptance/receive filtering, and CAN XL multi-frame +// fragmentation (extraction §5.11, §7, §13.7.11.3). // -// ROADMAP.md milestone 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN -// XL), ISELED, MDIO, Wakeup Control (v2.7.0)": CAN is the most structurally -// involved of this milestone's five endpoint types. Three points the -// roadmap calls out explicitly: +// ROADMAP.md "Phase 17" (cpp-RCP issue #129), Phase 3 ("Per-endpoint +// modules"): ported from c-RCP's include/rcp/ep_can.h + src/ep_can.c, this +// project's RC5-spec-conformant reference implementation for this endpoint +// type — including issues #610/#611/#614/#616, all fixed on the c-RCP side, +// which wired CAN XL multi-frame fragmentation via c-RCP's fragment.h. No +// spec prose, bit layout, or numeric constant is reproduced here. // -// 1. Data frames only — this header has no remote-frame request/response -// shape anywhere in it. The extraction does not name a remote-frame -// concept for this endpoint type, so none is modeled (not even as a -// rejected/unsupported code path) rather than inventing one. -// 2. CAN XL's payload can reach up to 2054 bytes (`kMaxXlPayloadSpec` -// below), which necessarily spans multiple AVTPDUs once it exceeds a -// single frame's budget. ROADMAP.md milestone 52 ("Fragmentation — -// Go/No-Go Decision", v2.8.0) has already made its go/no-go call for -// this development cycle: no-go. Consistent with that already-final -// decision — and following the same pattern rcp/uart.hpp's -// `kMaxReadSize` already established for UART's own single-AVTPDU -// accepted limitation — `kMaxXlPayloadSingleAvtpdu` bounds what this -// header will accept for a single CAN XL transfer to a conservative -// value strictly below the specification's own 2054-byte field width. -// A frame whose payload falls between that bound and the spec ceiling -// is reported via `CanErrc::xl_payload_exceeds_single_avtpdu_bound` -// rather than silently truncated or accepted — this header does not -// introduce any new fragmentation logic to carry the excess. -// 3. Unlike every other device-facing endpoint type in this codebase -// (GPIO, SPI, I2C, UART, ADC, PWM_OUT, PWM_IN, LIN, ISELED, MDIO — all -// of which build on rcp/endpoint.hpp's TriggerRegistry), the -// specification defines no trigger-signal table for CAN at all. This -// is not an oversight in this header — CanEndpoint deliberately has no -// TriggerRegistry member and no signal-id helper function, and no -// "fire a trigger" call anywhere below. A future reader adding one -// should first confirm the extraction actually defines one for this -// endpoint type, not assume it was merely forgotten here. +// ── What this pass changed relative to this file's pre-rewrite content ──── // -// Table 30/33 Row 2 evt[2:0] validation (post-v2.7.0, fifth endpoint type -// after I2C, ADC, PWM_IN, and LIN): CanEndpoint::handle_request is this -// header's own wiring of rcp::endpoint::evt_row2_kind_of — the shared -// 3-way evt[2:0] classifier for Table 33's {ADC, PWM_IN, I2C, LIN, CAN, -// UART, ISELED, MDIO} row — into CAN's request decode, following the exact -// shape rcp/i2c.hpp's I2cEndpoint::handle_request, rcp/adc.hpp's -// AdcEndpoint::handle_request, rcp/pwm.hpp's PwmInEndpoint::handle_request, -// and rcp/lin.hpp's LinEndpoint::handle_request established. See -// handle_request's own doc comment for why this is a completely separate -// concern from Figure 40's FrameFormat sub-field and from point 1 above's -// "no remote-frame concept" — evt[2:0] classification must not be confused -// with either. +// The pre-rewrite version of this header (ROADMAP.md milestone 51, v2.7.0) +// was explicitly validation-only: identifier range checks, frame-format +// payload ceilings, and XL acceptance filtering, with "no ACF-level wire +// encode/decode for CAN requests/responses anywhere in this codebase" (see +// ROADMAP.md's v2.24.0 investigation note) — a deliberate no-go on +// fragmentation (milestone 52) left CAN XL's real worst-case payload +// (2048 data octets, RCP_EP_CAN_XL_MAX_DATA_LEN on the c-RCP side) +// unreachable in a single AVTPDU. This pass closes that gap, porting: +// +// - FrameFormat: widened from this header's old 3-value +// {Classical,Fd,Xl} to c-RCP's real 6-value Table 57 set (Cbff/Ceff/ +// Fbff/Feff/XlClassicalPl/XlNewPl), each with its own TC18-defined +// 11-/29-bit arbitration-id width (frame_format_id_width()) — the old +// header modeled id width via a standalone CanIdentifier::extended +// flag independent of frame format, which could disagree with the +// format actually selected (e.g. format=Classical with +// extended=true); c-RCP's own real design derives id width from +// frame_format alone, so CanIdentifier is retired in favor of a plain +// arbitration_id validated against frame_format_id_width(format) — +// see arbitration_id_valid(). +// - The real ACF-level wire codec (encode_frame_request()/ +// decode_frame_request()/encode_frame_response()/ +// decode_frame_response()), TC18 §13.7.11.3 Figure 39's +// frame_format+arbitration_id leading quadlet, followed — only for a +// CAN XL format — by xl_header's sdt/vcid/af, followed by the raw CAN +// data bytes. RRS is not separately encoded; its value is implied by +// frame_format alone (present iff the format is a CAN XL variant) — +// see XlHeader's own comment. +// - CAN XL fragmentation (frame_request_fragment_count()/ +// encode_frame_request_fragmented(), frame_response_fragment_count()/ +// encode_frame_response_fragmented()/decode_frame_response_fragment()/ +// decode_reassembled_frame_response()), wired onto rcp/fragment.hpp's +// plan_count()/plan()/Reassembler — see "Fragmentation wiring" below. +// - CanFunctionalConfig (three independent bit-timing register sets, +// delay compensation, the execution-delay clock divider, the +// RCP_EP_CAN_XL_MAX_FILTERS-deep XL acceptance/ID filter table, and +// the TC18 §13.7.11.2 Table 56 EP_func register block — +// render_registers()/apply_reconfig()), all lifecycle-gated the same +// way every c-RCP endpoint type's functional config is +// (rcp::lifecycle::field_writable(), FieldKind::FunctionalW). CAN's +// own former ungated configure_bit_timing()/bit_timing() pair is +// retired in favor of this properly-gated surface; CanEndpoint's own +// receive() now matches an incoming CAN XL frame against +// CanFunctionalConfig::xl_filters (c-RCP's real, register-modeled +// filter table) instead of a separate, ungated xl_receive_filters_ +// vector this header used to carry as its own invention. +// +// What this pass deliberately did NOT change: CanEndpoint::transmit()/ +// receive()/handle_request()/last_transmitted()/last_received() keep their +// exact pre-existing signatures — rcp/mock.hpp's dispatch_can() already +// calls `can_.handle_request(req.evt_op, frame)` with a `can::CanDataFrame` +// built directly from a decoded ACF payload, and this pass does not touch +// rcp/mock.hpp at all (wiring the new ACF wire codec and fragmentation into +// mock.hpp's own dispatch loop is ROADMAP.md Phase 17 item 4, +// "Server/dispatch", not this pass's "per-endpoint modules" scope). CanErrc's +// four pre-existing enumerator names/values (identifier_out_of_range=1, +// payload_exceeds_format_limit=2, xl_payload_exceeds_single_avtpdu_bound=3, +// config_write_not_supported=4) are therefore also kept unchanged, since +// rcp/mock.hpp's wire_error_code_for() compares against them by name. +// +// ── Fragmentation wiring (the centerpiece of this pass) ────────────────── +// +// A worst-case CAN XL write's combined prefix-then-data payload +// (kArbitrationPrefixLen + kXlHeaderLen + kXlMaxDataLen = 4 + 6 + 2048 = +// 2058 octets, kXlMaxEncodedLen — the same figure c-RCP issue #614/#616's +// own writeup cites, and the exact figure rcp/fragment.hpp's own +// kDefaultReassemblyCapacity (4096) was sized with headroom over) does not +// fit within a single ACF message (rcp::acf::kAcfAbbMaxPayload, 2036 octets) +// — the real gap ep_can.h's own file header documents as the concrete +// driver for c-RCP's fragmentation go-decision. encode_frame_request_ +// fragmented()/encode_frame_response_fragmented() below build the combined +// payload into a fixed kXlMaxEncodedLen-byte stack buffer (no heap +// allocation — matching c-RCP issue #521's own fixed-capacity push), call +// rcp::fragment::plan_count()/plan() to split it into at most +// kMaxFragmentSegments (256, ported from c-RCP's own +// RCP_EP_CAN_MAX_FRAGMENT_SEGMENTS — see that constant's own comment for +// why this ceiling is realistic) ACF frames, and encode each one with the +// ms flag and read_size_or_segment_num field rcp::fragment::Segment +// prescribes. decode_frame_response_fragment() decodes one such fragment's +// ms/segment_num/payload without assuming it holds the whole leading +// quadlet (a caller feeds these three values straight into a +// rcp::fragment::Reassembler); once the Reassembler reports +// ReasmResult::kComplete, decode_reassembled_frame_response() applies +// Figure 39's leading-quadlet-then-data parsing to the fully reassembled +// buffer — the same function unfragmented single-frame decoding uses +// internally. +// +// ── The oversized-reassembly lesson (c-RCP issues #614/#616), and why this +// module does not re-fix it here ─────────────────────────────────────────── +// +// rcp/fragment.hpp's own header comment already documents this lesson in +// full: a reassembled payload can be genuinely, correctly complete +// (ReasmResult::kComplete, Reassembler::data()/size() fully populated) and +// still be too large to re-encode into a single response frame — the fix +// for that is not in the fragmentation primitive (nor in this endpoint +// module) but in the *caller* that reassembles a fragmented request and +// must check the result against a frame-format ceiling BEFORE attempting to +// re-encode it, building a real error response rather than silently +// dropping the request when that check fails. That caller, for this +// endpoint type, is rcp/mock.hpp's own dispatch loop (or any other future +// dispatch-loop caller) — explicitly out of this pass's scope, per the note +// above. This module's own contribution to closing that gap is making sure +// decode_reassembled_frame_response() never truncates or silently accepts +// an over-length reassembled buffer itself: it validates the leading +// quadlet and frame-format-dependent prefix length exactly as the +// unfragmented decoder does, and returns *out_rx_data/*out_rx_len as the +// full, true reassembled data length for the caller's own ceiling check. // // 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. The concrete bit-timing -// register field layout, the accepted single-AVTPDU XL payload bound, and -// the acceptance-filter matching rule chosen in this file are this -// implementation's own, same as the equivalent disclaimers in rcp/avtp.hpp, -// rcp/regmap.hpp, rcp/endpoint.hpp, rcp/uart.hpp, rcp/i2c.hpp, rcp/adc.hpp, -// rcp/pwm.hpp, and rcp/lin.hpp. +// register field layout and the general (non-XL) acceptance-filter bank +// (CanAcceptanceFilter/acceptance_filter_matches(), which has no c-RCP +// equivalent — see its own comment) are this implementation's own, same as +// the equivalent disclaimers in rcp/avtp.hpp, rcp/acf.hpp, rcp/regmap.hpp, +// rcp/endpoint.hpp, rcp/fragment.hpp, and rcp/lin.hpp. +// +// TODO(phase3-followup): same as rcp/lin.hpp's own TODO — once +// rcp/regmap.hpp's functional-config split is re-derived from c-RCP +// (ROADMAP.md Phase 17 item 4), recompose CanFunctionalConfig's five +// ep_enable/ep_clear_req_storage/ep_req_crc_enable/ep_response_ts_enable/ +// ep_suppress_response flags on top of that shared struct instead of +// carrying local duplicates of them here. Separately (matching c-RCP's own +// still-open finding): TC18's own Table 56 acceptance-filter/receive-filter +// address region (0x0024 onward) has a genuine, unresolved address-collision +// defect in the primary source — this module, like c-RCP's own +// rcp_ep_can_render_registers()/_apply_reconfig(), deliberately stops its +// own register-block model at 0x0024 (kEpFuncLen) and does not attempt to +// serialize that region at all. #pragma once +#include #include +#include +#include #include +#include #include #include +#include #include #include #include @@ -83,67 +172,179 @@ namespace rcp { namespace can { -// ── Frame format ────────────────────────────────────────────────────────────── -// The explicit FrameFormat sub-field a CAN request/response selects between -// (extraction §5.11): Classical CAN, CAN FD, and CAN XL each have their own -// payload ceiling and, for FD/XL, their own additional framing flags. - -enum class FrameFormat : uint8_t { Classical = 0, Fd = 1, Xl = 2 }; - -constexpr size_t kMaxClassicalPayload = 8; -constexpr size_t kMaxFdPayload = 64; - -// The specification's own CAN XL payload field width (extraction §5.11) — -// distinct from, and larger than, this implementation's accepted -// single-AVTPDU bound below. -constexpr size_t kMaxXlPayloadSpec = 2054; - -// This implementation's accepted single-AVTPDU ceiling for CAN XL, per the -// header-comment disclaimer above (point 2). Deliberately well below -// kMaxXlPayloadSpec; not derived from any exact MTU/overhead computation, -// same conservative-bound disclaimer as rcp/uart.hpp's kMaxReadSize. -constexpr size_t kMaxXlPayloadSingleAvtpdu = 256; - -// CAN XL's extra header region beyond the framing Classical/FD already carry -// (extraction §5.11) — this header does not itself lay out that region's -// sub-fields, only accounts for its length where relevant (e.g. a future -// wire codec sizing a CAN XL frame's on-wire footprint). -constexpr size_t kXlHeaderExtraLen = 6; - -constexpr size_t max_payload_for(FrameFormat fmt) noexcept { - switch (fmt) { - case FrameFormat::Classical: return kMaxClassicalPayload; - case FrameFormat::Fd: return kMaxFdPayload; - case FrameFormat::Xl: return kMaxXlPayloadSingleAvtpdu; - default: return 0; +// ── Frame format (TC18 §13.7.11.3 Figure 39 / Table 57) ───────────────────── +// Packed into the request/response's own leading payload quadlet — NOT into +// evt[2:0], which for CAN (a member of TC18 §13.5 Table 33's {ADC, PWM_IN, +// I2C, LIN, CAN, UART, ISELED, MDIO} row) has the same ordinary meaning +// every other endpoint in that row gives it (see CanEndpoint::handle_request +// below). Ported from rcp_ep_can_frame_format_t. + +enum class FrameFormat : uint8_t { + Cbff = 0, // Classical Base Frame Format, 11-bit id + Ceff = 1, // Classical Extended Frame Format, 29-bit id + Fbff = 2, // FD Base Frame Format, 11-bit id + Feff = 3, // FD Extended Frame Format, 29-bit id + XlClassicalPl = 4, // XL frame, classical CAN physical layer + XlNewPl = 5, // XL frame, new CAN XL physical layer +}; + +enum class IdWidth : uint8_t { Base11 = 0, Extended29 = 1 }; + +constexpr size_t kClassicalMaxDataLen = 8; +constexpr size_t kFdMaxDataLen = 64; +constexpr size_t kXlMaxDataLen = 2048; + +// True iff v (a raw wire value) selects one of the six defined frame +// formats, i.e. v <= 5. Values 6 and 7 select no defined format. Ported +// from rcp_ep_can_frame_format_valid(). +constexpr bool frame_format_valid(uint8_t v) noexcept { + return v <= static_cast(FrameFormat::XlNewPl); +} + +// True iff format is one of the two CAN XL variants. Ported from +// rcp_ep_can_frame_format_is_xl(). +constexpr bool frame_format_is_xl(FrameFormat format) noexcept { + return format == FrameFormat::XlClassicalPl || format == FrameFormat::XlNewPl; +} + +// EXTENDED_29 for CEFF/FEFF, BASE_11 for every other defined format +// (CBFF, FBFF, and both CAN XL variants — CAN XL's own arbitration-phase +// identifier is a base-width Priority ID, independent of physical-layer +// selection). An invalid format value fails safe to BASE_11. Ported from +// rcp_ep_can_frame_format_id_width(). +constexpr IdWidth frame_format_id_width(FrameFormat format) noexcept { + switch (format) { + case FrameFormat::Ceff: + case FrameFormat::Feff: return IdWidth::Extended29; + default: return IdWidth::Base11; } } -// ── Identifier ──────────────────────────────────────────────────────────────── +// True iff id is in range for format's own id width (<= 0x7FF for +// Base11, <= 0x1FFFFFFF for Extended29); always false for an invalid +// format value. Ported from rcp_ep_can_arbitration_id_valid(). +constexpr bool arbitration_id_valid(FrameFormat format, uint32_t id) noexcept { + if (!frame_format_valid(static_cast(format))) return false; + return frame_format_id_width(format) == IdWidth::Extended29 ? id <= 0x1FFFFFFFu : id <= 0x7FFu; +} -constexpr uint32_t kMaxStandardId = 0x7FFu; // 11-bit identifier -constexpr uint32_t kMaxExtendedId = 0x1FFFFFFFu; // 29-bit identifier +// The largest raw CAN data length (octets) format's own frame kind permits: +// 8 (CBFF/CEFF), 64 (FBFF/FEFF), or kXlMaxDataLen (2048, both XL variants); +// 0 for an invalid format value. Ported from +// rcp_ep_can_frame_format_max_data_len(). +constexpr size_t frame_format_max_data_len(FrameFormat format) noexcept { + switch (format) { + case FrameFormat::Cbff: + case FrameFormat::Ceff: return kClassicalMaxDataLen; + case FrameFormat::Fbff: + case FrameFormat::Feff: return kFdMaxDataLen; + case FrameFormat::XlClassicalPl: + case FrameFormat::XlNewPl: return kXlMaxDataLen; + default: return 0; + } +} + +// REQ-CANEP-038: the endpoint-level constraint that a frame's own XL variant +// must agree with the endpoint's provisioned physical layer +// (xl_new_pl_provisioned, CanFunctionalConfig) — a non-XL frame trivially +// matches, since it carries no PL choice of its own to conflict with. Ported +// from rcp_ep_can_xl_frame_matches_provisioned_pl(). Like c-RCP's own +// version, this is exposed as a standalone validator a caller MAY use — it +// is not automatically enforced by transmit()/receive()/handle_request() +// below (wiring it into a real dispatch path is Phase 4 scope, same as the +// fragmentation-into-mock.hpp note above). +constexpr bool xl_frame_matches_provisioned_pl(bool xl_new_pl_provisioned, + FrameFormat format) noexcept { + if (!frame_format_is_xl(format)) return true; + return xl_new_pl_provisioned ? (format == FrameFormat::XlNewPl) + : (format == FrameFormat::XlClassicalPl); +} -struct CanIdentifier { - uint32_t value = 0; - bool extended = false; +// ── CAN XL's extra header fields (SDT/VCID/AF) ─────────────────────────────── +// Only meaningful (and only ever populated on decode) when the associated +// frame_format is XlClassicalPl or XlNewPl. RRS (Remote Request +// Substitution) is deliberately not a member here: its value is implied +// entirely by frame_format itself (present, at a fixed value, whenever +// frame_format is a CAN XL variant; absent otherwise) — this module never +// carries it as a separately encoded field. Ported from +// rcp_ep_can_xl_header_t. +struct XlHeader { + uint8_t sdt = 0; // SDU Type + uint8_t vcid = 0; // Virtual CAN Network ID + uint32_t af = 0; // Acceptance Field }; +constexpr size_t kArbitrationPrefixLen = 4; // frame_format(3 bits) + arbitration_id(29 bits), one quadlet +constexpr size_t kXlHeaderLen = 6; // sdt(1) + vcid(1) + af(4) + +// Worst-case combined prefix-then-data payload this module's own encode +// functions can produce for a CAN XL frame request/response — ported from +// RCP_EP_CAN_XL_MAX_ENCODED_LEN (4 + 6 + 2048 = 2058). +constexpr size_t kXlMaxEncodedLen = kArbitrationPrefixLen + kXlHeaderLen + kXlMaxDataLen; + +// The largest CAN XL data length whose combined prefix-then-data payload +// still fits within a single (unfragmented) ACF message +// (rcp::acf::kAcfAbbMaxPayload, 2036 octets) — 2036 - 10 = 2026. This is the +// real TC18/ACF capability gap ep_can.h's own file header documents (a +// worst-case CAN XL frame does not fit in one ACF message at all): a +// CanDataFrame whose data falls between this bound and kXlMaxDataLen is +// structurally valid (accepted by frame_format_max_data_len()'s own +// ceiling) but not representable by encode_frame_request()/ +// encode_frame_response() alone — a caller must use +// encode_frame_request_fragmented()/encode_frame_response_fragmented() +// instead. validate_frame() below reports this specific condition as +// CanErrc::xl_payload_exceeds_single_avtpdu_bound. +constexpr size_t kMaxXlPayloadSingleFrame = acf::kAcfAbbMaxPayload - kArbitrationPrefixLen - kXlHeaderLen; + +// The largest number of ACF frames this module's own fragmented encoders +// will produce — ported from c-RCP issue #521's own +// RCP_EP_CAN_MAX_FRAGMENT_SEGMENTS: kXlMaxEncodedLen (2058) bytes is this +// module's hard compile-time ceiling on the combined payload being +// fragmented, so a genuinely unbounded fragment count is not possible; 256 +// is this module's own realistic ceiling on top of that, chosen so a +// max_fragment_payload as small as ~8 octets is still representable. A +// caller configuring a smaller max_fragment_payload gets an empty result +// from the fragment_count()/fragmented-encode functions below rather than a +// silently truncated plan. +constexpr size_t kMaxFragmentSegments = 256; + +// This module's own chosen CAN-XL acceptance/ID filter-table depth — not a +// spec-derived number (TC18's own Table 56 acceptance-filter region has a +// genuine, unresolved address-collision defect — see this file's own header +// comment). Ported from RCP_EP_CAN_XL_MAX_FILTERS. +constexpr uint8_t kMaxXlFilters = 4; + // ── Errors ──────────────────────────────────────────────────────────────────── +// identifier_out_of_range/payload_exceeds_format_limit/ +// xl_payload_exceeds_single_avtpdu_bound/config_write_not_supported (values +// 1-4) are unchanged from this header's pre-existing content — rcp/mock.hpp's +// wire_error_code_for() compares against all four by name. +// bad_frame_format..bad_arbitration_id (5-10) are new, ported from +// rcp_ep_can_errc_t (RCP_EP_CAN_ERR_*), for the new wire-codec functions +// below. enum class CanErrc : int { - identifier_out_of_range = 1, // CanIdentifier::value exceeds its 11-/29-bit range + identifier_out_of_range = 1, // arbitration_id exceeds its frame-format-derived 11-/29-bit range payload_exceeds_format_limit = 2, // payload length exceeds the selected FrameFormat's own max - xl_payload_exceeds_single_avtpdu_bound = 3, // within the spec's 2054-byte ceiling, but beyond this - // implementation's accepted single-AVTPDU bound (see header comment) + // Within the format's own ceiling, but the combined prefix-then-data + // payload does not fit within a single (unfragmented) ACF message — see + // kMaxXlPayloadSingleFrame above. A caller must use the *_fragmented() + // functions below instead. + xl_payload_exceeds_single_avtpdu_bound = 3, // evt_row2_kind_of classified the request as ConfigWrite (evt[2:0] == - // 111b, §12.7.1). This milestone's follow-up deliberately does not - // implement the configuration-write shape (relative EP_functional- - // config start address + configuration data) — see handle_request's - // own comment. Reported explicitly rather than silently accepted as a - // plain transmit or silently ignored, same as I2C's, ADC's, PWM_IN's, - // and LIN's own config_write_not_supported variants. + // 111b, §12.7.1). CanEndpoint::handle_request deliberately does not + // implement the configuration-write shape end to end (that needs + // dispatch-level wiring to CanFunctionalConfig/apply_reconfig() below, + // Phase 4 scope) — reported explicitly rather than silently accepted as + // a plain transmit or silently ignored. config_write_not_supported = 4, + bad_frame_format = 5, // ported from RCP_EP_CAN_ERR_BAD_FRAME_FORMAT + short_frame = 6, // ported from RCP_EP_CAN_ERR_SHORT_FRAME + bad_msg_type = 7, // ported from RCP_EP_CAN_ERR_BAD_MSG_TYPE + wrong_bus = 8, // ported from RCP_EP_CAN_ERR_WRONG_BUS + wrong_op = 9, // ported from RCP_EP_CAN_ERR_WRONG_OP + bad_evt = 10, // ported from RCP_EP_CAN_ERR_BAD_EVT + bad_arbitration_id = 11, // ported from RCP_EP_CAN_ERR_BAD_ARBITRATION_ID }; inline const std::error_category& can_category() noexcept { @@ -152,13 +353,20 @@ inline const std::error_category& can_category() noexcept { std::string message(int ev) const override { switch (static_cast(ev)) { case CanErrc::identifier_out_of_range: - return "rcp/can: identifier exceeds its 11-/29-bit range"; + return "rcp/can: arbitration_id exceeds its frame-format-derived 11-/29-bit range"; case CanErrc::payload_exceeds_format_limit: return "rcp/can: payload exceeds the selected FrameFormat's payload limit"; case CanErrc::xl_payload_exceeds_single_avtpdu_bound: - return "rcp/can: CAN XL payload requires multi-AVTPDU fragmentation, not supported this cycle"; + return "rcp/can: CAN XL payload requires fragmentation — use the *_fragmented functions"; case CanErrc::config_write_not_supported: return "rcp/can: evt[2:0]=111b configuration-write requests are not yet implemented"; + case CanErrc::bad_frame_format: return "rcp/can: invalid frame format"; + case CanErrc::short_frame: return "rcp/can: frame too short"; + case CanErrc::bad_msg_type: return "rcp/can: unexpected ACF message type"; + case CanErrc::wrong_bus: return "rcp/can: wrong byte_bus_id"; + case CanErrc::wrong_op: return "rcp/can: wrong ACF op"; + case CanErrc::bad_evt: return "rcp/can: evt[2:0] is not 0b000"; + case CanErrc::bad_arbitration_id: return "rcp/can: arbitration_id out of range for frame format"; default: return "rcp/can: unknown error"; } @@ -172,57 +380,59 @@ inline std::error_code make_error_code(CanErrc e) noexcept { return {static_cast(e), can_category()}; } -inline std::error_code validate_identifier(const CanIdentifier& id) noexcept { - const uint32_t max = id.extended ? kMaxExtendedId : kMaxStandardId; - if (id.value > max) return make_error_code(CanErrc::identifier_out_of_range); +inline std::error_code validate_identifier(FrameFormat format, uint32_t arbitration_id) noexcept { + if (!arbitration_id_valid(format, arbitration_id)) return make_error_code(CanErrc::identifier_out_of_range); return {}; } -// ── Data frame (no remote-frame shape — see header comment point 1) ───────── +// ── Data frame (no remote-frame shape) ─────────────────────────────────────── +// TC18's own text states outright that sending remote frames is not +// supported — there is no wire representation, encode function, or decode +// outcome for a remote frame anywhere in this module. struct CanDataFrame { - CanIdentifier id; - FrameFormat format = FrameFormat::Classical; - bool bit_rate_switch = false; // FD/XL data-phase BRS; ignored for Classical - bool error_state_indicator = false; // FD/XL ESI; ignored for Classical - std::vector data; + FrameFormat format = FrameFormat::Cbff; + uint32_t arbitration_id = 0; + XlHeader xl_header{}; // meaningful only when frame_format_is_xl(format) + std::vector data; }; -// validate_frame checks the identifier's own range, then the payload against -// the selected format's ceiling — for FrameFormat::Xl specifically -// distinguishing "exceeds the specification's own field width" from -// "exceeds this implementation's accepted single-AVTPDU bound" so callers -// can tell a genuinely invalid frame from one this development cycle simply -// does not carry (extraction §5.11; see header comment point 2). +// validate_frame checks arbitration_id against format's own id width, then +// the payload against the selected format's ceiling — for a CAN XL format +// specifically distinguishing "exceeds the format's own ceiling" from +// "exceeds what a single unfragmented ACF message can carry" so callers can +// tell a genuinely invalid frame from one that needs +// encode_frame_request_fragmented()/encode_frame_response_fragmented() +// instead of encode_frame_request()/encode_frame_response(). inline std::error_code validate_frame(const CanDataFrame& f) noexcept { - auto ec = validate_identifier(f.id); + auto ec = validate_identifier(f.format, f.arbitration_id); if (ec) return ec; - if (f.format == FrameFormat::Xl && f.data.size() > kMaxXlPayloadSingleAvtpdu) { - if (f.data.size() > kMaxXlPayloadSpec) - return make_error_code(CanErrc::payload_exceeds_format_limit); - return make_error_code(CanErrc::xl_payload_exceeds_single_avtpdu_bound); - } - - if (f.data.size() > max_payload_for(f.format)) + if (f.data.size() > frame_format_max_data_len(f.format)) return make_error_code(CanErrc::payload_exceeds_format_limit); + if (frame_format_is_xl(f.format) && f.data.size() > kMaxXlPayloadSingleFrame) + return make_error_code(CanErrc::xl_payload_exceeds_single_avtpdu_bound); + return {}; } // ── Per-phase bit-timing register sets ─────────────────────────────────────── // Separate register sets for the arbitration phase (used by every frame // format for at least its identifier/arbitration field) and the FD/XL -// data-phase bit rates, which run faster than arbitration once BRS is set -// (extraction §5.11). CAN XL's data phase is its own third register set, -// distinct from FD's — the two are not required to share timing. +// data-phase bit rates, which run faster than arbitration once BRS is set. +// CAN XL's data phase is its own third register set, distinct from FD's. +// Standard, publicly documented Bosch-CAN-style bit-timing register +// concepts, not values taken from the specification — ported from +// rcp_ep_can_bit_timing_t (field widths, including sync_jump_width's 8-bit +// width, match exactly). struct CanBitTimingPhase { - uint32_t prescaler = 1; + uint32_t prescaler = 0; uint16_t prop_seg = 0; uint16_t phase_seg1 = 0; uint16_t phase_seg2 = 0; - uint16_t sync_jump_width = 0; + uint8_t sync_jump_width = 0; }; struct CanBitTimingConfig { @@ -231,51 +441,765 @@ struct CanBitTimingConfig { CanBitTimingPhase xl_data; // XL's data-phase register set, independent of fd_data }; -// ── CAN-XL-specific acceptance / receive filters ───────────────────────────── -// A single id/mask acceptance filter shape reused for both CAN's general -// acceptance filtering and CAN XL's own receive filters (extraction §5.11). -// acceptance_filter_matches implements the standard masked-compare rule: -// bits set in `mask` must match between the filter's `id` and the candidate -// identifier; bits clear in `mask` are don't-care. +// ── Functional config ───────────────────────────────────────────────────────── +// Ported from rcp_ep_can_functional_cfg_t — see this file's own header +// comment for why the five Table 35 "common" flags are local members here +// rather than composed from rcp/regmap.hpp's EndpointFunctionalConfig. + +struct CanXlFilter { + uint32_t id = 0; + uint32_t mask = 0; + bool enable = false; +}; + +struct CanFunctionalConfig { + bool ep_enable = false; + bool ep_clear_req_storage = false; + bool ep_req_crc_enable = false; + bool ep_response_ts_enable = false; + bool ep_suppress_response = false; + + CanBitTimingConfig timing; + bool delay_comp_enable = false; + uint8_t delay_comp_offset = 0; + uint32_t exec_delay_clk_divider = 0; // execution-delay timing only — NOT bit timing + std::array xl_filters{}; + uint16_t ep_status = 0; // can_ep_status, Table 56 0x0006 + uint32_t status = 0; // CAN EP status, Table 56 0x001C + uint32_t fifo_status = 0; // FIFO status, Table 56 0x0020 + // "usage of new PL (YES|NO) for CAN XL", §13.7.11.2 — in-memory only, no + // wire offset (TC18 gives this setting no register offset anywhere in + // Table 56 — a genuine specification gap, not a local implementation + // one). See xl_frame_matches_provisioned_pl() above. + bool xl_new_pl_provisioned = false; +}; + +// True iff index is a valid filter-table index (0..kMaxXlFilters-1). Ported +// from rcp_ep_can_xl_filter_index_valid(). +constexpr bool xl_filter_index_valid(uint8_t index) noexcept { return index < kMaxXlFilters; } + +// functional_cfg_writable is a thin, named wrapper over +// rcp::lifecycle::field_writable() with FieldKind::FunctionalW — ported +// from rcp_ep_can_functional_cfg_writable(). +inline bool functional_cfg_writable(lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + return lifecycle::field_writable(state, lifecycle::FieldKind::FunctionalW, writer); +} + +inline bool set_arbitration_timing(CanFunctionalConfig& cfg, CanBitTimingPhase timing, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.timing.arbitration = timing; + return true; +} + +inline bool set_fd_data_timing(CanFunctionalConfig& cfg, CanBitTimingPhase timing, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.timing.fd_data = timing; + return true; +} + +inline bool set_xl_data_timing(CanFunctionalConfig& cfg, CanBitTimingPhase timing, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.timing.xl_data = timing; + return true; +} + +// One setter for both delay_comp_enable/delay_comp_offset (always +// reconfigured as a pair on the wire). +inline bool set_delay_compensation(CanFunctionalConfig& cfg, bool enable, uint8_t offset, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.delay_comp_enable = enable; + cfg.delay_comp_offset = offset; + return true; +} + +inline bool set_xl_new_pl_provisioned(CanFunctionalConfig& cfg, bool new_pl_provisioned, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.xl_new_pl_provisioned = new_pl_provisioned; + return true; +} + +inline bool set_exec_delay_clk_divider(CanFunctionalConfig& cfg, uint32_t divider, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.exec_delay_clk_divider = divider; + return true; +} + +// Sets cfg.xl_filters[index] iff index is xl_filter_index_valid() and +// functional_cfg_writable() authorizes the write; returns whether the write +// was applied. cfg is left entirely unchanged otherwise. +inline bool set_xl_filter(CanFunctionalConfig& cfg, uint8_t index, CanXlFilter filter, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!xl_filter_index_valid(index)) return false; + if (!functional_cfg_writable(state, writer)) return false; + cfg.xl_filters[index] = filter; + return true; +} + +// ── CAN's own general (non-XL) acceptance-filter bank ──────────────────────── +// This bank — and acceptance_filter_matches()/CanEndpoint::receive()'s use +// of it below for Classical/FD frames — has NO c-RCP equivalent: c-RCP's +// own functional config models only the CAN-XL-specific xl_filters table +// above. This is this implementation's own extension, kept from this +// header's pre-existing content, same disclaimer as this file's own header +// comment. struct CanAcceptanceFilter { uint32_t id = 0; uint32_t mask = 0; - bool extended = false; + bool extended = false; // matches only a frame whose own frame_format_id_width() agrees }; -inline bool acceptance_filter_matches(const CanAcceptanceFilter& filt, const CanIdentifier& id) noexcept { - if (filt.extended != id.extended) return false; - return (id.value & filt.mask) == (filt.id & filt.mask); +inline bool acceptance_filter_matches(const CanAcceptanceFilter& filt, FrameFormat format, + uint32_t arbitration_id) noexcept { + if (filt.extended != (frame_format_id_width(format) == IdWidth::Extended29)) return false; + return (arbitration_id & filt.mask) == (filt.id & filt.mask); +} + +// ── The EP_func register block (the evt[2:0] == 111b target), TC18 +// §13.7.11.2 Table 56 ──────────────────────────────────────────────────────── +// +// 0x0000 can_ep_len 8 bit R kEpFuncLen (0x24) +// 0x0001 Reserved 8 bit R reads 0x00 +// 0x0002 can_ep_enable&clr 8 bit R/W Table 35 common entries +// 0x0003 can_ep_options 8 bit R/W* Table 35 common entries +// 0x0004 can_base_clk 16 bit R CAN system clock (always 0 +// — no real clock modelled) +// 0x0006 can_ep_status 16 bit R/W +// 0x0008-0x001B clk_divider, two reserved octets, and the three "CAN bit +// time register" fields plus TDCC — TC18 gives these +// 32-bit registers no sub-field bit-layout anywhere in the +// specification text (this file's own header comment, +// ported from ep_can.h's identical finding), so this span +// is treated as one contiguous read-only region rather +// than inventing an unverified bit-packing scheme. +// 0x001C status 32 bit R/W +// 0x0020 fifo_status 32 bit R/W +// +// closing at kEpFuncLen (0x0024), immediately before Table 56's own +// acceptance-filter region — see this file's own header comment for why +// this module's register-block model deliberately stops there. + +constexpr uint16_t kRegEpLen = 0x0000; +constexpr uint16_t kRegReserved01 = 0x0001; +constexpr uint16_t kRegEpEnableClr = 0x0002; +constexpr uint16_t kRegEpOptions = 0x0003; +constexpr uint16_t kRegBaseClk = 0x0004; +constexpr uint16_t kRegEpStatus = 0x0006; +constexpr uint16_t kRegUndecomposedStart = 0x0008; +constexpr uint16_t kRegUndecomposedLen = 0x0014; // 0x0008..0x001B +constexpr uint16_t kRegStatus = 0x001C; +constexpr uint16_t kRegFifoStatus = 0x0020; + +constexpr size_t kEpFuncLen = 0x0024; +constexpr size_t kReconfigAddrLen = 2; + +namespace detail { +constexpr uint8_t kEnableClrBitEnable = (1u << 0); +// Table 35 fixes ep_clear_req_storage at bit 4 for every endpoint type +// (c-RCP issue #470 fixed CAN's own register block from an earlier, +// wrong (1u<<1) value — this port uses the correct bit position from the +// start). +constexpr uint8_t kEnableClrBitClear = (1u << 4); +constexpr uint8_t kOptionsBitReqCrc = (1u << 0); +constexpr uint8_t kOptionsBitRespTs = (1u << 3); +constexpr uint8_t kOptionsBitSuppress = (1u << 7); + +inline void put_u16(uint8_t* p, uint16_t v) noexcept { + p[0] = static_cast((v >> 8) & 0xFFu); + p[1] = static_cast(v & 0xFFu); +} +inline uint16_t get_u16(const uint8_t* p) noexcept { + return static_cast((static_cast(p[0]) << 8) | p[1]); +} +inline void put_u32(uint8_t* p, uint32_t v) noexcept { + p[0] = static_cast((v >> 24) & 0xFFu); + p[1] = static_cast((v >> 16) & 0xFFu); + p[2] = static_cast((v >> 8) & 0xFFu); + p[3] = static_cast(v & 0xFFu); +} +inline uint32_t get_u32(const uint8_t* p) noexcept { + return (static_cast(p[0]) << 24) | (static_cast(p[1]) << 16) | + (static_cast(p[2]) << 8) | static_cast(p[3]); +} +inline bool reg_offset_read_only(uint16_t addr) noexcept { + return addr == kRegEpLen || addr == kRegReserved01 || + (addr >= kRegBaseClk && addr < kRegBaseClk + 2u) || + (addr >= kRegUndecomposedStart && addr < kRegUndecomposedStart + kRegUndecomposedLen); +} +} // namespace detail + +// Serializes cfg's EP_func registers into out[0..kEpFuncLen) exactly as a +// configuration *read* of the whole block would report them. Ported from +// rcp_ep_can_render_registers(). +inline void render_registers(const CanFunctionalConfig& cfg, + std::array& out) noexcept { + uint8_t enable_clr = 0; + uint8_t options = 0; + if (cfg.ep_enable) enable_clr |= detail::kEnableClrBitEnable; + if (cfg.ep_clear_req_storage) enable_clr |= detail::kEnableClrBitClear; + if (cfg.ep_req_crc_enable) options |= detail::kOptionsBitReqCrc; + if (cfg.ep_response_ts_enable) options |= detail::kOptionsBitRespTs; + if (cfg.ep_suppress_response) options |= detail::kOptionsBitSuppress; + + out[kRegEpLen] = static_cast(kEpFuncLen); + out[kRegReserved01] = 0; + out[kRegEpEnableClr] = enable_clr; + out[kRegEpOptions] = options; + detail::put_u16(&out[kRegBaseClk], 0); // no real clock source modelled + detail::put_u16(&out[kRegEpStatus], cfg.ep_status); + std::fill(out.begin() + kRegUndecomposedStart, + out.begin() + kRegUndecomposedStart + kRegUndecomposedLen, uint8_t{0}); + detail::put_u32(&out[kRegStatus], cfg.status); + detail::put_u32(&out[kRegFifoStatus], cfg.fifo_status); +} + +enum class CanReconfigErrc : int { + short_payload = 1, // payload carries no address prefix, or an address prefix with no data octet after it + out_of_range = 2, // start_address + data length exceeds kEpFuncLen — the whole write is ignored +}; + +inline const std::error_category& can_reconfig_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.can.reconfig"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case CanReconfigErrc::short_payload: return "rcp/can: CAN configuration write has no address and data"; + case CanReconfigErrc::out_of_range: return "rcp/can: CAN configuration write extends past the EP_func block"; + default: return "rcp/can: CAN unknown configuration-write error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(CanReconfigErrc e) noexcept { + return {static_cast(e), can_reconfig_category()}; +} + +// Applies the configuration escape hatch (evt[2:0] == 111b) — a 16-bit +// big-endian relative start address followed by configuration data octets. +// Ported from rcp_ep_can_apply_reconfig(). cfg is left entirely unchanged +// on error. Octets landing on a read-only register (EP_LEN, the reserved +// octet, base_clk, and the whole not-yet-decomposed 0x0008-0x001B span) are +// left at their current values while the rest of the span is still applied. +inline std::error_code apply_reconfig(CanFunctionalConfig& cfg, const uint8_t* payload, + size_t payload_len) { + if (payload_len <= kReconfigAddrLen) return make_error_code(CanReconfigErrc::short_payload); + + const uint16_t start_address = detail::get_u16(payload); + const size_t data_len = payload_len - kReconfigAddrLen; + + if (static_cast(start_address) + data_len > kEpFuncLen) + return make_error_code(CanReconfigErrc::out_of_range); + + std::array block{}; + render_registers(cfg, block); + for (size_t i = 0; i < data_len; ++i) { + const auto addr = static_cast(start_address + i); + if (detail::reg_offset_read_only(addr)) continue; // write ignored + block[addr] = payload[kReconfigAddrLen + i]; + } + + cfg.ep_enable = (block[kRegEpEnableClr] & detail::kEnableClrBitEnable) != 0; + cfg.ep_clear_req_storage = (block[kRegEpEnableClr] & detail::kEnableClrBitClear) != 0; + cfg.ep_req_crc_enable = (block[kRegEpOptions] & detail::kOptionsBitReqCrc) != 0; + cfg.ep_response_ts_enable = (block[kRegEpOptions] & detail::kOptionsBitRespTs) != 0; + cfg.ep_suppress_response = (block[kRegEpOptions] & detail::kOptionsBitSuppress) != 0; + cfg.ep_status = detail::get_u16(&block[kRegEpStatus]); + cfg.status = detail::get_u32(&block[kRegStatus]); + cfg.fifo_status = detail::get_u32(&block[kRegFifoStatus]); + + return {}; +} + +// ── Wire layout helpers (TC18 §13.7.11.3 Figure 39) ────────────────────────── + +namespace detail { +inline size_t prefix_len_for(FrameFormat format) noexcept { + return frame_format_is_xl(format) ? (kArbitrationPrefixLen + kXlHeaderLen) : kArbitrationPrefixLen; +} + +inline void write_prefix(uint8_t* p, FrameFormat format, uint32_t arbitration_id, + const std::optional& xl_header) noexcept { + const uint32_t combined = (static_cast(format) << 29) | (arbitration_id & 0x1FFFFFFFu); + put_u32(p, combined); + if (frame_format_is_xl(format) && xl_header) { + p[4] = xl_header->sdt; + p[5] = xl_header->vcid; + put_u32(&p[6], xl_header->af); + } +} + +inline FrameFormat read_frame_format(const uint8_t* p) noexcept { + return static_cast((get_u32(p) >> 29) & 0x7u); +} + +inline void read_prefix(const uint8_t* p, FrameFormat format, uint32_t& out_arbitration_id, + XlHeader& out_xl_header) noexcept { + out_arbitration_id = get_u32(p) & 0x1FFFFFFFu; + if (frame_format_is_xl(format)) { + out_xl_header.sdt = p[4]; + out_xl_header.vcid = p[5]; + out_xl_header.af = get_u32(&p[6]); + } +} + +// Validates the shared encode preconditions for a request/response; returns +// false (nothing further should be encoded) iff any precondition fails. +inline bool encode_preconditions_ok(FrameFormat frame_format, uint32_t arbitration_id, + const std::optional& xl_header, + size_t data_len) noexcept { + if (!frame_format_valid(static_cast(frame_format))) return false; + if (!arbitration_id_valid(frame_format, arbitration_id)) return false; + if (data_len > frame_format_max_data_len(frame_format)) return false; + + const bool is_xl = frame_format_is_xl(frame_format); + if (is_xl != xl_header.has_value()) return false; + + return true; +} + +// Builds this module's own prefix-then-data layout into a fixed +// kXlMaxEncodedLen-byte buffer — no heap allocation. Every caller below +// already validates data_len against frame_format_max_data_len() via +// encode_preconditions_ok() before calling, so prefix_len + data_len can +// never exceed kXlMaxEncodedLen. +inline size_t build_payload(FrameFormat frame_format, uint32_t arbitration_id, + const std::optional& xl_header, const uint8_t* data, + size_t data_len, std::array& out_buf) noexcept { + const size_t prefix_len = prefix_len_for(frame_format); + write_prefix(out_buf.data(), frame_format, arbitration_id, xl_header); + if (data_len > 0) std::copy(data, data + data_len, out_buf.begin() + static_cast(prefix_len)); + return prefix_len + data_len; +} +} // namespace detail + +// ── Frame request ───────────────────────────────────────────────────────────── +// Ported from rcp_ep_can_encode_frame_request()/_decode_frame_request(). + +// Encodes an ACF_ABB frame request addressed to byte_bus_id: evt is left +// entirely 0 (plain request), and the payload is Figure 39's layout — +// frame_format+arbitration_id (the leading quadlet), xl_header (only when +// frame_format is a CAN XL variant), and tx_data (the raw CAN data bytes). +// Returns an empty vector if: frame_format is not frame_format_valid(); +// arbitration_id is not arbitration_id_valid() for frame_format; tx_data +// exceeds frame_format_max_data_len(frame_format); or xl_header's presence +// does not match frame_format_is_xl(frame_format). +inline std::vector encode_frame_request(avtp::ByteBusId byte_bus_id, FrameFormat frame_format, + uint32_t arbitration_id, + const std::optional& xl_header, + const std::vector& tx_data, + uint8_t transaction_num) { + if (!detail::encode_preconditions_ok(frame_format, arbitration_id, xl_header, tx_data.size())) + return {}; + + std::array payload{}; + const size_t payload_len = + detail::build_payload(frame_format, arbitration_id, xl_header, tx_data.data(), tx_data.size(), payload); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = true; // write + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, std::vector(payload.begin(), payload.begin() + static_cast(payload_len))); +} + +// Decodes and validates an ACF-level CAN frame request from b[0..len). +// Fails with CanErrc::short_frame/bad_msg_type/wrong_bus/wrong_op/bad_evt/ +// bad_frame_format/bad_arbitration_id — see ep_can.c's own doc comment for +// the exact condition each maps to. On success, every output parameter is +// populated; out_xl_header is populated iff frame_format_is_xl(out_format). +inline std::error_code decode_frame_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + FrameFormat& out_format, uint32_t& out_arbitration_id, + XlHeader& out_xl_header, std::vector& out_tx_data, + uint8_t& out_transaction_num) { + acf::AcfMessageInfo hdr; + std::vector payload; + auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(CanErrc::short_frame); + if (ec) return make_error_code(CanErrc::bad_msg_type); + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(CanErrc::wrong_bus); + if (!hdr.op) return make_error_code(CanErrc::wrong_op); + if (!acf::evt_row2_is_plain(hdr.evt_op)) return make_error_code(CanErrc::bad_evt); + + if (payload.size() < kArbitrationPrefixLen) return make_error_code(CanErrc::short_frame); + const FrameFormat frame_format = detail::read_frame_format(payload.data()); + if (!frame_format_valid(static_cast(frame_format))) return make_error_code(CanErrc::bad_frame_format); + + const size_t prefix_len = detail::prefix_len_for(frame_format); + if (payload.size() < prefix_len) return make_error_code(CanErrc::short_frame); + + detail::read_prefix(payload.data(), frame_format, out_arbitration_id, out_xl_header); + if (!arbitration_id_valid(frame_format, out_arbitration_id)) + return make_error_code(CanErrc::bad_arbitration_id); + + out_format = frame_format; + out_tx_data = std::vector(payload.begin() + static_cast(prefix_len), payload.end()); + out_transaction_num = hdr.transaction_num; + return {}; +} + +// ── Fragmented request (c-RCP issue #611, rcp/fragment.hpp) ────────────────── +// The request-side counterpart of "Fragmented response" below. Unlike the +// response side, a request is never carried as ACF_GBB — every fragment is +// encoded as ACF_ABB, unconditionally. + +// The number of ACF frames encode_frame_request_fragmented() would produce +// for this request's combined prefix-then-data payload split into +// fragments of at most max_fragment_payload octets each. Returns 0 under +// the same conditions encode_frame_request() already fails +// encode_preconditions_ok() for, plus fragment::plan_count()'s own +// 0-sentinel conditions, plus kMaxFragmentSegments. +inline size_t frame_request_fragment_count(FrameFormat frame_format, uint32_t arbitration_id, + const std::optional& xl_header, size_t tx_len, + size_t max_fragment_payload) noexcept { + if (!detail::encode_preconditions_ok(frame_format, arbitration_id, xl_header, tx_len)) return 0; + const size_t combined_len = detail::prefix_len_for(frame_format) + tx_len; + const size_t count = fragment::plan_count(combined_len, max_fragment_payload); + if (count > kMaxFragmentSegments) return 0; + return count; +} + +// Encodes a CAN frame write request as one or more ACF_ABB frames, +// fragmenting via rcp::fragment's ms/segment_num mechanism whenever the +// combined prefix-then-data payload exceeds max_fragment_payload octets. +// Every fragment shares byte_bus_id/evt(0)/op(write)/transaction_num — +// frame_format itself lives inside the combined payload's own leading +// quadlet, not per-fragment header state, so only the first fragment +// actually carries it. When the combined payload already fits in one +// fragment, this produces exactly one frame identical to what +// encode_frame_request() itself would have produced — fragmentation is a +// strict superset of the unfragmented path. Returns an empty vector under +// the same conditions frame_request_fragment_count() returns 0 for. This +// function does not itself apply any E2E CRC — a caller wanting E2E +// protection wraps only the final (ms=false) frame after this function +// returns. +inline std::vector> +encode_frame_request_fragmented(avtp::ByteBusId byte_bus_id, FrameFormat frame_format, + uint32_t arbitration_id, const std::optional& xl_header, + const std::vector& tx_data, uint8_t transaction_num, + size_t max_fragment_payload) { + const size_t count = frame_request_fragment_count(frame_format, arbitration_id, xl_header, + tx_data.size(), max_fragment_payload); + if (count == 0) return {}; + + std::array combined{}; + const size_t combined_len = + detail::build_payload(frame_format, arbitration_id, xl_header, tx_data.data(), tx_data.size(), combined); + + std::array segs{}; + if (fragment::plan(combined_len, max_fragment_payload, segs.data(), count)) return {}; + + std::vector> out_frames; + out_frames.reserve(count); + for (size_t i = 0; i < count; ++i) { + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = true; // write + hdr.transaction_num = transaction_num; + hdr.ms = segs[i].ms; + hdr.read_size_or_segment_num = segs[i].ms ? segs[i].segment_num : 0; + + std::vector slice(combined.begin() + static_cast(segs[i].offset), + combined.begin() + static_cast(segs[i].offset + segs[i].len)); + out_frames.push_back(acf::encode_acf_abb(hdr, slice)); + } + return out_frames; +} + +// ── Response ─────────────────────────────────────────────────────────────────── +// Ported from rcp_ep_can_encode_frame_response()/_decode_frame_response(). + +// Encodes a CAN frame response with the same frame_format/arbitration_id/ +// xl_header/data-validation rules and prefix-then-data payload layout as +// encode_frame_request() (rx_data in place of tx_data), echoing +// transaction_num. Encoded as ACF_ABB when timed is false; as ACF_GBB +// (message_timestamp = timestamp, mtv = true) when timed is true. Returns +// an empty vector under the same conditions encode_frame_request() does. +inline std::vector encode_frame_response(avtp::ByteBusId byte_bus_id, FrameFormat frame_format, + uint32_t arbitration_id, + const std::optional& xl_header, + const std::vector& rx_data, + uint8_t transaction_num, bool timed, + uint64_t timestamp) { + if (!detail::encode_preconditions_ok(frame_format, arbitration_id, xl_header, rx_data.size())) + return {}; + + std::array payload{}; + const size_t payload_len = + detail::build_payload(frame_format, arbitration_id, xl_header, rx_data.data(), rx_data.size(), payload); + std::vector body(payload.begin(), payload.begin() + static_cast(payload_len)); + + if (timed) { + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read + hdr.rsp = true; + hdr.mtv = true; + hdr.transaction_num = transaction_num; + return acf::encode_acf_gbb(hdr, timestamp, body); + } + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read + hdr.rsp = true; + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, body); +} + +// Decodes a CAN frame response from either an ACF_ABB or ACF_GBB message +// (peeks the ACF message type itself, unlike decode_frame_request(), since +// a response's encoding depends on the responding endpoint's own +// timed/untimed choice). Fails with the same error set +// decode_frame_request() does. On success, every output parameter is +// populated. +inline std::error_code decode_frame_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + FrameFormat& out_format, uint32_t& out_arbitration_id, + XlHeader& out_xl_header, std::vector& out_rx_data, + bool& out_timed, uint64_t& out_timestamp, + uint8_t& out_transaction_num) { + uint8_t msg_type; + if (acf::peek_msg_type(b, len, msg_type)) return make_error_code(CanErrc::short_frame); + + acf::AcfMessageInfo hdr; + std::vector payload; + avtp::ByteBusId bus_id; + uint8_t evt_op; + bool timed; + uint64_t timestamp; + + if (msg_type == acf::kAcfMsgTypeGbb) { + uint64_t ts = 0; + auto ec = acf::decode_acf_gbb(b, len, hdr, ts, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(CanErrc::short_frame); + if (ec) return make_error_code(CanErrc::bad_msg_type); + bus_id = hdr.byte_bus_id; + evt_op = hdr.evt_op; + timed = hdr.mtv; + timestamp = timed ? ts : 0; + } else { + auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(CanErrc::short_frame); + if (ec) return make_error_code(CanErrc::bad_msg_type); + bus_id = hdr.byte_bus_id; + evt_op = hdr.evt_op; + timed = false; + timestamp = 0; + } + + if (bus_id != expected_bus_id) return make_error_code(CanErrc::wrong_bus); + if (!acf::evt_row2_is_plain(evt_op)) return make_error_code(CanErrc::bad_evt); + + if (payload.size() < kArbitrationPrefixLen) return make_error_code(CanErrc::short_frame); + const FrameFormat frame_format = detail::read_frame_format(payload.data()); + if (!frame_format_valid(static_cast(frame_format))) return make_error_code(CanErrc::bad_frame_format); + + const size_t prefix_len = detail::prefix_len_for(frame_format); + if (payload.size() < prefix_len) return make_error_code(CanErrc::short_frame); + + detail::read_prefix(payload.data(), frame_format, out_arbitration_id, out_xl_header); + if (!arbitration_id_valid(frame_format, out_arbitration_id)) + return make_error_code(CanErrc::bad_arbitration_id); + + out_format = frame_format; + out_rx_data = std::vector(payload.begin() + static_cast(prefix_len), payload.end()); + out_timed = timed; + out_timestamp = timestamp; + out_transaction_num = hdr.transaction_num; + return {}; +} + +// ── Fragmented response (rcp/fragment.hpp) ──────────────────────────────────── +// See this file's own header comment ("Fragmentation wiring") for the full +// picture. Ported from rcp_ep_can_frame_response_fragment_count()/ +// _encode_frame_response_fragmented()/_decode_frame_response_fragment()/ +// _decode_reassembled_frame_response(). + +inline size_t frame_response_fragment_count(FrameFormat frame_format, uint32_t arbitration_id, + const std::optional& xl_header, size_t rx_len, + size_t max_fragment_payload) noexcept { + if (!detail::encode_preconditions_ok(frame_format, arbitration_id, xl_header, rx_len)) return 0; + const size_t combined_len = detail::prefix_len_for(frame_format) + rx_len; + const size_t count = fragment::plan_count(combined_len, max_fragment_payload); + if (count > kMaxFragmentSegments) return 0; + return count; +} + +// Encodes a CAN frame response as one or more ACF frames, fragmenting via +// rcp::fragment's ms/segment_num mechanism whenever the combined +// prefix-then-data payload exceeds max_fragment_payload octets. Every +// fragment shares byte_bus_id/evt(0)/op(read)/transaction_num/timed/ +// timestamp — frame_format itself lives inside the combined payload's own +// leading quadlet, not per-fragment header state. When the combined payload +// already fits in one fragment, this produces exactly one frame identical +// to what encode_frame_response() itself would have produced. Returns an +// empty vector under the same conditions frame_response_fragment_count() +// returns 0 for. Does not itself apply any E2E CRC — see +// encode_frame_request_fragmented()'s own doc comment. +inline std::vector> +encode_frame_response_fragmented(avtp::ByteBusId byte_bus_id, FrameFormat frame_format, + uint32_t arbitration_id, const std::optional& xl_header, + const std::vector& rx_data, uint8_t transaction_num, + bool timed, uint64_t timestamp, size_t max_fragment_payload) { + const size_t count = frame_response_fragment_count(frame_format, arbitration_id, xl_header, + rx_data.size(), max_fragment_payload); + if (count == 0) return {}; + + std::array combined{}; + const size_t combined_len = + detail::build_payload(frame_format, arbitration_id, xl_header, rx_data.data(), rx_data.size(), combined); + + std::array segs{}; + if (fragment::plan(combined_len, max_fragment_payload, segs.data(), count)) return {}; + + std::vector> out_frames; + out_frames.reserve(count); + for (size_t i = 0; i < count; ++i) { + std::vector slice(combined.begin() + static_cast(segs[i].offset), + combined.begin() + static_cast(segs[i].offset + segs[i].len)); + + if (timed) { + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read + hdr.rsp = true; + hdr.mtv = true; + hdr.transaction_num = transaction_num; + hdr.ms = segs[i].ms; + hdr.read_size_or_segment_num = segs[i].ms ? segs[i].segment_num : 0; + out_frames.push_back(acf::encode_acf_gbb(hdr, timestamp, slice)); + } else { + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read + hdr.rsp = true; + hdr.transaction_num = transaction_num; + hdr.ms = segs[i].ms; + hdr.read_size_or_segment_num = segs[i].ms ? segs[i].segment_num : 0; + out_frames.push_back(acf::encode_acf_abb(hdr, slice)); + } + } + return out_frames; +} + +// Decodes one fragment of a (possibly multi-fragment) CAN frame response +// from b[0..len) — the same peek-message-type/byte_bus_id validation +// decode_frame_response() applies, but this function does NOT strip Figure +// 39's leading-quadlet-then-data layout from the payload (a fragment other +// than the first may not even contain the whole leading quadlet — +// fragmentation operates on the flat combined byte sequence, agnostic to +// its own internal structure). Since frame_format lives inside that +// leading quadlet rather than in evt, it is not obtainable per-fragment at +// all — decode_reassembled_frame_response() recovers it once, after +// reassembly. This function instead surfaces the fragment's own ms bit, +// read_size_or_segment_num (as *out_segment_num, meaningful only when +// *out_ms), and raw ACF payload, for a caller to feed straight into a +// rcp::fragment::Reassembler. +inline std::error_code decode_frame_response_fragment(const uint8_t* b, size_t len, + avtp::ByteBusId expected_bus_id, bool& out_ms, + uint16_t& out_segment_num, + std::vector& out_payload, bool& out_timed, + uint64_t& out_timestamp, + uint8_t& out_transaction_num) { + uint8_t msg_type; + if (acf::peek_msg_type(b, len, msg_type)) return make_error_code(CanErrc::short_frame); + + acf::AcfMessageInfo hdr; + std::vector payload; + avtp::ByteBusId bus_id; + uint8_t evt_op; + bool timed; + uint64_t timestamp; + + if (msg_type == acf::kAcfMsgTypeGbb) { + uint64_t ts = 0; + auto ec = acf::decode_acf_gbb(b, len, hdr, ts, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(CanErrc::short_frame); + if (ec) return make_error_code(CanErrc::bad_msg_type); + bus_id = hdr.byte_bus_id; + evt_op = hdr.evt_op; + timed = hdr.mtv; + timestamp = timed ? ts : 0; + } else { + auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(CanErrc::short_frame); + if (ec) return make_error_code(CanErrc::bad_msg_type); + bus_id = hdr.byte_bus_id; + evt_op = hdr.evt_op; + timed = false; + timestamp = 0; + } + + if (bus_id != expected_bus_id) return make_error_code(CanErrc::wrong_bus); + if (!acf::evt_row2_is_plain(evt_op)) return make_error_code(CanErrc::bad_evt); + + out_ms = hdr.ms; + out_segment_num = hdr.read_size_or_segment_num; + out_payload = std::move(payload); + out_timed = timed; + out_timestamp = timestamp; + out_transaction_num = hdr.transaction_num; + return {}; +} + +// Applies Figure 39's leading-quadlet-then-data parsing to a fully +// reassembled combined payload — rcp::fragment::Reassembler::data()/size()'s +// output once feed() has reported ReasmResult::kComplete. This is the +// second half of what decode_frame_response() does in one step for a +// single, unfragmented frame. +inline std::error_code decode_reassembled_frame_response(const uint8_t* reassembled, size_t reassembled_len, + FrameFormat& out_format, + uint32_t& out_arbitration_id, + XlHeader& out_xl_header, + std::vector& out_rx_data) { + if (reassembled_len < kArbitrationPrefixLen) return make_error_code(CanErrc::short_frame); + const FrameFormat frame_format = detail::read_frame_format(reassembled); + if (!frame_format_valid(static_cast(frame_format))) return make_error_code(CanErrc::bad_frame_format); + + const size_t prefix_len = detail::prefix_len_for(frame_format); + if (reassembled_len < prefix_len) return make_error_code(CanErrc::short_frame); + + detail::read_prefix(reassembled, frame_format, out_arbitration_id, out_xl_header); + if (!arbitration_id_valid(frame_format, out_arbitration_id)) + return make_error_code(CanErrc::bad_arbitration_id); + + out_format = frame_format; + out_rx_data = std::vector(reassembled + prefix_len, reassembled + reassembled_len); + return {}; } // ── CanEndpoint ─────────────────────────────────────────────────────────────── -// No TriggerRegistry member — see header comment point 3. transmit/receive -// model one data-frame exchange each; receive() applies the configured -// acceptance filters (general, then CAN-XL-specific for an XL-format frame) -// when any are configured, dropping frames that match none. +// No TriggerRegistry member: unlike every other device-facing endpoint type +// in this codebase, the specification defines no trigger-signal table for +// CAN at all (extraction §7) — CanEndpoint deliberately has no +// TriggerRegistry member and no signal-id helper function. +// +// transmit()/receive()/handle_request()/last_transmitted()/last_received() +// below are UNCHANGED from this header's pre-existing content (signature +// and behavior) — rcp/mock.hpp's dispatch_can() already calls +// `can_.handle_request(req.evt_op, frame)` with this exact signature and +// this pass does not touch rcp/mock.hpp. class CanEndpoint { public: - std::error_code configure_bit_timing(CanBitTimingConfig cfg) noexcept { - timing_ = cfg; - return {}; - } - const CanBitTimingConfig& bit_timing() const noexcept { return timing_; } + CanFunctionalConfig& functional_config() noexcept { return cfg_; } + const CanFunctionalConfig& functional_config() const noexcept { return cfg_; } + // General (non-XL) acceptance-filter bank — see CanAcceptanceFilter's + // own comment for why this has no c-RCP equivalent. void set_acceptance_filters(std::vector filters) { filters_ = std::move(filters); } const std::vector& acceptance_filters() const noexcept { return filters_; } - // set_xl_receive_filters configures CAN-XL-specific receive filters, - // kept as a bank distinct from the general acceptance_filters() above - // (extraction §5.11) — a CAN XL frame is matched against this bank - // instead of the general one when it is non-empty. - void set_xl_receive_filters(std::vector filters) { - xl_filters_ = std::move(filters); - } - const std::vector& xl_receive_filters() const noexcept { return xl_filters_; } - std::error_code transmit(CanDataFrame frame) { auto ec = validate_frame(frame); if (ec) return ec; @@ -283,57 +1207,30 @@ class CanEndpoint { return {}; } - // handle_request is CAN's request-decode entry point — the piece this - // header previously had none of, mirroring rcp::i2c::I2cEndpoint:: - // handle_request's shape (this repo's fifth Table 33 Row 2 endpoint - // type after I2C, ADC, PWM_IN, and LIN). It classifies the incoming - // request's evt[2:0] field via rcp::endpoint::evt_row2_kind_of before - // doing anything else, so a Reserved value can never reach transmit() - // and be misread as an ordinary transmit request, and a ConfigWrite - // value can never be silently accepted or silently dropped: + // handle_request is CAN's request-decode entry point, mirroring + // rcp::i2c::I2cEndpoint::handle_request's shape (this repo's fifth + // Table 33 Row 2 endpoint type after I2C, ADC, PWM_IN, and LIN). It + // classifies the incoming request's evt[2:0] field via + // rcp::endpoint::evt_row2_kind_of before doing anything else, so a + // Reserved value can never reach transmit() and be misread as an + // ordinary transmit request, and a ConfigWrite value can never be + // silently accepted or silently dropped: // - Plain (evt[2:0] == 000b): delegates straight to transmit() with - // `frame` unchanged — CAN's existing data-frame transmit model - // (extraction §13.7.11.3) already IS this row's correct "plain - // request" behavior; evt[2:0] carries no combinable value or - // channel selector for this row. + // `frame` unchanged. // - Reserved (evt[2:0] in 001b-110b): returns // endpoint::EndpointErrc::reserved_evt_row2 without touching any - // endpoint state (last_tx_ is left exactly as it was — `frame` is - // simply discarded) — TC18 requires this be rejected with error - // code UNSUPPORTED_CMD. - // - ConfigWrite (evt[2:0] == 111b): §12.7.1's configuration-write - // shape targets the CAN EP's own functional-config block (Table 56 - // — bit-timing registers, acceptance/receive filters, clock - // divider, ...), not a frame transmission at all. Full handling is - // deliberately out of scope for this milestone (nontrivial — it - // needs EP_functional-config wiring this header does not yet have, - // the same gap I2C's, ADC's, PWM_IN's, and LIN's own handle_request - // comments defer for the identical reason); this returns - // CanErrc::config_write_not_supported rather than crashing, - // silently accepting the request as a transmit, or silently doing - // nothing. + // endpoint state (last_tx_ is left exactly as it was). + // - ConfigWrite (evt[2:0] == 111b): returns + // CanErrc::config_write_not_supported — this handle_request/ + // transmit() object-model path is deliberately independent of the + // new apply_reconfig()/CanFunctionalConfig surface added by this + // pass, same as rcp/lin.hpp's identical note. // - // NOT to be confused with Figure 40's own "FrameFormat" sub-field - // (CBFF/CEFF/FBFF/FEFF/XL-classic/XL-new, Table 57), carried inside the - // request's byte_msg_payload alongside the CAN ID and CAN data: that - // field selects the CAN frame's own on-wire format and lives entirely - // in the payload, verified directly against TC18.txt's own Figure 40 - // layout (§13.7.11.3), where the Message Info octet's evt[2:0] bits and - // the Payload's FrameFormat/CAN-ID/CAN-data fields are drawn as - // distinct, non-overlapping regions of the request. Reading evt[2:0] as - // if it also selected (or combined with) FrameFormat, or as if it - // selected a remote-frame vs data-frame request — this endpoint type - // has no remote-frame concept at all (see header comment point 1; - // TC18's own text states outright "Sending remote frames is not - // supported") — would be exactly the kind of invented, non-spec- - // derived field encoding this codebase has had to remove elsewhere once - // discovered (e.g. rcp/iseled.hpp's and rcp/mdio.hpp's own header - // comments on previously invented, non-spec-derived field encodings - // later corrected, and the identical class of mistake rcp/lin.hpp's own - // handle_request comment calls out against confusing evt[2:0] with - // §13.7.10.1's separate compound-wait match-condition text). - // handle_request below calls the same shared evt_row2_kind_of every - // other Row 2 endpoint type uses and invents nothing of its own. + // NOT to be confused with Figure 39's own "FrameFormat" sub-field, + // carried inside the request's own payload alongside the arbitration id + // and CAN data: that field selects the CAN frame's own on-wire format + // and lives entirely in the payload — see this file's own header + // comment and encode_frame_request()/decode_frame_request() above. std::error_code handle_request(uint8_t evt_op, CanDataFrame frame) { switch (endpoint::evt_row2_kind_of(evt_op)) { case endpoint::EvtRow2Kind::Plain: @@ -347,18 +1244,29 @@ class CanEndpoint { } // receive models one inbound data frame arriving off the bus. Returns - // false (and does not record the frame) when the frame's format-scoped - // filter bank is non-empty and none of its entries match — true - // otherwise, including whenever no filter bank applies at all. + // false (and does not record the frame) when the frame's own + // format-scoped filter bank has at least one enabled entry and none of + // them match — true otherwise, including whenever no filter bank + // applies at all. A CAN XL frame is matched against + // functional_config().xl_filters (Table 56's own register-modeled + // filter table); every other frame is matched against the general + // acceptance_filters() bank (this implementation's own extension). bool receive(CanDataFrame frame) { - const std::vector& bank = - (frame.format == FrameFormat::Xl && !xl_filters_.empty()) ? xl_filters_ : filters_; - if (!bank.empty()) { - const bool matched = std::any_of(bank.begin(), bank.end(), [&](const CanAcceptanceFilter& f) { - return acceptance_filter_matches(f, frame.id); - }); - if (!matched) return false; + bool matched; + if (frame_format_is_xl(frame.format)) { + const bool any_enabled = std::any_of(cfg_.xl_filters.begin(), cfg_.xl_filters.end(), + [](const CanXlFilter& f) { return f.enable; }); + matched = !any_enabled || + std::any_of(cfg_.xl_filters.begin(), cfg_.xl_filters.end(), [&](const CanXlFilter& f) { + return f.enable && (frame.arbitration_id & f.mask) == (f.id & f.mask); + }); + } else { + matched = filters_.empty() || + std::any_of(filters_.begin(), filters_.end(), [&](const CanAcceptanceFilter& f) { + return acceptance_filter_matches(f, frame.format, frame.arbitration_id); + }); } + if (!matched) return false; last_rx_ = std::move(frame); return true; } @@ -367,9 +1275,8 @@ class CanEndpoint { const CanDataFrame& last_received() const noexcept { return last_rx_; } private: - CanBitTimingConfig timing_; + CanFunctionalConfig cfg_; std::vector filters_; - std::vector xl_filters_; CanDataFrame last_tx_; CanDataFrame last_rx_; }; @@ -377,8 +1284,10 @@ class CanEndpoint { } // namespace can } // namespace rcp -// Enable std::error_code construction from rcp::can::CanErrc. +// Enable std::error_code construction from rcp::can::CanErrc/CanReconfigErrc. namespace std { template <> struct is_error_code_enum : true_type {}; +template <> +struct is_error_code_enum : true_type {}; } // namespace std diff --git a/include/rcp/lin.hpp b/include/rcp/lin.hpp index b75d68a..e1acd2a 100644 --- a/include/rcp/lin.hpp +++ b/include/rcp/lin.hpp @@ -13,72 +13,75 @@ // generation, no schedule tables (extraction §5.10, §6 "significant // behavioral-scope question"). // -// ROADMAP.md milestone 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN -// XL), ISELED, MDIO, Wakeup Control (v2.7.0)": LIN is deliberately the -// simplest of this milestone's five endpoint types by design, not by -// omission — the specification puts every bit of LIN frame construction -// (break/sync generation, PID computation, checksum selection between -// classic and enhanced, schedule-table sequencing) on the client-side -// driver, not on the RC-Server endpoint. This is an explicit deviation from -// the pre-replacement `rcp/linbr.hpp` bridge's shape: that stub (now -// DEPRECATE per the Satellite Package Disposition table, left untouched by -// this milestone) was built against the old rcp::Controller/Zone/Command -// model bridging *from* a Zone, which is the inverse direction and had no -// occasion to assume the endpoint understood frame structure one way or the -// other — this header exists to make explicit, in code, that a native -// LIN-commander endpoint carries none of that structure either, so a future -// reader does not carry over any frame-aware assumption from `linbr.hpp`'s -// era. `LinEndpoint::transfer` below mirrors `rcp::i2c::I2cEndpoint:: -// transfer`'s raw byte-stream shape for exactly this reason: both endpoint -// types push/pull opaque bytes and leave interpretation to the caller. +// ROADMAP.md "Phase 17" (cpp-RCP issue #129), Phase 3 ("Per-endpoint +// modules"): ported from c-RCP's include/rcp/ep_lin.h + src/ep_lin.c, this +// project's RC5-spec-conformant reference implementation for this endpoint +// type. No spec prose, bit layout, or numeric constant is reproduced here. // -// Table 30/33 Row 2 evt[2:0] validation (post-v2.7.0, fourth endpoint type -// after I2C, ADC, and PWM_IN): LinEndpoint::handle_request is this header's -// own wiring of rcp::endpoint::evt_row2_kind_of — the shared 3-way evt[2:0] -// classifier for Table 33's {ADC, PWM_IN, I2C, LIN, CAN, UART, ISELED, -// MDIO} row — into LIN's request decode, following the exact shape -// rcp/i2c.hpp's I2cEndpoint::handle_request, rcp/adc.hpp's -// AdcEndpoint::handle_request, and rcp/pwm.hpp's PwmInEndpoint:: -// handle_request established. Plain (evt[2:0]==000b) delegates straight to -// the existing transfer() above, unchanged — LIN's raw-byte-pusher model -// already IS this row's correct "plain request" behavior, the same -// reasoning I2C's own handle_request gave for its identical shape. Reserved -// (001b-110b) is rejected with endpoint::EndpointErrc::reserved_evt_row2 -// without touching out_bytes/in_bytes or any transfer state. ConfigWrite -// (evt[2:0]==111b, §12.7.1) is reported as the new -// LinErrc::config_write_not_supported rather than crashing or silently -// accepted as a plain transfer — LIN has no EP_functional-config wiring in -// this codebase yet (same gap I2C's, ADC's, and PWM_IN's own handle_request -// comments call out for their own endpoint types), so full §12.7.1 handling -// is out of scope here too. +// This pass is additive on top of what this file already had (the +// LinEndpoint object model — transfer()/handle_request()/triggers() — from +// the pre-rewrite "Table 30/33 Row 2 evt[2:0] validation" milestone, KEPT +// with its exact existing signatures because rcp/mock.hpp's dispatch_lin +// already calls `lin_.handle_request(req.evt_op, payload, lin_response_, +// lin_responded_)` and this pass does not touch rcp/mock.hpp at all — see +// ROADMAP.md Phase 17 item 4, "Server/dispatch", which is where mock.hpp's +// own dispatch-loop rewiring belongs, not here): // -// NOT to be confused with §13.7.10.1's separate "conditions given by -// evt[2:0]" text describing pending-read-request byte-sequence matching -// against bus traffic (LIN's own analog of I2C's compound_wait_matches_bits -// — a different mechanism, out of this milestone's scope, not modeled by -// this header at all): that text is about compound-wait match conditions, -// not about Table 33's top-level Plain/Reserved/ConfigWrite request -// classification handle_request implements below. Reading the two as one -// "evt[2:0] selects a comparison mode" scheme would be exactly the kind of -// invented, non-spec-derived encoding this codebase has had to remove -// elsewhere once discovered (e.g. rcp/iseled.hpp's and rcp/mdio.hpp's own -// header comments on previously invented, non-spec-derived field encodings -// later corrected) — handle_request below calls the same shared -// evt_row2_kind_of every other Row 2 endpoint type uses and invents nothing -// of its own; §13.7.10.1's pending-read match semantics remain unimplemented -// here, called out rather than silently guessed at. +// - response_matches()/LinTrigger/trigger_fires(): ported from +// rcp_ep_lin_response_matches()/rcp_ep_lin_trigger_t/ +// rcp_ep_lin_trigger_fires(). response_matches() is a thin wrapper over +// rcp/acf.hpp's own compound_wait_match(evt=0, ...) (§13.5.1 mode 000b, +// exact match) — LIN's own analogue of I2C's compound_wait_matches_bits, +// not a fresh comparison routine. +// - LinFunctionalConfig + functional_cfg_writable()/set_clk_divider()/ +// set_trigger(): ported from rcp_ep_lin_functional_cfg_t and its +// setters, gated the same way every c-RCP endpoint type's functional +// config is: rcp::lifecycle::field_writable() with FieldKind::FunctionalW +// (c-RCP's RCP_LIFECYCLE_FIELD_FUNCTIONAL_W). The five ep_enable/ +// ep_clear_req_storage/ep_req_crc_enable/ep_response_ts_enable/ +// ep_suppress_response flags c-RCP composes via its own +// rcp_regmap_ep_functional_cfg_t "common" prefix are kept as plain +// members on LinFunctionalConfig directly instead — cpp-RCP's own +// rcp/regmap.hpp EndpointFunctionalConfig is still the pre-rewrite +// opaque-byte-blob shape as of this pass (regmap.hpp's real content +// port is ROADMAP.md Phase 17 item 4, not this one) and has no such +// five-flag "common" struct yet to compose against — see the +// TODO(phase3-followup) below. +// - The EP_func register block (TC18 §13.7.10.2 Table 55, evt[2:0]==111b +// target): render_registers()/apply_reconfig()/encode_reconfig_request(), +// ported from rcp_ep_lin_render_registers()/_apply_reconfig()/ +// _encode_reconfig_request(). kEpFuncLen (0x09) and every register +// offset below match Table 55 exactly, per c-RCP's own already-verified +// (no address-collision defect, unlike CAN's/GPIO's/I2C's own source +// tables) rendering of it. +// - The real ACF-level wire codec — encode_command_request()/ +// decode_command_request()/encode_response()/decode_response() — ported +// from rcp_ep_lin_encode_command_request()/_decode_command_request()/ +// _encode_response()/_decode_response(). LIN's payload never approaches +// ACF's own per-message ceiling (unlike CAN XL — see rcp/can.hpp), so +// unlike that module this one needs no rcp/fragment.hpp wiring at all. +// +// TODO(phase3-followup): once rcp/regmap.hpp's own functional-config split +// is re-derived from c-RCP (ROADMAP.md Phase 17 item 4), recompose +// LinFunctionalConfig's five ep_enable/ep_clear_req_storage/ep_req_crc_enable/ +// ep_response_ts_enable/ep_suppress_response flags on top of that shared +// struct (matching rcp_ep_lin_functional_cfg_t's own `common` composition) +// instead of carrying local duplicates of them here. // // 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. The concrete transfer-shape // and trigger-signal id encoding chosen in this file are this // implementation's own, same as the equivalent disclaimers in rcp/avtp.hpp, -// rcp/regmap.hpp, rcp/endpoint.hpp, rcp/i2c.hpp, rcp/adc.hpp, and -// rcp/pwm.hpp. +// rcp/acf.hpp, rcp/endpoint.hpp, rcp/i2c.hpp, rcp/adc.hpp, and rcp/pwm.hpp. #pragma once +#include #include +#include +#include +#include #include #include #include @@ -89,10 +92,11 @@ namespace rcp { namespace lin { // ── Errors ──────────────────────────────────────────────────────────────────── -// no_response is this header's only defined failure: the commander pushed -// bytes onto the bus and nothing came back within whatever window the -// caller's driver layer enforces (this header has no clock of its own, same -// disclaimer as every other bus endpoint type in this codebase). +// no_response/config_write_not_supported (values 1/2) are unchanged from +// this header's pre-existing content — rcp/mock.hpp's wire_error_code_for() +// compares against both by name. short_frame..bad_evt (3..6) are new, +// ported from rcp_ep_lin_errc_t (RCP_EP_LIN_ERR_*), for the new wire-codec +// functions below. enum class LinErrc : int { no_response = 1, @@ -104,6 +108,11 @@ enum class LinErrc : int { // plain transfer or silently ignored, same as I2C's, ADC's, and // PWM_IN's own config_write_not_supported variants. config_write_not_supported = 2, + short_frame = 3, // ported from RCP_EP_LIN_ERR_SHORT_FRAME + bad_msg_type = 4, // ported from RCP_EP_LIN_ERR_BAD_MSG_TYPE + wrong_bus = 5, // ported from RCP_EP_LIN_ERR_WRONG_BUS + wrong_op = 6, // ported from RCP_EP_LIN_ERR_WRONG_OP + bad_evt = 7, // ported from RCP_EP_LIN_ERR_BAD_EVT }; inline const std::error_category& lin_category() noexcept { @@ -114,6 +123,11 @@ inline const std::error_category& lin_category() noexcept { case LinErrc::no_response: return "rcp/lin: no response observed on the bus"; case LinErrc::config_write_not_supported: return "rcp/lin: evt[2:0]=111b configuration-write requests are not yet implemented"; + case LinErrc::short_frame: return "rcp/lin: frame too short"; + case LinErrc::bad_msg_type: return "rcp/lin: unexpected ACF message type"; + case LinErrc::wrong_bus: return "rcp/lin: wrong byte_bus_id"; + case LinErrc::wrong_op: return "rcp/lin: wrong ACF op"; + case LinErrc::bad_evt: return "rcp/lin: evt[2:0] is not plain (000b)"; default: return "rcp/lin: unknown error"; } } @@ -139,6 +153,361 @@ constexpr endpoint::TriggerRegistry::SignalId lin_signal_id(LinSignal sig) noexc return static_cast(sig); } +// ── evt[2:0]: exact-match, per Table 33's plain-request row ───────────────── +// response_matches is a thin, named wrapper over rcp/acf.hpp's +// compound_wait_match(evt=0, ...) — TC18 §13.5.1 mode 000b, exact match, +// length-capped — the only comparison a plain LIN command request's +// evt[2:0] can ever select (Table 33 constrains it to 000b). Ported from +// rcp_ep_lin_response_matches(). tx_data/rx_data may be empty. +inline bool response_matches(const uint8_t* tx_data, size_t tx_len, const uint8_t* rx_data, + size_t rx_len) noexcept { + return acf::compound_wait_match(0x0u, tx_data, tx_len, rx_data, rx_len); +} +inline bool response_matches(const std::vector& tx, const std::vector& rx) noexcept { + return response_matches(tx.data(), tx.size(), rx.data(), rx.size()); +} + +// ── Transmission-done trigger (entirely this module's own design — TC18 +// defines no "lin trigger outputs" table at all, per c-RCP's own +// c-RCP-AUDIT-06/issue #256 Group C finding) ───────────────────────────── + +enum class LinTrigger : uint8_t { None = 0, TxDone = 1 }; + +// True iff tx_done_event and trailing_time_expired together satisfy +// trigger: never for None; for TxDone iff BOTH are true. Ported from +// rcp_ep_lin_trigger_fires(). +inline bool trigger_fires(LinTrigger trigger, bool tx_done_event, + bool trailing_time_expired) noexcept { + switch (trigger) { + case LinTrigger::TxDone: return tx_done_event && trailing_time_expired; + case LinTrigger::None: + default: return false; + } +} + +// ── Functional config ───────────────────────────────────────────────────────── +// Ported from rcp_ep_lin_functional_cfg_t — see this file's own header +// comment for why the five Table 35 "common" flags are local members here +// rather than composed from rcp/regmap.hpp's EndpointFunctionalConfig. + +struct LinFunctionalConfig { + bool ep_enable = false; + bool ep_clear_req_storage = false; + bool ep_req_crc_enable = false; + bool ep_response_ts_enable = false; + bool ep_suppress_response = false; + + uint32_t lin_clk_divider = 0; // bit-time clock divider; see the file header + LinTrigger trigger = LinTrigger::None; + uint16_t ep_status = 0; // lin_ep_status, Table 55 + uint8_t wire_clk_divider = 0; // lin_clk_divider, Table 55 — see the file header +}; + +// functional_cfg_writable is a thin, named wrapper over +// rcp::lifecycle::field_writable() with FieldKind::FunctionalW — ported +// from rcp_ep_lin_functional_cfg_writable(). Reuses, and never duplicates, +// that function's authorization logic. +inline bool functional_cfg_writable(lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + return lifecycle::field_writable(state, lifecycle::FieldKind::FunctionalW, writer); +} + +// Sets cfg.lin_clk_divider iff functional_cfg_writable() authorizes the +// write; returns whether the write was applied. cfg is left entirely +// unchanged when it returns false. Ported from rcp_ep_lin_set_clk_divider(). +inline bool set_clk_divider(LinFunctionalConfig& cfg, uint32_t lin_clk_divider, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.lin_clk_divider = lin_clk_divider; + return true; +} + +// Same authorization rule, for cfg.trigger. Ported from +// rcp_ep_lin_set_trigger(). +inline bool set_trigger(LinFunctionalConfig& cfg, LinTrigger trigger, lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.trigger = trigger; + return true; +} + +// ── The EP_func register block (evt[2:0] == 111b target), TC18 §13.7.10.2 +// Table 55 ────────────────────────────────────────────────────────────────── +// +// 0x0000 lin_ep_len 8 bit R kEpFuncLen (0x09) +// 0x0001 Reserved 8 bit R reads 0x00 +// 0x0002 lin_ep_enable&clr 8 bit R/W Table 35 common entries +// 0x0003 lin_ep_options 8 bit R/W* Table 35 common entries +// 0x0004 lin_base_clk 16 bit R LIN system clock (always renders 0 +// — no real clock source modelled, +// same honesty every other endpoint +// type's own base_clk field commits +// to) +// 0x0006 lin_ep_status 16 bit R/W +// 0x0008 lin_clk_divider 8 bit R/W generates the LIN bit time +// +// Ported from ep_lin.c's RCP_EP_LIN_REG_*/rcp_ep_lin_render_registers()/ +// rcp_ep_lin_apply_reconfig(). + +constexpr uint16_t kRegEpLen = 0x0000; +constexpr uint16_t kRegReserved01 = 0x0001; +constexpr uint16_t kRegEpEnableClr = 0x0002; +constexpr uint16_t kRegEpOptions = 0x0003; +constexpr uint16_t kRegBaseClk = 0x0004; +constexpr uint16_t kRegEpStatus = 0x0006; +constexpr uint16_t kRegClkDivider = 0x0008; + +constexpr size_t kEpFuncLen = 0x0009; +constexpr size_t kReconfigAddrLen = 2; + +namespace detail { +constexpr uint8_t kEnableClrBitEnable = (1u << 0); +constexpr uint8_t kEnableClrBitClear = (1u << 4); +constexpr uint8_t kOptionsBitReqCrc = (1u << 0); +constexpr uint8_t kOptionsBitRespTs = (1u << 3); +constexpr uint8_t kOptionsBitSuppress = (1u << 7); + +inline void put_u16(uint8_t* p, uint16_t v) noexcept { + p[0] = static_cast((v >> 8) & 0xFFu); + p[1] = static_cast(v & 0xFFu); +} +inline uint16_t get_u16(const uint8_t* p) noexcept { + return static_cast((static_cast(p[0]) << 8) | p[1]); +} +} // namespace detail + +// Serializes cfg's EP_func registers into out[0..kEpFuncLen) exactly as a +// configuration *read* of the whole block would report them — the inverse +// of apply_reconfig()'s own parse step. Ported from +// rcp_ep_lin_render_registers(). +inline void render_registers(const LinFunctionalConfig& cfg, + std::array& out) noexcept { + uint8_t enable_clr = 0; + uint8_t options = 0; + if (cfg.ep_enable) enable_clr |= detail::kEnableClrBitEnable; + if (cfg.ep_clear_req_storage) enable_clr |= detail::kEnableClrBitClear; + if (cfg.ep_req_crc_enable) options |= detail::kOptionsBitReqCrc; + if (cfg.ep_response_ts_enable) options |= detail::kOptionsBitRespTs; + if (cfg.ep_suppress_response) options |= detail::kOptionsBitSuppress; + + out[kRegEpLen] = static_cast(kEpFuncLen); + out[kRegReserved01] = 0; + out[kRegEpEnableClr] = enable_clr; + out[kRegEpOptions] = options; + detail::put_u16(&out[kRegBaseClk], 0); // no real clock source modelled + detail::put_u16(&out[kRegEpStatus], cfg.ep_status); + out[kRegClkDivider] = cfg.wire_clk_divider; +} + +namespace detail { +inline bool reg_offset_read_only(uint16_t addr) noexcept { + return addr == kRegEpLen || addr == kRegReserved01 || addr == kRegBaseClk || + addr == static_cast(kRegBaseClk + 1); +} +} // namespace detail + +enum class LinReconfigErrc : int { + short_payload = 1, // payload carries no address prefix, or an address prefix with no data octet after it + out_of_range = 2, // start_address + data length exceeds kEpFuncLen — the whole write is ignored +}; + +inline const std::error_category& lin_reconfig_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.lin.reconfig"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case LinReconfigErrc::short_payload: return "rcp/lin: LIN configuration write has no address and data"; + case LinReconfigErrc::out_of_range: return "rcp/lin: LIN configuration write extends past the EP_func block"; + default: return "rcp/lin: LIN unknown configuration-write error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(LinReconfigErrc e) noexcept { + return {static_cast(e), lin_reconfig_category()}; +} + +// Applies the configuration escape hatch (evt[2:0] == 111b): payload is NOT +// presented at the interface but interpreted as an addressed write into +// this endpoint's own EP_func block — a 16-bit big-endian relative start +// address followed by the configuration data octets to write from that +// address onward (§12.7.1). Ported from rcp_ep_lin_apply_reconfig(). cfg is +// left entirely unchanged on error, per the specification's own "such a +// payload is to be ignored" rule. Octets of the addressed span that land on +// a read-only register (EP_LEN, the reserved octet, base_clk) are left at +// their current values while the rest of the span is still applied. +inline std::error_code apply_reconfig(LinFunctionalConfig& cfg, const uint8_t* payload, + size_t payload_len) { + if (payload_len <= kReconfigAddrLen) return make_error_code(LinReconfigErrc::short_payload); + + const uint16_t start_address = detail::get_u16(payload); + const size_t data_len = payload_len - kReconfigAddrLen; + + if (static_cast(start_address) + data_len > kEpFuncLen) + return make_error_code(LinReconfigErrc::out_of_range); + + std::array block{}; + render_registers(cfg, block); + for (size_t i = 0; i < data_len; ++i) { + const auto addr = static_cast(start_address + i); + if (detail::reg_offset_read_only(addr)) continue; // write ignored + block[addr] = payload[kReconfigAddrLen + i]; + } + + cfg.ep_enable = (block[kRegEpEnableClr] & detail::kEnableClrBitEnable) != 0; + cfg.ep_clear_req_storage = (block[kRegEpEnableClr] & detail::kEnableClrBitClear) != 0; + cfg.ep_req_crc_enable = (block[kRegEpOptions] & detail::kOptionsBitReqCrc) != 0; + cfg.ep_response_ts_enable = (block[kRegEpOptions] & detail::kOptionsBitRespTs) != 0; + cfg.ep_suppress_response = (block[kRegEpOptions] & detail::kOptionsBitSuppress) != 0; + cfg.ep_status = detail::get_u16(&block[kRegEpStatus]); + cfg.wire_clk_divider = block[kRegClkDivider]; + + return {}; +} + +// Encodes an ACF_ABB configuration request (evt[2:0] == 111b) addressed to +// byte_bus_id: payload is start_address (16-bit big-endian) followed by +// data. Returns an empty vector if data is empty. Ported from +// rcp_ep_lin_encode_reconfig_request(). +inline std::vector encode_reconfig_request(avtp::ByteBusId byte_bus_id, + uint16_t start_address, + const std::vector& data, + uint8_t transaction_num) { + if (data.empty()) return {}; + + std::vector payload(kReconfigAddrLen + data.size()); + detail::put_u16(payload.data(), start_address); + std::copy(data.begin(), data.end(), payload.begin() + static_cast(kReconfigAddrLen)); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = true; // write — §12.7.1 Figure 18 + hdr.evt_op = 0x7u; // evt[2:0] = 111b + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, payload); +} + +// ── ACF-level wire codec: command request / response ───────────────────────── +// Ported from rcp_ep_lin_encode_command_request()/_decode_command_request()/ +// _encode_response()/_decode_response(). LIN's payload never approaches +// ACF's own per-message ceiling, so this module needs no fragmentation. + +// Encodes an ACF_ABB command request addressed to byte_bus_id: the payload +// is exactly tx_data, the raw bytes driven directly onto the bus for this +// transaction — every LIN-frame semantic already constructed into these +// bytes by the caller (see the file header). Encoded as a read-direction +// request (op=false): a LIN command request expects the endpoint to reply +// with the bytes received on the bus, so encoding it as a write would tell +// a conforming peer the opposite of what the request means. evt is always 0. +inline std::vector encode_command_request(avtp::ByteBusId byte_bus_id, + const std::vector& tx_data, + uint8_t transaction_num) { + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read direction — see this function's own comment + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, tx_data); +} + +// Decodes and validates an ACF-level LIN command request from b[0..len). +// Fails with LinErrc::short_frame/bad_msg_type/wrong_bus/wrong_op/bad_evt — +// see ep_lin.c's own doc comment for the exact condition each maps to. On +// success, *out_tx_data/*out_transaction_num are populated; payload is +// round-tripped verbatim, byte for byte, with no protocol-level LIN-frame +// parsing of any kind. +inline std::error_code decode_command_request(const uint8_t* b, size_t len, + avtp::ByteBusId expected_bus_id, + std::vector& out_tx_data, + uint8_t& out_transaction_num) { + acf::AcfMessageInfo hdr; + std::vector payload; + auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(LinErrc::short_frame); + if (ec) return make_error_code(LinErrc::bad_msg_type); + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(LinErrc::wrong_bus); + if (hdr.op) return make_error_code(LinErrc::wrong_op); // read direction — see encode_command_request() + if (!acf::evt_row2_is_plain(hdr.evt_op)) return make_error_code(LinErrc::bad_evt); + + out_tx_data = std::move(payload); + out_transaction_num = hdr.transaction_num; + return {}; +} + +// Encodes a LIN response carrying rx_data (the raw bytes captured back from +// the bus) as its payload, echoing transaction_num. Encoded as ACF_ABB when +// timed is false; as ACF_GBB (message_timestamp = timestamp, mtv = true) +// when timed is true. +inline std::vector encode_response(avtp::ByteBusId byte_bus_id, + const std::vector& rx_data, + uint8_t transaction_num, bool timed, + uint64_t timestamp) { + if (timed) { + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; + hdr.rsp = true; + hdr.mtv = true; + hdr.transaction_num = transaction_num; + return acf::encode_acf_gbb(hdr, timestamp, rx_data); + } + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; + hdr.rsp = true; + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, rx_data); +} + +// Decodes a LIN response from either an ACF_ABB or ACF_GBB message (peeks +// the ACF message type itself, unlike decode_command_request(), since a +// response's encoding depends on the responding endpoint's own +// timed/untimed choice). Fails with LinErrc::short_frame/bad_msg_type/ +// wrong_bus. On success, every output parameter is populated; *out_timed +// and *out_timestamp report whether the message was ACF_GBB with a valid +// timestamp, and that timestamp's value (0 when !*out_timed). +inline std::error_code decode_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + std::vector& out_rx_data, bool& out_timed, + uint64_t& out_timestamp, uint8_t& out_transaction_num) { + uint8_t msg_type; + auto peek_ec = acf::peek_msg_type(b, len, msg_type); + if (peek_ec) return make_error_code(LinErrc::short_frame); + + acf::AcfMessageInfo hdr; + std::vector payload; + avtp::ByteBusId bus_id; + bool timed; + uint64_t timestamp; + + if (msg_type == acf::kAcfMsgTypeGbb) { + uint64_t ts = 0; + auto ec = acf::decode_acf_gbb(b, len, hdr, ts, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(LinErrc::short_frame); + if (ec) return make_error_code(LinErrc::bad_msg_type); + bus_id = hdr.byte_bus_id; + timed = hdr.mtv; + timestamp = timed ? ts : 0; + } else { + auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(LinErrc::short_frame); + if (ec) return make_error_code(LinErrc::bad_msg_type); + bus_id = hdr.byte_bus_id; + timed = false; + timestamp = 0; + } + + if (bus_id != expected_bus_id) return make_error_code(LinErrc::wrong_bus); + + out_rx_data = std::move(payload); + out_timed = timed; + out_timestamp = timestamp; + out_transaction_num = hdr.transaction_num; + return {}; +} + // ── LinEndpoint ─────────────────────────────────────────────────────────────── // Mirrors rcp::i2c::I2cEndpoint's shape deliberately: one request-dispatch // entry point per incoming LIN transfer, recording the raw bytes pushed and @@ -149,6 +518,11 @@ constexpr endpoint::TriggerRegistry::SignalId lin_signal_id(LinSignal sig) noexc // implementation's record of whatever came back over the same exchange // (supplied by the caller — this header models the request/response and // trigger-signal shape of a LIN commander transfer, not an actual bus). +// +// transfer()/handle_request() below are UNCHANGED from this header's +// pre-existing content (signature and behavior) — rcp/mock.hpp's +// dispatch_lin() already calls handle_request() with this exact signature +// and this pass does not touch rcp/mock.hpp. class LinEndpoint { public: std::error_code transfer(std::vector out_bytes, std::vector in_bytes, @@ -163,34 +537,26 @@ class LinEndpoint { return {}; } - // handle_request is LIN's request-decode entry point — the piece this - // header previously had none of, mirroring rcp::i2c::I2cEndpoint:: - // handle_request's shape exactly (this repo's fourth Table 33 Row 2 - // endpoint type after I2C, ADC, and PWM_IN). It classifies the incoming - // request's evt[2:0] field via rcp::endpoint::evt_row2_kind_of before - // doing anything else, so a Reserved value can never reach transfer() - // and be misread as an ordinary transfer, and a ConfigWrite value can - // never be silently accepted or silently dropped: + // handle_request is LIN's request-decode entry point, mirroring + // rcp::i2c::I2cEndpoint::handle_request's shape (this repo's fourth + // Table 33 Row 2 endpoint type after I2C, ADC, and PWM_IN). It + // classifies the incoming request's evt[2:0] field via + // rcp::endpoint::evt_row2_kind_of before doing anything else, so a + // Reserved value can never reach transfer() and be misread as an + // ordinary transfer, and a ConfigWrite value can never be silently + // accepted or silently dropped: // - Plain (evt[2:0] == 000b): delegates straight to transfer() with - // `out_bytes`/`in_bytes`/`responded` unchanged — LIN's existing - // raw-byte-pusher transfer model (extraction §13.7.10.3) already IS - // this row's correct "plain request" behavior; evt[2:0] carries no - // combinable value or channel selector for this row the way it - // does for GPIO/PWM_OUT or SPI. + // `out_bytes`/`in_bytes`/`responded` unchanged. // - Reserved (evt[2:0] in 001b-110b): returns // endpoint::EndpointErrc::reserved_evt_row2 without touching any - // endpoint state or recording anything as sent/received — TC18 - // requires this be rejected with error code UNSUPPORTED_CMD. - // - ConfigWrite (evt[2:0] == 111b): §12.7.1's configuration-write - // shape targets the LIN EP's own functional-config block (relative - // start address + configuration data), not a bus transfer at all. - // Full handling is deliberately out of scope for this milestone - // (nontrivial — it needs EP_functional-config wiring this header - // does not yet have, the same gap I2C's, ADC's, and PWM_IN's own - // handle_request comments defer for the identical reason); this - // returns LinErrc::config_write_not_supported rather than - // crashing, silently accepting the request as a transfer, or - // silently doing nothing. + // endpoint state or recording anything as sent/received. + // - ConfigWrite (evt[2:0] == 111b): returns + // LinErrc::config_write_not_supported — this handle_request/ + // transfer() object-model path is deliberately independent of the + // new apply_reconfig()/LinFunctionalConfig surface added by this + // pass (wiring the two together, like wiring fragmentation into + // mock.hpp's own dispatch loop, is Phase 4's "Server/dispatch" + // scope, not this pass's). std::error_code handle_request(uint8_t evt_op, std::vector out_bytes, std::vector in_bytes, bool responded = true) { switch (endpoint::evt_row2_kind_of(evt_op)) { @@ -208,17 +574,23 @@ class LinEndpoint { const std::vector& last_received() const noexcept { return last_in_; } endpoint::TriggerRegistry& triggers() noexcept { return triggers_; } + LinFunctionalConfig& functional_config() noexcept { return cfg_; } + const LinFunctionalConfig& functional_config() const noexcept { return cfg_; } + private: endpoint::TriggerRegistry triggers_; std::vector last_out_; std::vector last_in_; + LinFunctionalConfig cfg_; }; } // namespace lin } // namespace rcp -// Enable std::error_code construction from rcp::lin::LinErrc. +// Enable std::error_code construction from rcp::lin::LinErrc/LinReconfigErrc. namespace std { template <> struct is_error_code_enum : true_type {}; +template <> +struct is_error_code_enum : true_type {}; } // namespace std diff --git a/tests/test_can.cpp b/tests/test_can.cpp index 82d2a21..c98e5b3 100644 --- a/tests/test_can.cpp +++ b/tests/test_can.cpp @@ -8,15 +8,24 @@ // fusa:test REQ-CANEP-008 // fusa:test REQ-CANEP-009 -// Tests for rcp/can.hpp — the CAN controller endpoint type (ROADMAP.md -// milestone 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN XL), -// ISELED, MDIO, Wakeup Control", v2.7.0). +// Tests for rcp/can.hpp — the CAN controller endpoint type. Ported from +// c-RCP's tests/test_ep_can.c (ROADMAP.md "Phase 17", cpp-RCP issue #129, +// Phase 3 "Per-endpoint modules"), including this pass's centerpiece: CAN +// XL multi-frame fragmentation wired onto rcp/fragment.hpp, mirroring +// c-RCP's own worst-case (2048-octet) fragmented request/response tests +// (c-RCP issues #610/#611/#612/#613) and the oversized-reassembly lesson +// (c-RCP issues #614/#616, documented in fragment.hpp's own header comment). #include #include #include +#include +#include using namespace rcp::can; +using rcp::lifecycle::FieldKind; +using rcp::lifecycle::ServerState; +using rcp::lifecycle::WriterCtx; // ── ep_type id ──────────────────────────────────────────────────────────────── @@ -24,18 +33,75 @@ TEST_CASE("CAN's ep_type id is 0x0B", "[can][REQ-CANEP-001]") { REQUIRE(rcp::endpoint::kEndpointTypeCan == 0x0B); } -// ── Identifier range validation ─────────────────────────────────────────────── +// ── FrameFormat helpers ─────────────────────────────────────────────────────── -TEST_CASE("validate_identifier accepts in-range standard and extended ids", "[can][REQ-CANEP-002]") { - REQUIRE_FALSE(validate_identifier({0x7FF, false})); - REQUIRE_FALSE(validate_identifier({0x1FFFFFFF, true})); +TEST_CASE("frame_format_valid accepts 0..5 and rejects 6/7", "[can][REQ-CANEP-002]") { + for (uint8_t v = 0; v <= 5; ++v) REQUIRE(frame_format_valid(v)); + REQUIRE_FALSE(frame_format_valid(6)); + REQUIRE_FALSE(frame_format_valid(7)); } -TEST_CASE("validate_identifier rejects an out-of-range id for its own width", "[can][REQ-CANEP-002]") { - auto ec = validate_identifier({0x800, false}); // 12 bits, too wide for an 11-bit standard id +TEST_CASE("frame_format_is_xl is true only for the two CAN XL variants", "[can][REQ-CANEP-002]") { + REQUIRE_FALSE(frame_format_is_xl(FrameFormat::Cbff)); + REQUIRE_FALSE(frame_format_is_xl(FrameFormat::Ceff)); + REQUIRE_FALSE(frame_format_is_xl(FrameFormat::Fbff)); + REQUIRE_FALSE(frame_format_is_xl(FrameFormat::Feff)); + REQUIRE(frame_format_is_xl(FrameFormat::XlClassicalPl)); + REQUIRE(frame_format_is_xl(FrameFormat::XlNewPl)); +} + +TEST_CASE("frame_format_id_width: Extended29 for CEFF/FEFF, Base11 for every other defined format", + "[can][REQ-CANEP-002]") { + REQUIRE(frame_format_id_width(FrameFormat::Cbff) == IdWidth::Base11); + REQUIRE(frame_format_id_width(FrameFormat::Ceff) == IdWidth::Extended29); + REQUIRE(frame_format_id_width(FrameFormat::Fbff) == IdWidth::Base11); + REQUIRE(frame_format_id_width(FrameFormat::Feff) == IdWidth::Extended29); + REQUIRE(frame_format_id_width(FrameFormat::XlClassicalPl) == IdWidth::Base11); + REQUIRE(frame_format_id_width(FrameFormat::XlNewPl) == IdWidth::Base11); +} + +TEST_CASE("frame_format_max_data_len: 8/64/2048 per format, 0 for invalid", "[can][REQ-CANEP-003]") { + REQUIRE(frame_format_max_data_len(FrameFormat::Cbff) == kClassicalMaxDataLen); + REQUIRE(frame_format_max_data_len(FrameFormat::Ceff) == kClassicalMaxDataLen); + REQUIRE(frame_format_max_data_len(FrameFormat::Fbff) == kFdMaxDataLen); + REQUIRE(frame_format_max_data_len(FrameFormat::Feff) == kFdMaxDataLen); + REQUIRE(frame_format_max_data_len(FrameFormat::XlClassicalPl) == kXlMaxDataLen); + REQUIRE(frame_format_max_data_len(FrameFormat::XlNewPl) == kXlMaxDataLen); + REQUIRE(frame_format_max_data_len(static_cast(6)) == 0); +} + +TEST_CASE("xl_frame_matches_provisioned_pl: non-XL always matches; XL must match the provisioned PL", + "[can][REQ-CANEP-002]") { + REQUIRE(xl_frame_matches_provisioned_pl(true, FrameFormat::Cbff)); + REQUIRE(xl_frame_matches_provisioned_pl(false, FrameFormat::Feff)); + + REQUIRE(xl_frame_matches_provisioned_pl(true, FrameFormat::XlNewPl)); + REQUIRE_FALSE(xl_frame_matches_provisioned_pl(true, FrameFormat::XlClassicalPl)); + REQUIRE(xl_frame_matches_provisioned_pl(false, FrameFormat::XlClassicalPl)); + REQUIRE_FALSE(xl_frame_matches_provisioned_pl(false, FrameFormat::XlNewPl)); +} + +// ── Identifier range validation, per frame-format id width ─────────────────── + +TEST_CASE("validate_identifier accepts in-range ids for each format's own width", + "[can][REQ-CANEP-002]") { + REQUIRE_FALSE(validate_identifier(FrameFormat::Cbff, 0x7FF)); + REQUIRE_FALSE(validate_identifier(FrameFormat::Ceff, 0x1FFFFFFF)); + REQUIRE_FALSE(validate_identifier(FrameFormat::XlClassicalPl, 0x7FF)); // XL is always base-11 +} + +TEST_CASE("validate_identifier rejects an out-of-range id for its own format's width", + "[can][REQ-CANEP-002]") { + auto ec = validate_identifier(FrameFormat::Cbff, 0x800); // 12 bits, too wide for base-11 + REQUIRE(ec == make_error_code(CanErrc::identifier_out_of_range)); + + ec = validate_identifier(FrameFormat::Ceff, 0x20000000); // 30 bits, too wide for extended-29 REQUIRE(ec == make_error_code(CanErrc::identifier_out_of_range)); - ec = validate_identifier({0x20000000, true}); // 30 bits, too wide for a 29-bit extended id + // A Base11 format never accepts a > 11-bit id, even if it would be + // in-range for Extended29 — id width is derived from frame_format + // alone, not independently configurable. + ec = validate_identifier(FrameFormat::XlNewPl, 0x800); REQUIRE(ec == make_error_code(CanErrc::identifier_out_of_range)); } @@ -43,87 +109,179 @@ TEST_CASE("validate_identifier rejects an out-of-range id for its own width", "[ TEST_CASE("validate_frame enforces each FrameFormat's own payload ceiling", "[can][REQ-CANEP-003]") { CanDataFrame classical; - classical.id.value = 0x100; - classical.format = FrameFormat::Classical; - classical.data.assign(kMaxClassicalPayload, 0xAA); + classical.arbitration_id = 0x100; + classical.format = FrameFormat::Cbff; + classical.data.assign(kClassicalMaxDataLen, 0xAA); REQUIRE_FALSE(validate_frame(classical)); classical.data.push_back(0xAA); // 9 bytes, exceeds Classical's 8-byte ceiling REQUIRE(validate_frame(classical) == make_error_code(CanErrc::payload_exceeds_format_limit)); CanDataFrame fd; - fd.id.value = 0x100; - fd.format = FrameFormat::Fd; - fd.data.assign(kMaxFdPayload, 0x55); + fd.arbitration_id = 0x100; + fd.format = FrameFormat::Fbff; + fd.data.assign(kFdMaxDataLen, 0x55); REQUIRE_FALSE(validate_frame(fd)); fd.data.push_back(0x55); // 65 bytes, exceeds FD's 64-byte ceiling REQUIRE(validate_frame(fd) == make_error_code(CanErrc::payload_exceeds_format_limit)); } -// ── CAN XL: accepted single-AVTPDU limitation ──────────────────────────────── +// ── CAN XL: single-ACF-frame vs. format ceiling vs. fragmentation-required ── -TEST_CASE("validate_frame accepts a CAN XL payload within the single-AVTPDU bound", "[can][REQ-CANEP-004]") { +TEST_CASE("validate_frame accepts a CAN XL payload within the single-ACF-frame bound", + "[can][REQ-CANEP-004]") { CanDataFrame xl; - xl.id.value = 0x100; - xl.format = FrameFormat::Xl; - xl.data.assign(kMaxXlPayloadSingleAvtpdu, 0x11); + xl.arbitration_id = 0x100; + xl.format = FrameFormat::XlClassicalPl; + xl.data.assign(kMaxXlPayloadSingleFrame, 0x11); REQUIRE_FALSE(validate_frame(xl)); } -TEST_CASE("validate_frame reports xl_payload_exceeds_single_avtpdu_bound between the accepted " - "bound and the specification's own ceiling", +TEST_CASE("validate_frame reports xl_payload_exceeds_single_avtpdu_bound between the single-frame " + "bound and the format's own 2048-octet ceiling", "[can][REQ-CANEP-004]") { CanDataFrame xl; - xl.id.value = 0x100; - xl.format = FrameFormat::Xl; - xl.data.assign(kMaxXlPayloadSingleAvtpdu + 1, 0x11); - REQUIRE(kMaxXlPayloadSingleAvtpdu + 1 <= kMaxXlPayloadSpec); + xl.arbitration_id = 0x100; + xl.format = FrameFormat::XlClassicalPl; + xl.data.assign(kMaxXlPayloadSingleFrame + 1, 0x11); + REQUIRE(kMaxXlPayloadSingleFrame + 1 <= kXlMaxDataLen); + REQUIRE(validate_frame(xl) == make_error_code(CanErrc::xl_payload_exceeds_single_avtpdu_bound)); + + // The worst case this pass's fragmentation wiring exists for: exactly + // kXlMaxDataLen (2048) octets. + xl.data.assign(kXlMaxDataLen, 0x11); REQUIRE(validate_frame(xl) == make_error_code(CanErrc::xl_payload_exceeds_single_avtpdu_bound)); } -TEST_CASE("validate_frame reports payload_exceeds_format_limit beyond the specification's own " - "2054-byte CAN XL ceiling", +TEST_CASE("validate_frame reports payload_exceeds_format_limit beyond CAN XL's own 2048-octet " + "ceiling", "[can][REQ-CANEP-004]") { CanDataFrame xl; - xl.id.value = 0x100; - xl.format = FrameFormat::Xl; - xl.data.assign(kMaxXlPayloadSpec + 1, 0x11); + xl.arbitration_id = 0x100; + xl.format = FrameFormat::XlClassicalPl; + xl.data.assign(kXlMaxDataLen + 1, 0x11); REQUIRE(validate_frame(xl) == make_error_code(CanErrc::payload_exceeds_format_limit)); } -// ── Per-phase bit-timing register sets ─────────────────────────────────────── +// ── Per-phase bit-timing register sets (CanFunctionalConfig, lifecycle-gated) ─ -TEST_CASE("CanEndpoint::configure_bit_timing stores arbitration/fd_data/xl_data independently", +TEST_CASE("set_arbitration_timing/set_fd_data_timing/set_xl_data_timing store independently, " + "gated by functional_cfg_writable", "[can][REQ-CANEP-005]") { - CanEndpoint ep; - CanBitTimingConfig cfg; - cfg.arbitration.prescaler = 4; - cfg.fd_data.prescaler = 1; - cfg.xl_data.prescaler = 1; - cfg.arbitration.phase_seg1 = 10; - cfg.fd_data.phase_seg1 = 3; - cfg.xl_data.phase_seg1 = 2; + CanFunctionalConfig cfg; + WriterCtx via_owning; + via_owning.via_owning_stream = true; + WriterCtx none; + + CanBitTimingPhase arbitration; + arbitration.prescaler = 4; + arbitration.phase_seg1 = 10; + CanBitTimingPhase fd; + fd.prescaler = 1; + fd.phase_seg1 = 3; + CanBitTimingPhase xl; + xl.prescaler = 1; + xl.phase_seg1 = 2; + + REQUIRE_FALSE(set_arbitration_timing(cfg, arbitration, ServerState::HwUnconfigured, none)); + REQUIRE(set_arbitration_timing(cfg, arbitration, ServerState::HwConfigured, via_owning)); + REQUIRE(set_fd_data_timing(cfg, fd, ServerState::HwConfigured, via_owning)); + REQUIRE(set_xl_data_timing(cfg, xl, ServerState::HwConfigured, via_owning)); + + REQUIRE(cfg.timing.arbitration.prescaler == 4); + REQUIRE(cfg.timing.fd_data.prescaler == 1); + REQUIRE(cfg.timing.xl_data.prescaler == 1); + REQUIRE(cfg.timing.arbitration.phase_seg1 == 10); + REQUIRE(cfg.timing.fd_data.phase_seg1 == 3); + REQUIRE(cfg.timing.xl_data.phase_seg1 == 2); +} + +TEST_CASE("set_delay_compensation/set_exec_delay_clk_divider/set_xl_new_pl_provisioned apply only " + "when authorized", + "[can][REQ-CANEP-005]") { + CanFunctionalConfig cfg; + WriterCtx via_owning; + via_owning.via_owning_stream = true; + WriterCtx none; + + REQUIRE_FALSE(set_delay_compensation(cfg, true, 5, ServerState::HwUnconfigured, none)); + REQUIRE(set_delay_compensation(cfg, true, 5, ServerState::HwConfigured, via_owning)); + REQUIRE(cfg.delay_comp_enable); + REQUIRE(cfg.delay_comp_offset == 5); - REQUIRE_FALSE(ep.configure_bit_timing(cfg)); - REQUIRE(ep.bit_timing().arbitration.prescaler == 4); - REQUIRE(ep.bit_timing().fd_data.prescaler == 1); - REQUIRE(ep.bit_timing().xl_data.prescaler == 1); - REQUIRE(ep.bit_timing().arbitration.phase_seg1 == 10); - REQUIRE(ep.bit_timing().fd_data.phase_seg1 == 3); - REQUIRE(ep.bit_timing().xl_data.phase_seg1 == 2); + REQUIRE(set_exec_delay_clk_divider(cfg, 77, ServerState::HwConfigured, via_owning)); + REQUIRE(cfg.exec_delay_clk_divider == 77); + + REQUIRE(set_xl_new_pl_provisioned(cfg, true, ServerState::HwConfigured, via_owning)); + REQUIRE(cfg.xl_new_pl_provisioned); +} + +TEST_CASE("set_xl_filter applies only for a valid index and when authorized", "[can][REQ-CANEP-006]") { + CanFunctionalConfig cfg; + WriterCtx via_owning; + via_owning.via_owning_stream = true; + + REQUIRE_FALSE(set_xl_filter(cfg, kMaxXlFilters, CanXlFilter{0x100, 0x7FF, true}, + ServerState::HwConfigured, via_owning)); + REQUIRE(set_xl_filter(cfg, 0, CanXlFilter{0x100, 0x7FF, true}, ServerState::HwConfigured, via_owning)); + REQUIRE(cfg.xl_filters[0].id == 0x100); + REQUIRE(cfg.xl_filters[0].enable); } -// ── CAN-XL-specific acceptance/receive filters ─────────────────────────────── +// ── EP_func register block (Table 56) ───────────────────────────────────────── + +TEST_CASE("render_registers reports kEpFuncLen at offset 0, zeroes the reserved/base_clk/" + "undecomposed span, and round-trips ep_status/status/fifo_status", + "[can][REQ-CANEP-005]") { + CanFunctionalConfig cfg; + cfg.ep_enable = true; + cfg.ep_status = 0x1234; + cfg.status = 0xAABBCCDD; + cfg.fifo_status = 0x11223344; + + std::array out{}; + render_registers(cfg, out); + + REQUIRE(out[kRegEpLen] == static_cast(kEpFuncLen)); + REQUIRE(out[kRegReserved01] == 0); + REQUIRE(out[kRegBaseClk] == 0); + REQUIRE(out[kRegBaseClk + 1] == 0); + for (uint16_t off = kRegUndecomposedStart; off < kRegUndecomposedStart + kRegUndecomposedLen; ++off) + REQUIRE(out[off] == 0); + REQUIRE(((out[kRegEpStatus] << 8) | out[kRegEpStatus + 1]) == 0x1234); +} + +TEST_CASE("apply_reconfig round-trips a write to the status register, ignoring the read-only span", + "[can][REQ-CANEP-005]") { + CanFunctionalConfig cfg; + std::vector payload{0x00, static_cast(kRegStatus), 0xDE, 0xAD, 0xBE, 0xEF}; + auto ec = apply_reconfig(cfg, payload.data(), payload.size()); + REQUIRE_FALSE(ec); + REQUIRE(cfg.status == 0xDEADBEEFu); +} + +TEST_CASE("apply_reconfig reports short_payload/out_of_range", "[can][REQ-CANEP-005]") { + CanFunctionalConfig cfg; + std::vector too_short{0x00, 0x00}; + REQUIRE(apply_reconfig(cfg, too_short.data(), too_short.size()) == + make_error_code(CanReconfigErrc::short_payload)); + + std::vector oob{0x00, static_cast(kEpFuncLen - 1), 0xAA, 0xBB}; + REQUIRE(apply_reconfig(cfg, oob.data(), oob.size()) == make_error_code(CanReconfigErrc::out_of_range)); +} + +// ── CAN-XL-specific acceptance filters (CanFunctionalConfig::xl_filters) and +// the general (non-XL) acceptance-filter bank ──────────────────────────────── TEST_CASE("CanEndpoint::receive accepts everything when no filters are configured", "[can][REQ-CANEP-006]") { CanEndpoint ep; CanDataFrame f; - f.id.value = 0x123; - f.format = FrameFormat::Classical; + f.arbitration_id = 0x123; + f.format = FrameFormat::Cbff; REQUIRE(ep.receive(f)); - REQUIRE(ep.last_received().id.value == 0x123); + REQUIRE(ep.last_received().arbitration_id == 0x123); } TEST_CASE("CanEndpoint::receive drops a non-XL frame that matches no general acceptance filter", @@ -132,8 +290,8 @@ TEST_CASE("CanEndpoint::receive drops a non-XL frame that matches no general acc ep.set_acceptance_filters({CanAcceptanceFilter{0x200, 0x7FF, false}}); CanDataFrame f; - f.id.value = 0x100; - f.format = FrameFormat::Classical; + f.arbitration_id = 0x100; + f.format = FrameFormat::Cbff; REQUIRE_FALSE(ep.receive(f)); } @@ -143,37 +301,47 @@ TEST_CASE("CanEndpoint::receive accepts a frame matching a general acceptance fi ep.set_acceptance_filters({CanAcceptanceFilter{0x200, 0x700, false}}); // top 3 id bits must match 0x200's CanDataFrame f; - f.id.value = 0x21F; // top 3 bits (0x200) match; low bits are don't-care - f.format = FrameFormat::Classical; + f.arbitration_id = 0x21F; // top 3 bits (0x200) match; low bits are don't-care + f.format = FrameFormat::Cbff; REQUIRE(ep.receive(f)); } -TEST_CASE("CanEndpoint::receive matches an XL frame against xl_receive_filters, not the general " - "acceptance bank, once xl_receive_filters is configured", +TEST_CASE("CanEndpoint::receive matches an XL frame against CanFunctionalConfig::xl_filters, not " + "the general acceptance bank", "[can][REQ-CANEP-006]") { CanEndpoint ep; ep.set_acceptance_filters({CanAcceptanceFilter{0x999, 0x7FF, false}}); // would reject 0x100 - ep.set_xl_receive_filters({CanAcceptanceFilter{0x100, 0x7FF, false}}); // accepts 0x100 + WriterCtx via_owning; + via_owning.via_owning_stream = true; + REQUIRE(set_xl_filter(ep.functional_config(), 0, CanXlFilter{0x100, 0x7FF, true}, + ServerState::HwConfigured, via_owning)); // accepts 0x100 CanDataFrame f; - f.id.value = 0x100; - f.format = FrameFormat::Xl; + f.arbitration_id = 0x100; + f.format = FrameFormat::XlClassicalPl; REQUIRE(ep.receive(f)); } -// ── No trigger-signal table for CAN (extraction §5.11, §7) ────────────────── -// CAN is the one device-facing endpoint type in this codebase with no -// TriggerRegistry at all — there is no triggers() accessor on CanEndpoint to -// call in the first place, so the "no trigger table" property is enforced -// structurally (a caller cannot arm/drain anything), not by a runtime check. -// This test only documents that CanEndpoint's public surface has no -// trigger-related members, by construction: the class compiles without -// exposing one. +TEST_CASE("CanEndpoint::receive drops an XL frame matching no enabled xl_filters entry", + "[can][REQ-CANEP-006]") { + CanEndpoint ep; + WriterCtx via_owning; + via_owning.via_owning_stream = true; + REQUIRE(set_xl_filter(ep.functional_config(), 0, CanXlFilter{0x200, 0x7FF, true}, + ServerState::HwConfigured, via_owning)); + + CanDataFrame f; + f.arbitration_id = 0x100; + f.format = FrameFormat::XlNewPl; + REQUIRE_FALSE(ep.receive(f)); +} + +// ── No trigger-signal table for CAN ─────────────────────────────────────────── TEST_CASE("CanEndpoint exposes no TriggerRegistry accessor", "[can][REQ-CANEP-007]") { CanEndpoint ep; CanDataFrame f; - f.id.value = 0x1; + f.arbitration_id = 0x1; REQUIRE(ep.transmit(f) == std::error_code{}); // No ep.triggers() call exists to make here — that is the point. } @@ -184,13 +352,13 @@ TEST_CASE("CanEndpoint::handle_request delegates a Plain (evt[2:0]==000b) reques "[can][REQ-CANEP-008]") { CanEndpoint ep; CanDataFrame f; - f.id.value = 0x123; - f.format = FrameFormat::Classical; - f.data = {0xDE, 0xAD}; + f.arbitration_id = 0x123; + f.format = FrameFormat::Cbff; + f.data = {0xDE, 0xAD}; auto ec = ep.handle_request(/*evt_op=*/0, f); REQUIRE_FALSE(ec); - REQUIRE(ep.last_transmitted().id.value == 0x123); + REQUIRE(ep.last_transmitted().arbitration_id == 0x123); REQUIRE(ep.last_transmitted().data == std::vector{0xDE, 0xAD}); } @@ -200,13 +368,13 @@ TEST_CASE("CanEndpoint::handle_request rejects every reserved evt[2:0] value (00 for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { CanEndpoint ep; CanDataFrame f; - f.id.value = 0x123; - f.data = {0xAA}; + f.arbitration_id = 0x123; + f.data = {0xAA}; auto ec = ep.handle_request(evt_op, f); REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); // A rejected reserved evt must not record anything as transmitted — // last_transmitted() stays at its default-constructed value. - REQUIRE(ep.last_transmitted().id.value == 0); + REQUIRE(ep.last_transmitted().arbitration_id == 0); REQUIRE(ep.last_transmitted().data.empty()); } } @@ -216,11 +384,11 @@ TEST_CASE("CanEndpoint::handle_request reports config_write_not_supported for ev "[can][REQ-CANEP-009]") { CanEndpoint ep; CanDataFrame f; - f.id.value = 0x123; - f.data = {0x00, 0xAB}; + f.arbitration_id = 0x123; + f.data = {0x00, 0xAB}; auto ec = ep.handle_request(/*evt_op=*/7, f); REQUIRE(ec == make_error_code(CanErrc::config_write_not_supported)); - REQUIRE(ep.last_transmitted().id.value == 0); + REQUIRE(ep.last_transmitted().arbitration_id == 0); REQUIRE(ep.last_transmitted().data.empty()); } @@ -228,7 +396,7 @@ TEST_CASE("CanEndpoint::handle_request masks evt_op down to 3 bits before classi "[can][REQ-CANEP-008]") { CanEndpoint ep; CanDataFrame f; - f.id.value = 0x123; + f.arbitration_id = 0x123; REQUIRE_FALSE(ep.handle_request(/*evt_op=*/0xF8, f)); // low 3 bits 000 -> Plain auto ec = ep.handle_request(/*evt_op=*/0xF9, f); // low 3 bits 001 -> Reserved REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); @@ -237,16 +405,11 @@ TEST_CASE("CanEndpoint::handle_request masks evt_op down to 3 bits before classi TEST_CASE("CanEndpoint::handle_request Reserved/ConfigWrite classification is independent of " "FrameFormat/CAN-ID — evt[2:0] carries no frame-format or remote-frame selector", "[can][REQ-CANEP-008]") { - // Guards against confusing Table 33's evt[2:0] classification with - // Figure 40's separate FrameFormat payload sub-field (see - // handle_request's own header comment): a Reserved/ConfigWrite evt is - // rejected identically no matter which FrameFormat the frame itself - // carries. - for (auto fmt : {FrameFormat::Classical, FrameFormat::Fd, FrameFormat::Xl}) { + for (auto fmt : {FrameFormat::Cbff, FrameFormat::Fbff, FrameFormat::XlClassicalPl}) { CanEndpoint ep; CanDataFrame f; - f.id.value = 0x123; - f.format = fmt; + f.arbitration_id = 0x123; + f.format = fmt; REQUIRE(ep.handle_request(/*evt_op=*/3, f) == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); REQUIRE(ep.handle_request(/*evt_op=*/7, f) == make_error_code(CanErrc::config_write_not_supported)); @@ -267,3 +430,334 @@ TEST_CASE("CanErrc::config_write_not_supported reports a non-empty message in it REQUIRE(ec.category() == can_category()); REQUIRE_FALSE(ec.message().empty()); } + +// ── ACF-level wire codec: unfragmented frame request/response round-trips ──── + +TEST_CASE("encode_frame_request/decode_frame_request round-trip, Classical (no xl_header)", + "[can][REQ-CANEP-003]") { + std::vector tx{0x11, 0x22, 0x33}; + auto frame = encode_frame_request(/*byte_bus_id=*/7, FrameFormat::Cbff, 0x123, std::nullopt, tx, + /*transaction_num=*/4); + REQUIRE_FALSE(frame.empty()); + + FrameFormat out_fmt{}; + uint32_t out_id = 0; + XlHeader out_xl{}; + std::vector out_tx; + uint8_t out_txn = 0; + auto ec = decode_frame_request(frame.data(), frame.size(), 7, out_fmt, out_id, out_xl, out_tx, out_txn); + REQUIRE_FALSE(ec); + REQUIRE(out_fmt == FrameFormat::Cbff); + REQUIRE(out_id == 0x123); + REQUIRE(out_tx == tx); + REQUIRE(out_txn == 4); +} + +TEST_CASE("encode_frame_request/decode_frame_request round-trip, CAN XL (with xl_header)", + "[can][REQ-CANEP-003]") { + std::vector tx{0xAA, 0xBB, 0xCC, 0xDD}; + XlHeader xl{0x5, 0x7, 0xDEADBEEF}; + auto frame = encode_frame_request(7, FrameFormat::XlClassicalPl, 0x321, xl, tx, 9); + REQUIRE_FALSE(frame.empty()); + + FrameFormat out_fmt{}; + uint32_t out_id = 0; + XlHeader out_xl{}; + std::vector out_tx; + uint8_t out_txn = 0; + auto ec = decode_frame_request(frame.data(), frame.size(), 7, out_fmt, out_id, out_xl, out_tx, out_txn); + REQUIRE_FALSE(ec); + REQUIRE(out_fmt == FrameFormat::XlClassicalPl); + REQUIRE(out_id == 0x321); + REQUIRE(out_xl.sdt == 0x5); + REQUIRE(out_xl.vcid == 0x7); + REQUIRE(out_xl.af == 0xDEADBEEFu); + REQUIRE(out_tx == tx); + REQUIRE(out_txn == 9); +} + +TEST_CASE("encode_frame_request returns empty for a mismatched xl_header presence or an invalid " + "identifier", + "[can][REQ-CANEP-003]") { + REQUIRE(encode_frame_request(7, FrameFormat::Cbff, 0, XlHeader{}, {}, 0).empty()); // xl_header on non-XL + REQUIRE(encode_frame_request(7, FrameFormat::XlClassicalPl, 0, std::nullopt, {}, 0).empty()); // missing xl_header on XL + REQUIRE(encode_frame_request(7, FrameFormat::Cbff, 0x800, std::nullopt, {}, 0).empty()); // id out of range +} + +TEST_CASE("decode_frame_request reports wrong_bus/wrong_op/bad_evt/short_frame", + "[can][REQ-CANEP-003]") { + auto frame = encode_frame_request(7, FrameFormat::Cbff, 0x1, std::nullopt, {0xAA}, 1); + + FrameFormat out_fmt{}; + uint32_t out_id = 0; + XlHeader out_xl{}; + std::vector out_tx; + uint8_t out_txn = 0; + + REQUIRE(decode_frame_request(frame.data(), frame.size(), 8, out_fmt, out_id, out_xl, out_tx, out_txn) == + make_error_code(CanErrc::wrong_bus)); + + std::vector too_short{0x1C}; // valid ACF_ABB acf_msg_type (0x0E) but shorter than the fixed header + REQUIRE(decode_frame_request(too_short.data(), too_short.size(), 7, out_fmt, out_id, out_xl, out_tx, + out_txn) == make_error_code(CanErrc::short_frame)); + + std::vector forced_read = frame; + forced_read[6] &= static_cast(~0x80); // clear op bit -> read direction, not write + REQUIRE(decode_frame_request(forced_read.data(), forced_read.size(), 7, out_fmt, out_id, out_xl, + out_tx, out_txn) == make_error_code(CanErrc::wrong_op)); + + std::vector forced_evt = frame; + forced_evt[4] |= 0x10; // evt_op bit0 (byte4 bits6:4) -> evt[2:0] = 001b, Reserved + REQUIRE(decode_frame_request(forced_evt.data(), forced_evt.size(), 7, out_fmt, out_id, out_xl, + out_tx, out_txn) == make_error_code(CanErrc::bad_evt)); +} + +TEST_CASE("encode_frame_response/decode_frame_response round-trip, untimed (ACF_ABB)", + "[can][REQ-CANEP-003]") { + std::vector rx{0x01, 0x02, 0x03}; + auto frame = encode_frame_response(7, FrameFormat::Fbff, 0x55, std::nullopt, rx, 2, false, 0); + REQUIRE_FALSE(frame.empty()); + + FrameFormat out_fmt{}; + uint32_t out_id = 0; + XlHeader out_xl{}; + std::vector out_rx; + bool out_timed = true; + uint64_t out_ts = 0; + uint8_t out_txn = 0; + auto ec = decode_frame_response(frame.data(), frame.size(), 7, out_fmt, out_id, out_xl, out_rx, + out_timed, out_ts, out_txn); + REQUIRE_FALSE(ec); + REQUIRE(out_fmt == FrameFormat::Fbff); + REQUIRE(out_id == 0x55); + REQUIRE(out_rx == rx); + REQUIRE_FALSE(out_timed); + REQUIRE(out_txn == 2); +} + +TEST_CASE("encode_frame_response/decode_frame_response round-trip, timed (ACF_GBB)", + "[can][REQ-CANEP-003]") { + std::vector rx{0xEE}; + auto frame = encode_frame_response(7, FrameFormat::Ceff, 0x1FFFFFFF, std::nullopt, rx, 6, true, + 0xCAFEBABEDEADBEEFull); + REQUIRE_FALSE(frame.empty()); + + FrameFormat out_fmt{}; + uint32_t out_id = 0; + XlHeader out_xl{}; + std::vector out_rx; + bool out_timed = false; + uint64_t out_ts = 0; + uint8_t out_txn = 0; + auto ec = decode_frame_response(frame.data(), frame.size(), 7, out_fmt, out_id, out_xl, out_rx, + out_timed, out_ts, out_txn); + REQUIRE_FALSE(ec); + REQUIRE(out_fmt == FrameFormat::Ceff); + REQUIRE(out_id == 0x1FFFFFFF); + REQUIRE(out_rx == rx); + REQUIRE(out_timed); + REQUIRE(out_ts == 0xCAFEBABEDEADBEEFull); + REQUIRE(out_txn == 6); +} + +// ── Fragmentation: CAN XL, worst-case 2048-octet round-trips ──────────────── + +TEST_CASE("frame_response_fragment_count/encode_frame_response_fragmented produce a single frame " + "when the combined payload already fits", + "[can][REQ-CANEP-003]") { + std::vector rx{0x01, 0x02, 0x03}; + auto count = frame_response_fragment_count(FrameFormat::Cbff, 0x1, std::nullopt, rx.size(), 64); + REQUIRE(count == 1); + + auto frames = encode_frame_response_fragmented(7, FrameFormat::Cbff, 0x1, std::nullopt, rx, 3, false, + 0, 64); + REQUIRE(frames.size() == 1); + REQUIRE(frames[0] == encode_frame_response(7, FrameFormat::Cbff, 0x1, std::nullopt, rx, 3, false, 0)); +} + +TEST_CASE("encode_frame_request_fragmented + Reassembler + decode_reassembled_frame_response " + "round-trip a worst-case 2048-octet CAN XL write request", + "[can][REQ-CANEP-003]") { + std::vector tx(kXlMaxDataLen, 0x5A); + XlHeader xl{0x1, 0x2, 0x33445566}; + constexpr size_t kMaxFragmentPayload = 200; + + auto count = frame_request_fragment_count(FrameFormat::XlNewPl, 0x42, xl, tx.size(), kMaxFragmentPayload); + REQUIRE(count > 1); // genuinely needs multiple fragments + + auto frames = encode_frame_request_fragmented(7, FrameFormat::XlNewPl, 0x42, xl, tx, 11, + kMaxFragmentPayload); + REQUIRE(frames.size() == count); + + rcp::fragment::Reassembler reasm(kXlMaxEncodedLen); + rcp::fragment::ReasmResult last_result = rcp::fragment::ReasmResult::kErrTooLarge; + for (size_t i = 0; i < frames.size(); ++i) { + bool ms; + uint16_t segment_num; + std::vector payload; + bool timed; + uint64_t timestamp; + uint8_t txn; + // A request is decoded the same way a response fragment is at the + // wire-shape level (both are just ms/segment_num/payload over ACF) + // — decode_frame_response_fragment() is reused here exactly as + // ep_can.c's own file header documents c-RCP reusing its response + // decoder for a reassembled REQUEST's combined payload too. + auto ec = decode_frame_response_fragment(frames[i].data(), frames[i].size(), 7, ms, segment_num, + payload, timed, timestamp, txn); + REQUIRE_FALSE(ec); + REQUIRE(txn == 11); + last_result = reasm.feed(ms, segment_num, payload.data(), payload.size()); + if (i + 1 < frames.size()) + REQUIRE(last_result == rcp::fragment::ReasmResult::kContinue); + } + REQUIRE(last_result == rcp::fragment::ReasmResult::kComplete); + REQUIRE(reasm.size() == kXlMaxEncodedLen); + + FrameFormat out_fmt{}; + uint32_t out_id = 0; + XlHeader out_xl{}; + std::vector out_data; + auto ec = decode_reassembled_frame_response(reasm.data(), reasm.size(), out_fmt, out_id, out_xl, out_data); + REQUIRE_FALSE(ec); + REQUIRE(out_fmt == FrameFormat::XlNewPl); + REQUIRE(out_id == 0x42); + REQUIRE(out_xl.sdt == 0x1); + REQUIRE(out_xl.vcid == 0x2); + REQUIRE(out_xl.af == 0x33445566u); + REQUIRE(out_data == tx); +} + +TEST_CASE("encode_frame_response_fragmented + Reassembler round-trip a worst-case 2048-octet CAN " + "XL read response, timed (ACF_GBB)", + "[can][REQ-CANEP-003]") { + std::vector rx(kXlMaxDataLen, 0xC3); + XlHeader xl{0x0, 0x0, 0}; + constexpr size_t kMaxFragmentPayload = 512; + + auto count = frame_response_fragment_count(FrameFormat::XlClassicalPl, 0x7FF, xl, rx.size(), + kMaxFragmentPayload); + REQUIRE(count > 1); + REQUIRE(count <= kMaxFragmentSegments); + + auto frames = encode_frame_response_fragmented(7, FrameFormat::XlClassicalPl, 0x7FF, xl, rx, 200, + /*timed=*/true, 0x1122334455667788ull, + kMaxFragmentPayload); + REQUIRE(frames.size() == count); + + rcp::fragment::Reassembler reasm(kXlMaxEncodedLen); + rcp::fragment::ReasmResult last_result = rcp::fragment::ReasmResult::kErrTooLarge; + for (auto& frame : frames) { + bool ms; + uint16_t segment_num; + std::vector payload; + bool timed; + uint64_t timestamp; + uint8_t txn; + auto ec = decode_frame_response_fragment(frame.data(), frame.size(), 7, ms, segment_num, payload, + timed, timestamp, txn); + REQUIRE_FALSE(ec); + REQUIRE(timed); + REQUIRE(timestamp == 0x1122334455667788ull); + REQUIRE(txn == 200); + last_result = reasm.feed(ms, segment_num, payload.data(), payload.size()); + } + REQUIRE(last_result == rcp::fragment::ReasmResult::kComplete); + + FrameFormat out_fmt{}; + uint32_t out_id = 0; + XlHeader out_xl{}; + std::vector out_data; + auto ec = decode_reassembled_frame_response(reasm.data(), reasm.size(), out_fmt, out_id, out_xl, out_data); + REQUIRE_FALSE(ec); + REQUIRE(out_fmt == FrameFormat::XlClassicalPl); + REQUIRE(out_id == 0x7FF); + REQUIRE(out_data == rx); +} + +TEST_CASE("frame_response_fragment_count returns 0 (not representable) when max_fragment_payload " + "is 0 and the combined payload does not fit in one fragment", + "[can][REQ-CANEP-003]") { + std::vector rx(kXlMaxDataLen, 0); + REQUIRE(frame_response_fragment_count(FrameFormat::XlClassicalPl, 0, std::nullopt, rx.size(), 0) == 0); + REQUIRE(encode_frame_response_fragmented(7, FrameFormat::XlClassicalPl, 0, std::nullopt, rx, 1, + false, 0, 0) + .empty()); +} + +// ── The oversized-reassembly lesson (c-RCP issues #614/#616) ───────────────── +// A request whose every fragment is individually well-formed can still +// reassemble into a payload too large for a caller's own bound — +// fragment::Reassembler fails closed (kErrTooLarge) rather than silently +// accepting or truncating it. This module's own contribution is making sure +// decode_reassembled_frame_response() never gets a chance to paper over +// that: a caller must check the Reassembler's own result before ever +// calling it (Phase 4's dispatch-loop wiring is where that check belongs +// end to end — see this file's own header comment). + +TEST_CASE("Reassembler reports kErrTooLarge, not kComplete, when a fully-fragmented CAN XL " + "response's reassembled size would exceed a caller-supplied max_total_len smaller than " + "the actual combined payload", + "[can][REQ-CANEP-003]") { + std::vector rx(kXlMaxDataLen, 0x42); + XlHeader xl{0, 0, 0}; + constexpr size_t kMaxFragmentPayload = 512; + + auto frames = encode_frame_response_fragmented(7, FrameFormat::XlClassicalPl, 0x10, xl, rx, + 1, false, 0, kMaxFragmentPayload); + REQUIRE(frames.size() > 1); + + // A caller that (mis)configures its own Reassembler with too small a + // bound — smaller than this response's real combined payload + // (kArbitrationPrefixLen + rx.size()) — must see every fragment beyond + // that bound rejected, never silently truncated or accepted. + rcp::fragment::Reassembler undersized(/*max_total_len=*/100); + bool ms; + uint16_t segment_num; + std::vector payload; + bool timed; + uint64_t timestamp; + uint8_t txn; + auto ec = decode_frame_response_fragment(frames[0].data(), frames[0].size(), 7, ms, segment_num, + payload, timed, timestamp, txn); + REQUIRE_FALSE(ec); + auto result = undersized.feed(ms, segment_num, payload.data(), payload.size()); + // The first fragment's own payload (kMaxFragmentPayload=512) already + // exceeds the 100-byte bound on its own. + REQUIRE(result == rcp::fragment::ReasmResult::kErrTooLarge); + REQUIRE_FALSE(undersized.is_collecting()); +} + +TEST_CASE("Reassembler correctly reports kErrTooLarge partway through a multi-fragment sequence " + "whose EARLY fragments individually fit but whose running total does not", + "[can][REQ-CANEP-003]") { + std::vector rx(600, 0x7E); // combined = 10 + 600 = 610 octets + XlHeader xl{0, 0, 0}; + constexpr size_t kMaxFragmentPayload = 100; + + auto frames = encode_frame_response_fragmented(7, FrameFormat::XlClassicalPl, 0x1, xl, rx, + 1, false, 0, kMaxFragmentPayload); + REQUIRE(frames.size() > 1); + + // Bound the Reassembler to fit the first two fragments but not the + // whole 604-octet combined payload. + rcp::fragment::Reassembler bounded(/*max_total_len=*/250); + bool ms; + uint16_t segment_num; + std::vector payload; + bool timed; + uint64_t timestamp; + uint8_t txn; + bool saw_too_large = false; + for (auto& frame : frames) { + auto ec = decode_frame_response_fragment(frame.data(), frame.size(), 7, ms, segment_num, payload, + timed, timestamp, txn); + REQUIRE_FALSE(ec); + auto result = bounded.feed(ms, segment_num, payload.data(), payload.size()); + if (result == rcp::fragment::ReasmResult::kErrTooLarge) { + saw_too_large = true; + break; + } + } + REQUIRE(saw_too_large); +} diff --git a/tests/test_lin.cpp b/tests/test_lin.cpp index 3d8633c..64b0a75 100644 --- a/tests/test_lin.cpp +++ b/tests/test_lin.cpp @@ -5,15 +5,26 @@ // fusa:test REQ-LINEP-005 // fusa:test REQ-LINEP-006 -// Tests for rcp/lin.hpp — the LIN commander endpoint type (ROADMAP.md -// milestone 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN XL), -// ISELED, MDIO, Wakeup Control", v2.7.0). +// Tests for rcp/lin.hpp — the LIN commander endpoint type. Ported from +// c-RCP's tests/test_ep_lin.c (ROADMAP.md "Phase 17", cpp-RCP issue #129, +// Phase 3 "Per-endpoint modules"). The first block below (through the +// LinErrc category-sanity tests) is this file's pre-existing content, +// unchanged — LinEndpoint::transfer()/handle_request() kept their exact +// pre-rewrite signatures/behavior (see lin.hpp's own header comment). Every +// TEST_CASE after that is new, covering this pass's ported content: the +// evt[2:0] exact-match wrapper, the transmission-done trigger, functional +// config + lifecycle gating, the EP_func register block, and the real +// ACF-level wire codec. #include #include +#include #include using namespace rcp::lin; +using rcp::lifecycle::FieldKind; +using rcp::lifecycle::ServerState; +using rcp::lifecycle::WriterCtx; // ── ep_type id ──────────────────────────────────────────────────────────────── @@ -134,3 +145,241 @@ TEST_CASE("LinErrc::config_write_not_supported reports a non-empty message in it REQUIRE(ec.category() == lin_category()); REQUIRE_FALSE(ec.message().empty()); } + +// ── evt[2:0] exact-match wrapper (response_matches) ────────────────────────── + +TEST_CASE("response_matches is exact-match, length-capped to the shorter of the two buffers", + "[lin][REQ-LINEP-005]") { + std::vector tx{0xAA, 0xBB}; + std::vector rx{0xAA, 0xBB, 0xCC}; + REQUIRE(response_matches(tx, rx)); // rx's extra trailing byte is ignored + + std::vector rx_short{0xAA}; + REQUIRE_FALSE(response_matches(tx, rx_short)); // rx shorter than tx never matches + + std::vector rx_mismatch{0xAA, 0xCC}; + REQUIRE_FALSE(response_matches(tx, rx_mismatch)); + + REQUIRE(response_matches({}, {})); // both empty -> trivially matches +} + +// ── Transmission-done trigger ───────────────────────────────────────────────── + +TEST_CASE("trigger_fires: TxDone requires both tx_done_event and trailing_time_expired", + "[lin][REQ-LINEP-003]") { + REQUIRE(trigger_fires(LinTrigger::TxDone, true, true)); + REQUIRE_FALSE(trigger_fires(LinTrigger::TxDone, true, false)); + REQUIRE_FALSE(trigger_fires(LinTrigger::TxDone, false, true)); + REQUIRE_FALSE(trigger_fires(LinTrigger::TxDone, false, false)); +} + +TEST_CASE("trigger_fires: None never fires", "[lin][REQ-LINEP-003]") { + REQUIRE_FALSE(trigger_fires(LinTrigger::None, true, true)); +} + +// ── Functional config + lifecycle gating ───────────────────────────────────── + +TEST_CASE("functional_cfg_writable follows FieldKind::FunctionalW's own gating rule", + "[lin][REQ-LINEP-006]") { + WriterCtx via_owning; + via_owning.via_owning_stream = true; + WriterCtx none; + + REQUIRE_FALSE(functional_cfg_writable(ServerState::HwUnconfigured, via_owning)); + REQUIRE(functional_cfg_writable(ServerState::HwConfigured, via_owning)); + REQUIRE_FALSE(functional_cfg_writable(ServerState::HwConfigured, none)); + REQUIRE(functional_cfg_writable(ServerState::RcpConfigured, via_owning)); +} + +TEST_CASE("set_clk_divider/set_trigger apply only when authorized, leaving cfg untouched otherwise", + "[lin][REQ-LINEP-006]") { + LinFunctionalConfig cfg; + WriterCtx none; + WriterCtx via_owning; + via_owning.via_owning_stream = true; + + REQUIRE_FALSE(set_clk_divider(cfg, 42, ServerState::HwUnconfigured, none)); + REQUIRE(cfg.lin_clk_divider == 0); + + REQUIRE(set_clk_divider(cfg, 42, ServerState::HwConfigured, via_owning)); + REQUIRE(cfg.lin_clk_divider == 42); + + REQUIRE_FALSE(set_trigger(cfg, LinTrigger::TxDone, ServerState::HwUnconfigured, none)); + REQUIRE(cfg.trigger == LinTrigger::None); + + REQUIRE(set_trigger(cfg, LinTrigger::TxDone, ServerState::HwConfigured, via_owning)); + REQUIRE(cfg.trigger == LinTrigger::TxDone); +} + +// ── EP_func register block (Table 55) ───────────────────────────────────────── + +TEST_CASE("render_registers reports kEpFuncLen at offset 0 and zero at the reserved/base_clk " + "offsets", + "[lin][REQ-LINEP-006]") { + LinFunctionalConfig cfg; + cfg.ep_enable = true; + cfg.ep_status = 0x1234; + cfg.wire_clk_divider = 7; + + std::array out{}; + render_registers(cfg, out); + + REQUIRE(out[kRegEpLen] == static_cast(kEpFuncLen)); + REQUIRE(out[kRegReserved01] == 0); + REQUIRE(out[kRegBaseClk] == 0); + REQUIRE(out[kRegBaseClk + 1] == 0); + REQUIRE((out[kRegEpEnableClr] & 0x01) != 0); // ep_enable bit + REQUIRE(((out[kRegEpStatus] << 8) | out[kRegEpStatus + 1]) == 0x1234); + REQUIRE(out[kRegClkDivider] == 7); +} + +TEST_CASE("apply_reconfig round-trips a write through render_registers", "[lin][REQ-LINEP-006]") { + LinFunctionalConfig cfg; + std::vector payload{0x00, kRegClkDivider, 99}; // start_address=kRegClkDivider, data=[99] + + auto ec = apply_reconfig(cfg, payload.data(), payload.size()); + REQUIRE_FALSE(ec); + REQUIRE(cfg.wire_clk_divider == 99); +} + +TEST_CASE("apply_reconfig leaves cfg untouched and reports short_payload for a too-short payload", + "[lin][REQ-LINEP-006]") { + LinFunctionalConfig cfg; + std::vector payload{0x00, 0x00}; // address only, no data octet + auto ec = apply_reconfig(cfg, payload.data(), payload.size()); + REQUIRE(ec == make_error_code(LinReconfigErrc::short_payload)); + REQUIRE(cfg.wire_clk_divider == 0); +} + +TEST_CASE("apply_reconfig reports out_of_range when the addressed span exceeds kEpFuncLen", + "[lin][REQ-LINEP-006]") { + LinFunctionalConfig cfg; + std::vector payload{0x00, static_cast(kEpFuncLen - 1), 0xAA, 0xBB}; // 2 data octets starting 1 before the end + auto ec = apply_reconfig(cfg, payload.data(), payload.size()); + REQUIRE(ec == make_error_code(LinReconfigErrc::out_of_range)); +} + +TEST_CASE("apply_reconfig ignores writes to read-only offsets while still applying the rest of " + "the span", + "[lin][REQ-LINEP-006]") { + LinFunctionalConfig cfg; + // Address 0 (lin_ep_len, read-only) through address 2 (lin_ep_enable&clr, R/W). + std::vector payload{0x00, 0x00, 0xFF, 0xFF, 0x01}; + auto ec = apply_reconfig(cfg, payload.data(), payload.size()); + REQUIRE_FALSE(ec); + REQUIRE(cfg.ep_enable); // bit 0 of enable&clr (offset 2) was applied +} + +// ── ACF-level wire codec: command request ───────────────────────────────────── + +TEST_CASE("encode_command_request/decode_command_request round-trip", "[lin][REQ-LINEP-002]") { + std::vector tx{0x55, 0x21, 0xAA}; + auto frame = encode_command_request(/*byte_bus_id=*/6, tx, /*transaction_num=*/9); + REQUIRE_FALSE(frame.empty()); + + std::vector out_tx; + uint8_t out_txn = 0; + auto ec = decode_command_request(frame.data(), frame.size(), /*expected_bus_id=*/6, out_tx, out_txn); + REQUIRE_FALSE(ec); + REQUIRE(out_tx == tx); + REQUIRE(out_txn == 9); +} + +TEST_CASE("decode_command_request reports wrong_bus/wrong_op/bad_evt/short_frame", "[lin][REQ-LINEP-002]") { + std::vector tx{0xAA}; + auto frame = encode_command_request(6, tx, 1); + + std::vector out_tx; + uint8_t out_txn = 0; + + REQUIRE(decode_command_request(frame.data(), frame.size(), /*expected_bus_id=*/7, out_tx, out_txn) == + make_error_code(LinErrc::wrong_bus)); + + std::vector too_short{0x1C, 0x00}; // valid ACF_ABB acf_msg_type (0x0E) but shorter than the fixed header + REQUIRE(decode_command_request(too_short.data(), too_short.size(), 6, out_tx, out_txn) == + make_error_code(LinErrc::short_frame)); + + // A command request is always encoded read-direction (op=false, see + // encode_command_request's own comment) — force op=true (write + // direction, what encode_response's own frames never carry either) by + // hand-editing byte 6's top bit, to exercise wrong_op explicitly. + std::vector forced_write = frame; + forced_write[6] |= 0x80; // op bit + REQUIRE(decode_command_request(forced_write.data(), forced_write.size(), 6, out_tx, out_txn) == + make_error_code(LinErrc::wrong_op)); +} + +// ── ACF-level wire codec: response ───────────────────────────────────────────── + +TEST_CASE("encode_response/decode_response round-trip, untimed (ACF_ABB)", "[lin][REQ-LINEP-002]") { + std::vector rx{0xDE, 0xAD, 0xBE, 0xEF}; + auto frame = encode_response(/*byte_bus_id=*/6, rx, /*transaction_num=*/3, /*timed=*/false, 0); + REQUIRE_FALSE(frame.empty()); + + std::vector out_rx; + bool out_timed = true; + uint64_t out_ts = 0; + uint8_t out_txn = 0; + auto ec = decode_response(frame.data(), frame.size(), 6, out_rx, out_timed, out_ts, out_txn); + REQUIRE_FALSE(ec); + REQUIRE(out_rx == rx); + REQUIRE_FALSE(out_timed); + REQUIRE(out_ts == 0); + REQUIRE(out_txn == 3); +} + +TEST_CASE("encode_response/decode_response round-trip, timed (ACF_GBB)", "[lin][REQ-LINEP-002]") { + std::vector rx{0x01, 0x02}; + auto frame = encode_response(6, rx, 5, /*timed=*/true, /*timestamp=*/0x1122334455667788ull); + + std::vector out_rx; + bool out_timed = false; + uint64_t out_ts = 0; + uint8_t out_txn = 0; + auto ec = decode_response(frame.data(), frame.size(), 6, out_rx, out_timed, out_ts, out_txn); + REQUIRE_FALSE(ec); + REQUIRE(out_rx == rx); + REQUIRE(out_timed); + REQUIRE(out_ts == 0x1122334455667788ull); + REQUIRE(out_txn == 5); +} + +TEST_CASE("decode_response reports wrong_bus/short_frame", "[lin][REQ-LINEP-002]") { + auto frame = encode_response(6, {0xAA}, 1, false, 0); + + std::vector out_rx; + bool out_timed = false; + uint64_t out_ts = 0; + uint8_t out_txn = 0; + REQUIRE(decode_response(frame.data(), frame.size(), 7, out_rx, out_timed, out_ts, out_txn) == + make_error_code(LinErrc::wrong_bus)); + + std::vector empty; + REQUIRE(decode_response(empty.data(), empty.size(), 6, out_rx, out_timed, out_ts, out_txn) == + make_error_code(LinErrc::short_frame)); +} + +// ── Reconfig request encode ─────────────────────────────────────────────────── + +TEST_CASE("encode_reconfig_request builds a WRITE frame with evt[2:0]=111b", "[lin][REQ-LINEP-006]") { + std::vector data{0x2A}; + auto frame = encode_reconfig_request(/*byte_bus_id=*/6, /*start_address=*/kRegClkDivider, data, + /*transaction_num=*/1); + REQUIRE_FALSE(frame.empty()); + REQUIRE((frame[6] & 0x80) != 0); // op=write + + std::vector out_tx; + uint8_t out_txn = 0; + auto ec = decode_command_request(frame.data(), frame.size(), 6, out_tx, out_txn); + // decode_command_request only accepts a read-direction (op=false) + // request — a config-write frame (op=true, evt[2:0]=111b) is correctly + // rejected as wrong_op before its evt[2:0] is even inspected, since the + // op check runs first. This documents the two decoders' deliberate + // separation of concerns: a config-write frame is never a valid + // "plain command request" on any axis. + REQUIRE(ec == make_error_code(LinErrc::wrong_op)); +} + +TEST_CASE("encode_reconfig_request returns empty for empty data", "[lin][REQ-LINEP-006]") { + REQUIRE(encode_reconfig_request(6, 0, {}, 1).empty()); +}