diff --git a/.fusa-reqs.json b/.fusa-reqs.json index a9a9e84..698e95c 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -770,64 +770,112 @@ }, { "id": "REQ-E2E-001", - "title": "E2E header prepended to payload", - "text": "The e2e::wrap() function shall prepend a 6-byte header (4-byte seq + 2-byte CRC-16) to the payload.", + "title": "TC18 end-to-end CRC32 algorithm parameters", + "text": "rcp::e2e::crc32 shall compute a 32-bit CRC using polynomial 0xF4ACFB13, initial value 0xFFFFFFFF, both input and output reflection enabled, and a final XOR of 0xFFFFFFFF.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" }, { "id": "REQ-E2E-002", - "title": "Sequence counter increments per send", - "text": "Each call to e2e::Controller::send() shall increment the sequence counter atomically.", + "title": "CRC coverage buffer layout", + "text": "rcp::e2e::coverage_buffer shall assemble, in order, the 8-byte big-endian stream_id, the 4-byte big-endian avtp_timestamp (zero-filled when std::nullopt is passed, the NTSCF stand-in), the 10-byte ACF shared header, and the payload; rcp::e2e::compute_crc shall compute the CRC over exactly that buffer.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" }, { "id": "REQ-E2E-003", - "title": "CRC-16/CCITT-FALSE algorithm", - "text": "The CRC shall be computed using the CRC-16/CCITT-FALSE polynomial (0x1021, init 0xFFFF).", + "title": "Trailing-CRC length pre-adjustment", + "text": "rcp::e2e::apply_acf_length_adjustment shall add exactly kCrcLengthAdjustQuadlets (1) to AcfMessageInfo::acf_msg_length; rcp::e2e::apply_frame_length_adjustment shall add exactly kCrcLengthAdjustOctets (4) to an NtscfHeader's or TscfHeader's control_data_length.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" }, { "id": "REQ-E2E-004", - "title": "CRC covers seq and payload", - "text": "The CRC-16 shall cover both the 4-byte sequence number and the entire payload.", + "title": "verify_crc detects a mismatched or corrupted CRC", + "text": "rcp::e2e::verify_crc shall return {} when the received CRC matches compute_crc's result and shall return E2eErrc::crc_error (CRC_ERROR) otherwise.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" }, { "id": "REQ-E2E-005", - "title": "Unwrap validates CRC", - "text": "e2e::unwrap() shall return E2EErrc::crc_mismatch if the CRC in the frame does not match the computed CRC.", + "title": "Per-endpoint CRC safe-mode toggles gate per message role", + "text": "rcp::e2e::crc_required shall report regmap::EndpointGenericConfig::ep_req_crc_enable, ep_ack_crc_enable, and ep_response_crc_enable independently for MessageRole::Request, Acknowledge, and Response respectively; rcp::e2e::implemented_options_bit shall report regmap::kOptSafetyRequests only when its safety_requests_implemented argument is true.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" }, { "id": "REQ-E2E-006", - "title": "Unwrap rejects short frames", - "text": "e2e::unwrap() shall return E2EErrc::short_frame if the frame is shorter than HeaderLen bytes.", + "title": "RxStreamGuard implements rx_enforce_e2e's drop-vs-latch rule", + "text": "rcp::e2e::RxStreamGuard::record_crc_result shall return CRC_ERROR for a failing request without affecting later requests when RequestStreamConfig::rx_enforce_e2e is clear, and shall additionally latch the whole stream (every subsequent call reports CRC_ERROR until reset_latch()) when rx_enforce_e2e is set.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" }, { "id": "REQ-E2E-007", - "title": "ReplayGuard rejects duplicate seq", - "text": "ReplayGuard::check() shall return E2EErrc::replay for any sequence number that was already accepted within the window.", + "title": "RxSequenceGuard enforces rx_enforce_seq independently of the watchdog", + "text": "rcp::e2e::RxSequenceGuard::check shall accept every sequence number when RequestStreamConfig::rx_enforce_seq is clear, and shall return E2eErrc::sequence_violation for any sequence number that is not strictly greater than the last accepted one when rx_enforce_seq is set.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" }, { "id": "REQ-E2E-008", - "title": "ReplayGuard rejects old seq outside window", - "text": "ReplayGuard::check() shall return E2EErrc::replay for sequence numbers more than ReplayWindowSize behind the current high-water mark.", + "title": "RxWatchdog overflow detection respects rx_wd_enable and rx_wd_timeout_interval", + "text": "rcp::e2e::RxWatchdog::overflowed shall return false when RequestStreamConfig::rx_wd_enable is clear or kick() has never been called, and shall return true once more than rx_wd_timeout_interval milliseconds have elapsed since the last kick().", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-E2E-009", + "title": "Watchdog overflow purges normal requests and retains safety-tagged ones", + "text": "rcp::e2e::apply_watchdog_overflow shall, when RequestStreamConfig::rx_wd_safestate_enable is set, latch the RxWatchdog into safe state and cancel every pending/started request on the ledger whose sequencer::RequestRecord::is_safety is false while leaving is_safety=true records untouched (via RequestLedger::cancel_all(true)); it shall purge nothing and not enter safe state when rx_wd_safestate_enable is clear.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-E2E-010", + "title": "Queue overflow drives the same purge-normal/retain-safety rule via a distinct trigger", + "text": "rcp::e2e::apply_queue_overflow shall implement the same purge-normal/retain-safety behavior as apply_watchdog_overflow, gated on RequestStreamConfig::rx_ovrflw_safestate_enable instead of rx_wd_safestate_enable.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-E2E-011", + "title": "endpoint_in_configured_safe_state implements both rx_safety_measure strategies", + "text": "rcp::e2e::endpoint_in_configured_safe_state shall return the caller-supplied force_high_impedance_asserted flag unchanged when RequestStreamConfig::rx_safety_measure is RxSafetyMeasure::ForceHighImpedance, and shall return whether rx_safestate_sequencer's current SequencerTable value equals rx_safe_sequencer_state (false if the index is out of range) when rx_safety_measure is RxSafetyMeasure::RunSafeSequencer.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-E2E-012", + "title": "may_execute_now gates safety-tagged requests on safe-state entry", + "text": "rcp::e2e::may_execute_now shall return true unconditionally for a sequencer::RequestRecord whose is_safety is false, and shall return its endpoint_in_safe_state argument unchanged for a record whose is_safety is true.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-E2E-013", + "title": "RxWatchdog repeating info notification tracks rx_wd_info_enable while latched", + "text": "rcp::e2e::RxWatchdog::should_emit_info_notification shall return true if and only if the watchdog is currently latched into safe state (enter_safe_state() called, clear_safe_state() not since called) and RequestStreamConfig::rx_wd_info_enable is set.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-E2E-014", + "title": "E2eErrc is a distinct error category with non-empty, distinct messages", + "text": "Every rcp::e2e::E2eErrc value shall report rcp::e2e::e2e_category() as its category and shall have a non-empty diagnostic message distinct from every other E2eErrc value's message.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -2882,8 +2930,8 @@ }, { "id": "REQ-REGMAP-009", - "title": "Request-stream config carries inert rx_* fields", - "text": "RequestStreamConfig shall provide rx_wd_timeout_s, rx_wd_action, and rx_safety_measure fields that store and return an assigned value; no watchdog/safe-state behavior is wired to these fields until v2.6.0.", + "title": "RequestStreamConfig carries the full v2.6.0 watchdog/safe-state register set", + "text": "regmap::RequestStreamConfig shall carry rx_wd_timeout_interval, rx_wd_enable, rx_wd_safestate_enable, rx_wd_info_enable, rx_enforce_e2e, rx_enforce_seq, rx_seq_safestate_enable, rx_ovrflw_safestate_enable, rx_safety_measure, rx_safestate_sequencer, and rx_safe_sequencer_state as independently settable fields, each defaulting to disabled/false/RxSafetyMeasure::ForceHighImpedance.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -2928,6 +2976,14 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-REGMAP-015", + "title": "EndpointGenericConfig's per-endpoint E2E CRC safe-mode toggles", + "text": "regmap::EndpointGenericConfig::ep_req_crc_enable, ep_ack_crc_enable, and ep_response_crc_enable shall each default to false and shall be independently settable without affecting one another.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, { "id": "REQ-DISC-001", "title": "Discovery request framing \u2014 byte_bus_id 0, register-map address 0", @@ -3423,6 +3479,30 @@ "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" + }, + { + "id": "REQ-SEQ-010", + "title": "decode_request_type accepts exactly the three 0x8x safety-tagged opcodes", + "text": "rcp::sequencer::is_valid_request_type and decode_request_type shall accept RequestTypeOpcode::CompoundSafety (0x8F), CompoundWaitSafety (0x8B), and TriggeredSafety (0x8E) as valid opcodes, round-tripping their parameter bytes exactly as the eight pre-existing opcodes do, and shall reject every other MSB-set byte value as SequencerErrc::unknown_request_type.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-SEQ-011", + "title": "Safety-tagged opcodes share their base opcode's priority category", + "text": "rcp::sequencer::is_safety_variant shall return true for exactly CompoundSafety/CompoundWaitSafety/TriggeredSafety and false for every other RequestTypeOpcode; rcp::sequencer::category_of shall map each safety-tagged opcode to the same RequestCategory as its corresponding base opcode (Compound, CompoundWait, Triggered).", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-SEQ-012", + "title": "request_record_for derives RequestRecord::is_safety from the opcode", + "text": "rcp::sequencer::request_record_for shall set the returned RequestRecord's is_safety field to is_safety_variant(*type) when type has a value, to false when type is std::nullopt (the standard request kind), and the resulting record shall be excluded from cancellation by RequestLedger::cancel_all(non_safestate_only=true) if and only if is_safety is true.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" } ] } diff --git a/CMakeLists.txt b/CMakeLists.txt index 93c4329..467bc65 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.21) project(cpp-rcp - VERSION 2.5.0 + VERSION 2.6.0 DESCRIPTION "Remote Control Protocol for automotive zonal architecture (C++)" HOMEPAGE_URL "https://github.com/SoundMatt/cpp-RCP" LANGUAGES CXX diff --git a/ROADMAP.md b/ROADMAP.md index 8546aae..28e229b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -504,6 +504,64 @@ the Phase 13 introduction above. ### 50. E2E CRC Safe Points & Safety-Request Variants (v2.6.0) +**Done (v2.6.0):** `rcp/e2e.hpp` is REPLACED in full, per the Satellite +Package Disposition table's entry for this file — the pre-replacement +CRC-16/CCITT-FALSE + sequence-counter + replay-window wrapper around +`rcp.hpp`'s `Controller` is discarded outright, not adapted; nothing else in +the tree depended on that old API, so no `legacy_e2e.hpp` split was needed +the way `rcp/legacy_wire.hpp` was at v2.0.0. `crc32`/`detail::crc32_update` +implement the specification's actual end-to-end CRC via the standard +table-less right-shifting construction for a RefIn=true/RefOut=true CRC +(polynomial `0xF4ACFB13`, init `0xFFFFFFFF`, final XOR `0xFFFFFFFF`). +`coverage_buffer`/`compute_crc` assemble and hash exactly `stream_id` + +`avtp_timestamp` (zero-filled — `std::nullopt` — under NTSCF) + the full ACF +shared header + payload; `apply_acf_length_adjustment` and +`apply_frame_length_adjustment` implement the +1 quadlet / +4 octet +pre-adjustment respectively, layered on top of `rcp/wire.hpp`'s existing ACF +codec without changing that codec's core framing. `crc_required` gates CRC +checking per message role off `regmap::EndpointGenericConfig`'s three new +`ep_req_crc_enable`/`ep_ack_crc_enable`/`ep_response_crc_enable` toggles, and +`E2eErrc::crc_error` is the `CRC_ERROR` failure path, following the +category/message pattern `RegMapErrc`/`LifecycleErrc`/`SequencerErrc` +already established. `RxStreamGuard` implements `rx_enforce_e2e`'s +per-request-drop-vs-whole-stream-latch choice; `RxSequenceGuard` implements +`rx_enforce_seq`'s monotonic check, independent of the watchdog; `RxWatchdog` +implements `rx_wd_enable`/`rx_wd_timeout_interval` overflow detection, the +safe-state latch, and `rx_wd_info_enable`'s repeating-notification flag; +`apply_watchdog_overflow`/`apply_queue_overflow` implement the +purge-normal/retain-safety queue rule for their respective triggers +(`rx_wd_safestate_enable`/`rx_ovrflw_safestate_enable`) by calling +`rcp::sequencer::RequestLedger::cancel_all(non_safestate_only=true)` +directly rather than reimplementing cancellation — that call already existed +from v2.5.0's clear-non-safestate support and needed no changes. +`endpoint_in_configured_safe_state` implements both `rx_safety_measure` +strategies (`RxSafetyMeasure::ForceHighImpedance`'s externally-asserted flag +vs. `RunSafeSequencer`'s `rx_safestate_sequencer`-reads-`rx_safe_sequencer_state` +check), and `may_execute_now` is the load-bearing safe-state gate: a +safety-tagged request is only ever eligible once that check reports true. +`rcp/regmap.hpp`'s `RequestStreamConfig` is expanded from its three +placeholder fields (`rx_wd_timeout_s`/`rx_wd_action`/`rx_safety_measure` as a +bare `uint8_t`) to the full eleven-field set the roadmap calls for, plus the +new `RxSafetyMeasure` enum; `EndpointGenericConfig` gains the three CRC +toggles above. `rcp/sequencer.hpp` gains the three MSB-set safety-tagged +opcodes — `RequestTypeOpcode::CompoundSafety` (`0x8F`), `CompoundWaitSafety` +(`0x8B`), `TriggeredSafety` (`0x8E`) — accepted by `is_valid_request_type`/ +`decode_request_type` and mapped by `category_of` onto their base opcode's +existing priority category; `is_safety_variant` is the single source of +truth for which opcodes are safety-tagged, and the new `request_record_for` +factory derives `RequestRecord::is_safety` from it automatically rather than +leaving that assignment to each call site by hand. New coverage lives in +`tests/test_e2e.cpp` (REQ-E2E-001..014, entirely rewritten — the prior +REQ-E2E-001..008 described the discarded CRC-16 scheme and no longer +apply), `tests/test_sequencer.cpp` (REQ-SEQ-010..012, added), and +`tests/test_regmap.cpp` (REQ-REGMAP-009 rewritten for the expanded field +set, REQ-REGMAP-015 added), traced in `.fusa-reqs.json`. Full wire-level +conformance against other TC18 implementations is still not claimed — see +this file's own disclaimer pattern in `rcp/wire.hpp`/`rcp/regmap.hpp`/ +`rcp/sequencer.hpp` — but this is the milestone the Phase 13 introduction +above names as "the point at which the mandatory baseline plus safe-points +exist." + - Implement the specification's actual end-to-end CRC: 32-bit, polynomial `0xF4ACFB13`, initial value `0xFFFFFFFF`, final XOR `0xFFFFFFFF`, both input and output reflection enabled — **not** the ad-hoc CRC-16/CCITT diff --git a/include/rcp/e2e.hpp b/include/rcp/e2e.hpp index 31f99f4..1ae12bf 100644 --- a/include/rcp/e2e.hpp +++ b/include/rcp/e2e.hpp @@ -6,49 +6,82 @@ // fusa:req REQ-E2E-006 // fusa:req REQ-E2E-007 // fusa:req REQ-E2E-008 - -// End-to-end communication protection (ISO 26262 Part 7 E2E profile). +// fusa:req REQ-E2E-009 +// fusa:req REQ-E2E-010 +// fusa:req REQ-E2E-011 +// fusa:req REQ-E2E-012 +// fusa:req REQ-E2E-013 +// fusa:req REQ-E2E-014 + +// End-to-end CRC safe points — the OPEN Alliance TC18 Remote Control +// Protocol Specification v0.5.1_RC's actual E2E integrity mechanism, plus +// the per-request-stream watchdog and safe-state primitives that the +// safety-tagged (0x8x) request variants (rcp/sequencer.hpp) depend on +// (extraction §3.8, §4.4, §4.7, §6 item 4). +// +// ROADMAP.md milestone 50, "E2E CRC Safe Points & Safety-Request Variants +// (v2.6.0)": this header REPLACES this file's pre-replacement content in +// full, per the Satellite Package Disposition table's entry for `e2e.hpp` +// — the prior ad-hoc CRC-16/CCITT-FALSE + sequence-counter + replay-window +// wrapper around rcp.hpp's Controller is discarded, not adapted. Nothing +// else in this tree depended on that old API (only this file's own test +// did), so no legacy shim is needed here, unlike rcp/wire.hpp's +// rcp/legacy_wire.hpp split at v2.0.0. // -// Three layers of defence: -// 1. Sequence counter — per-Controller monotonically incrementing uint32. -// 2. CRC-16/CCITT-FALSE checksum — computed over seq + original payload. -// 3. Replay guard — sliding window rejects previously seen sequence numbers. +// This header rides on top of rcp/wire.hpp's AcfMessageInfo/StreamId +// (v2.0.0), rcp/regmap.hpp's RequestStreamConfig/EndpointGenericConfig +// (v2.1.0, expanded to their full v2.6.0 field set alongside this header), +// and rcp/sequencer.hpp's RequestRecord/RequestLedger/SequencerTable +// (v2.5.0/v2.6.0) without modifying any of their core framing — the CRC +// coverage builder below serializes an already-built AcfMessageInfo rather +// than reaching into rcp/wire.hpp's codec internals, and the +// watchdog-overflow queue behavior reuses +// rcp::sequencer::RequestLedger::cancel_all(non_safestate_only) directly +// rather than reimplementing cancellation. // -// Sender: wrap any Controller with e2e::Controller. -// Receiver: call e2e::unwrap() then ReplayGuard::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 CRC +// bit-packing, byte layout of the coverage buffer, and watchdog/safe-state +// class shapes chosen in this file are this implementation's own encoding +// of that behavior — full bit-for-bit conformance against other TC18 +// implementations is not claimed, same as the equivalent disclaimers in +// rcp/wire.hpp, rcp/regmap.hpp, and rcp/sequencer.hpp. This header +// provides primitives, not a running scheduler or timer thread — deciding +// *when* to call overflowed()/kick()/should_emit_info_notification() is +// left to the embedding application, same as every other header in this +// codebase. #pragma once -#include "rcp.hpp" +#include +#include +#include -#include -#include +#include #include -#include +#include +#include #include +#include namespace rcp { namespace e2e { -// HeaderLen is the byte overhead prepended by wrap(). -// Layout: [0:4] SeqNum uint32 big-endian, [4:6] CRC-16 uint16 big-endian. -constexpr size_t HeaderLen = 6; +// ── Errors ──────────────────────────────────────────────────────────────────── -// Error codes -enum class E2EErrc : int { - crc_mismatch = 1, - short_frame = 2, - replay = 3, +enum class E2eErrc : int { + crc_error = 1, // CRC_ERROR — computed CRC does not match the received trailer, or the stream is latched + sequence_violation = 2, // rx_enforce_seq: received sequence number is not strictly increasing }; inline const std::error_category& e2e_category() noexcept { struct Cat : std::error_category { const char* name() const noexcept override { return "rcp.e2e"; } std::string message(int ev) const override { - switch (static_cast(ev)) { - case E2EErrc::crc_mismatch: return "e2e: CRC-16 mismatch — payload corrupted"; - case E2EErrc::short_frame: return "e2e: frame too short for E2E header"; - case E2EErrc::replay: return "e2e: replayed sequence number detected"; - default: return "e2e: unknown error"; + switch (static_cast(ev)) { + case E2eErrc::crc_error: return "rcp/e2e: CRC_ERROR"; + case E2eErrc::sequence_violation: return "rcp/e2e: sequence number is not strictly increasing"; + default: return "rcp/e2e: unknown error"; } } }; @@ -56,171 +89,356 @@ inline const std::error_category& e2e_category() noexcept { return instance; } -inline std::error_code make_error_code(E2EErrc e) noexcept { +inline std::error_code make_error_code(E2eErrc e) noexcept { return {static_cast(e), e2e_category()}; } -// ── CRC-16/CCITT-FALSE ──────────────────────────────────────────────────────── +// ── CRC32 primitive (extraction §4.7) ──────────────────────────────────────── +// 32-bit width, polynomial 0xF4ACFB13, initial value 0xFFFFFFFF, both input +// and output reflected, final XOR 0xFFFFFFFF. This replaces the ad-hoc +// CRC-16/CCITT-FALSE this file used prior to v2.6.0 in full — the two +// schemes share no code. +// +// Implemented as the standard table-less right-shifting technique for a +// RefIn=true/RefOut=true CRC (the same structure zlib's crc32 uses for its +// own, unrelated, polynomial): the working polynomial is bit-reflected once +// up front, after which every byte is folded in LSB-first with no +// per-byte reflection step needed. This is a well-known, independently +// verifiable construction, not something guessed at from the confidential +// specification text. namespace detail { -inline uint16_t crc16_update(uint16_t crc, uint8_t b) noexcept { - constexpr uint16_t poly = 0x1021; - crc ^= static_cast(b) << 8; - for (int i = 0; i < 8; ++i) { - crc = (crc & 0x8000u) ? static_cast((crc << 1) ^ poly) : static_cast(crc << 1); +constexpr uint32_t kCrc32PolyNormal = 0xF4ACFB13; // TC18 end-to-end CRC polynomial, non-reflected form + +constexpr uint32_t reflect32(uint32_t v) noexcept { + uint32_t r = 0; + for (int i = 0; i < 32; ++i) { + r = (r << 1) | (v & 1u); + v >>= 1; } - return crc; + return r; } -inline uint16_t crc16(const uint8_t* prefix, size_t prefix_len, - const uint8_t* data, size_t data_len) noexcept { - uint16_t crc = 0xFFFF; - for (size_t i = 0; i < prefix_len; ++i) crc = crc16_update(crc, prefix[i]); - for (size_t i = 0; i < data_len; ++i) crc = crc16_update(crc, data[i]); +inline uint32_t crc32_reflected_poly() noexcept { + static const uint32_t poly = reflect32(kCrc32PolyNormal); + return poly; +} + +inline uint32_t crc32_update(uint32_t crc, uint8_t byte_in) noexcept { + crc ^= byte_in; + const uint32_t poly = crc32_reflected_poly(); + for (int i = 0; i < 8; ++i) { + crc = (crc & 1u) ? ((crc >> 1) ^ poly) : (crc >> 1); + } return crc; } -inline void put_u16(uint8_t* p, uint16_t v) noexcept { - p[0] = static_cast(v >> 8); - p[1] = static_cast(v & 0xFF); +} // namespace detail + +// crc32 computes the TC18 end-to-end CRC over an arbitrary byte range +// (extraction §4.7). +inline uint32_t crc32(const uint8_t* data, size_t len) noexcept { + uint32_t crc = 0xFFFFFFFFu; // initial value + for (size_t i = 0; i < len; ++i) crc = detail::crc32_update(crc, data[i]); + return crc ^ 0xFFFFFFFFu; // final XOR } -inline void put_u32(uint8_t* p, uint32_t v) noexcept { - p[0] = static_cast(v >> 24); - p[1] = static_cast((v >> 16) & 0xFF); - p[2] = static_cast((v >> 8) & 0xFF); - p[3] = static_cast(v & 0xFF); + +inline uint32_t crc32(const std::vector& data) noexcept { + return crc32(data.data(), data.size()); } -inline uint32_t get_u32(const uint8_t* p) noexcept { - return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) | - (uint32_t(p[2]) << 8) | uint32_t(p[3]); + +// ── CRC coverage & the trailing-CRC length pre-adjustment (extraction §4.7) ── +// Coverage is exactly: stream_id + avtp_timestamp (zero-filled when the +// frame rides under an NTSCF header, since NTSCF carries no avtp_timestamp +// field to begin with) + the full ACF shared header + payload. Because the +// CRC is computed *over* the ACF header, and the header's own +// acf_msg_length field must already reflect the trailer's length before +// that header is serialized, the length adjustment has to be applied +// first, not patched in after the fact. + +constexpr uint16_t kCrcLengthAdjustQuadlets = 1; // +1 quadlet on AcfMessageInfo::acf_msg_length +constexpr uint16_t kCrcLengthAdjustOctets = 4; // +4 octets on an outer AVTPDU frame-length field + +// apply_acf_length_adjustment mutates `info.acf_msg_length` in place, per +// kCrcLengthAdjustQuadlets. Call this before encode_acf_abb/encode_acf_gbb +// so the trailing CRC's length is already baked into the header the CRC +// itself covers. +inline void apply_acf_length_adjustment(wire::AcfMessageInfo& info) noexcept { + info.acf_msg_length = static_cast(info.acf_msg_length + kCrcLengthAdjustQuadlets); } -inline uint16_t get_u16(const uint8_t* p) noexcept { - return static_cast((uint16_t(p[0]) << 8) | p[1]); + +// apply_frame_length_adjustment mutates an outer AVTPDU header's +// control_data_length in place, per kCrcLengthAdjustOctets — the paired +// +4 octet adjustment at the frame level. Templated over +// wire::NtscfHeader/wire::TscfHeader since both carry a control_data_length +// field of the same name but rcp/wire.hpp deliberately keeps them as two +// independent structs with no common base to adjust through instead. +template +inline void apply_frame_length_adjustment(Header& hdr) noexcept { + hdr.control_data_length = static_cast(hdr.control_data_length + kCrcLengthAdjustOctets); } +namespace detail { +inline void put_u32_be(std::vector& buf, uint32_t v) { + buf.push_back(static_cast(v >> 24)); + buf.push_back(static_cast((v >> 16) & 0xFF)); + buf.push_back(static_cast((v >> 8) & 0xFF)); + buf.push_back(static_cast(v & 0xFF)); +} +inline void put_u64_be(std::vector& buf, uint64_t v) { + for (int i = 0; i < 8; ++i) buf.push_back(static_cast((v >> (56 - 8 * i)) & 0xFF)); +} } // namespace detail -// ── wrap / unwrap ───────────────────────────────────────────────────────────── - -// wrap prepends a 6-byte E2E header (seqNum + CRC-16) to payload. -inline std::vector wrap(uint32_t seq_num, const std::vector& payload) { - std::vector frame(HeaderLen + payload.size()); - detail::put_u32(&frame[0], seq_num); - std::copy(payload.begin(), payload.end(), frame.begin() + HeaderLen); - uint16_t crc = detail::crc16(&frame[0], 4, payload.data(), payload.size()); - detail::put_u16(&frame[4], crc); - return frame; -} - -// unwrap validates the CRC and extracts seq_num + original payload. -inline std::error_code unwrap(const std::vector& frame, - uint32_t& seq_num_out, - std::vector& payload_out) { - if (frame.size() < HeaderLen) - return make_error_code(E2EErrc::short_frame); - seq_num_out = detail::get_u32(frame.data()); - uint16_t got_crc = detail::get_u16(&frame[4]); - const uint8_t* pl = frame.data() + HeaderLen; - size_t pl_len = frame.size() - HeaderLen; - uint16_t want_crc = detail::crc16(frame.data(), 4, pl, pl_len); - if (got_crc != want_crc) - return make_error_code(E2EErrc::crc_mismatch); - payload_out.assign(pl, pl + pl_len); +// coverage_buffer assembles the exact byte sequence the E2E CRC is computed +// over (extraction §4.7). `avtp_timestamp` is std::nullopt when the frame +// rides under an NTSCF header — coverage_buffer supplies the documented +// zero-filled stand-in in that case rather than requiring the caller to +// pass an explicit zero (making the NTSCF case a visible, named choice at +// every call site instead of an easy-to-miss "just pass 0"). `info` must +// already have apply_acf_length_adjustment() applied if the caller wants +// the trailer's length reflected in the coverage — this function only +// serializes whatever AcfMessageInfo it is given. +inline std::vector coverage_buffer(const wire::StreamId& stream_id, + std::optional avtp_timestamp, + const wire::AcfMessageInfo& info, + const std::vector& payload) { + std::vector buf; + buf.reserve(8 + 4 + wire::kAcfCommonHeaderLen + payload.size()); + + detail::put_u64_be(buf, stream_id.to_u64()); + detail::put_u32_be(buf, avtp_timestamp.value_or(0)); // zero-filled stand-in under NTSCF + + uint8_t hdr[wire::kAcfCommonHeaderLen]; + wire::encode_acf_message_info(info, hdr); + buf.insert(buf.end(), hdr, hdr + wire::kAcfCommonHeaderLen); + + buf.insert(buf.end(), payload.begin(), payload.end()); + return buf; +} + +// compute_crc is coverage_buffer() + crc32() in one call — the usual way a +// caller actually wants this used. +inline uint32_t compute_crc(const wire::StreamId& stream_id, std::optional avtp_timestamp, + const wire::AcfMessageInfo& info, const std::vector& payload) { + return crc32(coverage_buffer(stream_id, avtp_timestamp, info, payload)); +} + +// append_crc appends the 4-octet big-endian CRC trailer to `frame` — the +// kCrcLengthAdjustOctets the frame-length field above must already account +// for. +inline void append_crc(std::vector& frame, uint32_t crc) { + frame.push_back(static_cast(crc >> 24)); + frame.push_back(static_cast((crc >> 16) & 0xFF)); + frame.push_back(static_cast((crc >> 8) & 0xFF)); + frame.push_back(static_cast(crc & 0xFF)); +} + +// verify_crc recomputes the CRC over stream_id/avtp_timestamp/info/payload +// and compares it against `received_crc` (as decoded from a frame's +// trailing 4 octets). Returns E2eErrc::crc_error — the CRC_ERROR failure +// path — on mismatch. +inline std::error_code verify_crc(const wire::StreamId& stream_id, std::optional avtp_timestamp, + const wire::AcfMessageInfo& info, const std::vector& payload, + uint32_t received_crc) { + if (compute_crc(stream_id, avtp_timestamp, info, payload) != received_crc) + return make_error_code(E2eErrc::crc_error); return {}; } -// ── ReplayGuard ─────────────────────────────────────────────────────────────── +// ── Per-endpoint opt-in safe mode (extraction §4.4, §4.7) ──────────────────── +// Which of an ACF message's three roles the caller is currently +// checking/producing a CRC for — regmap::EndpointGenericConfig's three +// ep_*_crc_enable toggles are independently settable per role. + +enum class MessageRole { Request, Acknowledge, Response }; + +// crc_required reports whether `cfg` opts the given message role into E2E +// CRC checking. A caller that gets `false` back should neither compute nor +// expect a trailing CRC for that role. +inline bool crc_required(const regmap::EndpointGenericConfig& cfg, MessageRole role) noexcept { + switch (role) { + case MessageRole::Request: return cfg.ep_req_crc_enable; + case MessageRole::Acknowledge: return cfg.ep_ack_crc_enable; + case MessageRole::Response: return cfg.ep_response_crc_enable; + } + return false; +} -constexpr size_t ReplayWindowSize = 32; +// implemented_options_bit reports regmap::kOptSafetyRequests when the +// caller's server actually implements the E2E CRC safe-point / safety- +// request mechanism — an explicit, testable single call site rather than a +// bit a caller might set by hand and forget to gate on real support, +// mirroring rcp::sequencer::implemented_options_bits' equivalent pattern. +inline uint32_t implemented_options_bit(bool safety_requests_implemented) noexcept { + return safety_requests_implemented ? regmap::kOptSafetyRequests : 0; +} -// ReplayGuard implements a bitmap sliding window to detect duplicate or -// replayed sequence numbers (ISO 26262 Part 7 E2E counter protection). -// -// The window covers [high_water - ReplayWindowSize + 1, high_water]. -// Sequence numbers older than the window floor are unconditionally rejected. -// Thread-safe. -class ReplayGuard { +// ── RxStreamGuard — rx_enforce_e2e (extraction §3.8) ───────────────────────── +// Implements the per-request-drop vs. whole-stream-latch choice: with +// rx_enforce_e2e clear, one failing request's CRC_ERROR is reported for +// that request alone; with it set, the first failure additionally latches +// the whole stream so every subsequent request also reports CRC_ERROR +// until reset_latch() is called (e.g. on stream reconfiguration). +class RxStreamGuard { public: - // check returns {} if seq_num is fresh and within the valid window, - // or E2EErrc::replay if it is a duplicate or too old. - std::error_code check(uint32_t seq_num) { - std::lock_guard lk(mu_); - - if (!initialized_) { - // Bootstrap — accept the very first sequence number. - initialized_ = true; - high_water_ = seq_num; - bitmap_[seq_num % ReplayWindowSize] = true; - return {}; - } + // record_crc_result is called once per checked request on this stream. + // `ok` is the outcome of that request's own verify_crc() call. + std::error_code record_crc_result(const regmap::RequestStreamConfig& cfg, bool ok) noexcept { + if (latched_) return make_error_code(E2eErrc::crc_error); + if (ok) return {}; + if (cfg.rx_enforce_e2e) latched_ = true; + return make_error_code(E2eErrc::crc_error); + } - if (seq_num > high_water_) { - // Advance window: clear slots that are no longer in range. - for (uint32_t i = high_water_ + 1; i != seq_num + 1; ++i) { - bitmap_[i % ReplayWindowSize] = false; - } - high_water_ = seq_num; - } else { - // seq_num <= high_water_ - uint32_t age = high_water_ - seq_num; - if (age >= static_cast(ReplayWindowSize)) { - // Too old — reject. - return make_error_code(E2EErrc::replay); - } - } + bool latched() const noexcept { return latched_; } + void reset_latch() noexcept { latched_ = false; } - size_t slot = seq_num % ReplayWindowSize; - if (bitmap_[slot]) { - return make_error_code(E2EErrc::replay); // duplicate within window - } - bitmap_[slot] = true; +private: + bool latched_ = false; +}; + +// ── RxSequenceGuard — rx_enforce_seq / rx_seq_safestate_enable ─────────────── +// Monotonic sequence-number check, orthogonal to the watchdog below — a +// stream can enforce either, both, or neither independently (extraction +// §3.8). This class only reports the violation; whether that additionally +// drives the endpoint into safe state is the caller's decision, gated on +// cfg.rx_seq_safestate_enable, same "primitive, not policy" split +// RxWatchdog uses below. +class RxSequenceGuard { +public: + // check verifies `seq` is strictly greater than the last accepted + // sequence number when cfg.rx_enforce_seq is set; disabled entirely + // (always accepts) when it is clear. The first observed sequence + // number is always accepted, bootstrapping the comparison. + std::error_code check(const regmap::RequestStreamConfig& cfg, uint32_t seq) noexcept { + if (!cfg.rx_enforce_seq) return {}; + if (has_last_ && seq <= last_seq_) return make_error_code(E2eErrc::sequence_violation); + has_last_ = true; + last_seq_ = seq; return {}; } private: - std::mutex mu_; - std::array bitmap_{}; - uint32_t high_water_ = 0; - bool initialized_ = false; + bool has_last_ = false; + uint32_t last_seq_ = 0; }; -// ── Controller wrapper ──────────────────────────────────────────────────────── - -// Controller wraps any rcp::Controller and automatically applies E2E protection -// (sequence counter + CRC-16) to every command payload on send(). -class Controller final : public rcp::Controller { +// ── RxWatchdog — rx_wd_* / rx_ovrflw_safestate_enable (extraction §3.8) ────── +// Per-request-stream watchdog and safe-state latch. Deciding *when* to call +// kick()/overflowed() (i.e. running a timer loop) is left to the embedding +// application, same as rcp::sequencer::select_next_due's equivalent +// "primitives, not a scheduler" split. +class RxWatchdog { public: - explicit Controller(std::shared_ptr inner) - : inner_(std::move(inner)) {} - - Zone zone() const noexcept override { return inner_->zone(); } - - std::error_code send(const rcp::Context& ctx, - const Command& cmd, - Response& out) override { - uint32_t seq = ++seq_; - Command protected_cmd = cmd; - protected_cmd.payload = wrap(seq, cmd.payload); - return inner_->send(ctx, protected_cmd, out); + // kick resets the watchdog's last-seen-activity clock to `now_ms`. + // Callers invoke this once per accepted request on the stream. + void kick(uint64_t now_ms) noexcept { + last_kick_ms_ = now_ms; + kicked_ = true; } - std::error_code subscribe(const rcp::Context& ctx, - std::shared_ptr& out) override { - return inner_->subscribe(ctx, out); + // overflowed reports whether more than cfg.rx_wd_timeout_interval + // milliseconds have elapsed since the last kick(), gated on + // cfg.rx_wd_enable — a disabled watchdog never overflows, and one that + // has never been kicked has nothing to time out yet. + bool overflowed(const regmap::RequestStreamConfig& cfg, uint64_t now_ms) const noexcept { + if (!cfg.rx_wd_enable || !kicked_) return false; + return (now_ms - last_kick_ms_) > cfg.rx_wd_timeout_interval; } - std::error_code close() override { return inner_->close(); } + // Safe-state latch. Entering safe state on watchdog/queue overflow is + // gated by the caller (see apply_watchdog_overflow/apply_queue_overflow + // below) on the relevant *_safestate_enable field — a register-map + // field existing does not by itself mean the behavior it gates is on. + void enter_safe_state() noexcept { in_safe_state_ = true; } + void clear_safe_state() noexcept { in_safe_state_ = false; } + bool in_safe_state() const noexcept { return in_safe_state_; } + + // should_emit_info_notification reports whether a repeating + // "still in safe state" notification (rx_wd_info_enable) should be + // sent right now — true exactly while latched into safe state with + // that feature enabled. Repeat cadence/timing is left to the embedding + // application. + bool should_emit_info_notification(const regmap::RequestStreamConfig& cfg) const noexcept { + return in_safe_state_ && cfg.rx_wd_info_enable; + } private: - std::shared_ptr inner_; - std::atomic seq_{0}; + bool kicked_ = false; + uint64_t last_kick_ms_ = 0; + bool in_safe_state_ = false; }; -// new_controller is a convenience factory. -inline std::shared_ptr new_controller(std::shared_ptr inner) { - return std::make_shared(std::move(inner)); +// apply_watchdog_overflow implements the roadmap's purge-normal/retain- +// safety rule for a watchdog-expiry trigger: when cfg.rx_wd_safestate_enable +// is set, `wd` latches into safe state and every pending/started *normal* +// (non-safety) request already queued on `ledger` for this stream is +// canceled exactly as clear-non-safestate (0x06) would, while safety-tagged +// (0x8x) records are left untouched so they can go on to drive the +// endpoint through its safe-state sequence. This reuses +// sequencer::RequestLedger::cancel_all directly rather than reimplementing +// cancellation — see rcp/sequencer.hpp's header comment. Returns the count +// cancel_all() itself returns (0 if the feature is disabled, since nothing +// is purged in that case). +inline size_t apply_watchdog_overflow(const regmap::RequestStreamConfig& cfg, RxWatchdog& wd, + sequencer::RequestLedger& ledger) noexcept { + if (!cfg.rx_wd_safestate_enable) return 0; + wd.enter_safe_state(); + return ledger.cancel_all(/*non_safestate_only=*/true); +} + +// apply_queue_overflow is the analogous rule for a request-queue overrun — +// a distinct trigger from watchdog expiry (extraction §3.8) that can also +// be configured, via cfg.rx_ovrflw_safestate_enable, to drive the endpoint +// into safe state and purge normal requests the same way. +inline size_t apply_queue_overflow(const regmap::RequestStreamConfig& cfg, RxWatchdog& wd, + sequencer::RequestLedger& ledger) noexcept { + if (!cfg.rx_ovrflw_safestate_enable) return 0; + wd.enter_safe_state(); + return ledger.cancel_all(/*non_safestate_only=*/true); +} + +// ── Safe-state gating for safety-tagged (0x8x) requests ────────────────────── +// The load-bearing rule the roadmap calls out as new relative to the +// pre-replacement design: a safety-tagged request only actually executes +// once the endpoint is in its configured safe state. + +// endpoint_in_configured_safe_state implements the two rx_safety_measure +// strategies (extraction §3.8): RxSafetyMeasure::ForceHighImpedance's +// "safe state" is an external boolean this header does not derive on its +// own — there is no sequencer to consult, so the embedding hardware layer +// reports it directly (typically via an RxWatchdog::in_safe_state() this +// class already tracks once entered, but callers with a different safe- +// state source may pass anything here); RxSafetyMeasure::RunSafeSequencer's +// "safe state" is structural: cfg.rx_safestate_sequencer currently holding +// exactly cfg.rx_safe_sequencer_state. +inline bool endpoint_in_configured_safe_state(const regmap::RequestStreamConfig& cfg, + const sequencer::SequencerTable& sequencers, + bool force_high_impedance_asserted) noexcept { + if (cfg.rx_safety_measure == regmap::RxSafetyMeasure::ForceHighImpedance) + return force_high_impedance_asserted; + regmap::SequencerState cur = 0; + auto ec = sequencers.state_of(cfg.rx_safestate_sequencer, cur); + if (ec) return false; + return cur == cfg.rx_safe_sequencer_state; +} + +// may_execute_now gates dispatch of one decoded request record: a +// non-safety record is always eligible as far as this rule is concerned +// (subject to whatever other checks the caller already applies elsewhere); +// a safety-tagged (0x8x, sequencer::RequestRecord::is_safety) record is +// only eligible once `endpoint_in_safe_state` is true. +inline bool may_execute_now(const sequencer::RequestRecord& rec, bool endpoint_in_safe_state) noexcept { + return !rec.is_safety || endpoint_in_safe_state; } } // namespace e2e } // namespace rcp + +// Enable std::error_code construction from rcp::e2e::E2eErrc. +namespace std { +template <> +struct is_error_code_enum : true_type {}; +} // namespace std diff --git a/include/rcp/regmap.hpp b/include/rcp/regmap.hpp index 205183c..5fe9f9a 100644 --- a/include/rcp/regmap.hpp +++ b/include/rcp/regmap.hpp @@ -12,6 +12,7 @@ // fusa:req REQ-REGMAP-012 // fusa:req REQ-REGMAP-013 // fusa:req REQ-REGMAP-014 +// fusa:req REQ-REGMAP-015 // RC Server register-map data model and EP0 pseudo-endpoint — the // whole-device configuration surface an OPEN Alliance TC18 Remote Control @@ -39,6 +40,14 @@ // scope here — this milestone only defines the generic/functional config // split and an opaque byte-blob functional config slot; interpreting that // slot's contents per endpoint type is v2.3.0 onward. +// +// ROADMAP.md milestone 50, "E2E CRC Safe Points & Safety-Request Variants +// (v2.6.0)": RequestStreamConfig's per-stream watchdog/safe-state fields +// and EndpointGenericConfig's per-endpoint CRC-enable toggles are expanded +// to their full field set at this milestone, superseding the three +// placeholder fields v2.1.0 reserved layout for. The behavior that reads +// and acts on these fields lives in rcp/e2e.hpp and rcp/sequencer.hpp, not +// here — same config-vs-behavior split as everything else in this header. #pragma once #include @@ -106,6 +115,16 @@ struct EndpointGenericConfig { std::vector hw_pin_indices; // indices into RegisterMap::hw_pin_map this endpoint claims uint16_t request_queue_size = 0; uint16_t response_queue_size = 0; + + // Per-endpoint opt-in E2E CRC "safe mode" (extraction §4.4, §4.7), + // ROADMAP.md milestone 50 (v2.6.0) — independently toggled per message + // role, since a given endpoint's request/ack/response traffic can have + // different integrity needs. Behavior lives in rcp/e2e.hpp; this struct + // only owns the durable configuration bits, same split as every other + // config-vs-behavior boundary in this codebase. + bool ep_req_crc_enable = false; + bool ep_ack_crc_enable = false; + bool ep_response_crc_enable = false; }; struct EndpointFunctionalConfig { @@ -119,19 +138,69 @@ struct HwPinMapEntry { uint8_t function = 0; // pin function/mode selector; meaning is endpoint-type-defined }; +// ── Sequencer-state registers (extraction §3.11, §3.16) ────────────────────── +// Persistent 8-bit values; behavior lives in rcp/sequencer.hpp (v2.5.0). +// Declared here, ahead of RequestStreamConfig below, since that struct's +// rx_safe_sequencer_state field (v2.6.0) needs the type name already +// in scope. + +using SequencerState = uint8_t; + +// ── rx_safety_measure selector (extraction §3.8) ────────────────────────────── +// Which mechanism a safety-tagged (0x8x) request drives the endpoint +// through once it is in safe state — ROADMAP.md milestone 50 (v2.6.0). +// Behavior lives in rcp/e2e.hpp; this enum is the durable register value +// selecting between the two. + +enum class RxSafetyMeasure : uint8_t { + ForceHighImpedance = 0, // hold outputs high-impedance; no sequencer consulted + RunSafeSequencer = 1, // "safe" is rx_safestate_sequencer reading rx_safe_sequencer_state +}; + // ── Request-stream config (extraction §3.8) ────────────────────────────────── -// rx_wd_timeout_s / rx_wd_action / rx_safety_measure exist here purely as -// durable register storage for now — the roadmap is explicit that only the -// *fields* need to exist at this milestone; the watchdog/safe-state behavior -// that reads and acts on them is wired up at v2.6.0. Storing a value here -// before then has no observable effect. +// The full per-request-stream watchdog and safe-state register set +// ROADMAP.md milestone 50 (v2.6.0) calls for. Behavior that reads and acts +// on these fields lives in rcp/e2e.hpp (watchdog overflow, CRC +// enforcement, sequence checking, safe-state gating) and +// rcp/sequencer.hpp (the 0x8x safety-tagged request variants these fields +// exist to support) — this struct is durable storage only, same +// config-vs-behavior split used throughout this header. +// +// This supersedes the three placeholder fields (rx_wd_timeout_s, +// rx_wd_action, rx_safety_measure as a bare uint8_t) v2.1.0 added purely to +// reserve register-map layout ahead of this milestone's real field list. struct RequestStreamConfig { wire::StreamId stream_id{}; - uint16_t queue_size = 0; - uint8_t rx_wd_timeout_s = 0; // watchdog timeout in seconds; unwired until v2.6.0 - uint8_t rx_wd_action = 0; // watchdog-expiry action selector; unwired until v2.6.0 - uint8_t rx_safety_measure = 0; // safety-measure selector; unwired until v2.6.0 + uint16_t queue_size = 0; + + // Watchdog (extraction §3.8). rx_wd_timeout_interval's unit + // (milliseconds) is this implementation's own choice, same as every + // other concrete-width decision elsewhere in this header. + uint32_t rx_wd_timeout_interval = 0; + bool rx_wd_enable = false; + bool rx_wd_safestate_enable = false; // watchdog overflow drives the endpoint into safe state + bool rx_wd_info_enable = false; // repeating notification while latched in safe state + + // E2E CRC enforcement (extraction §3.8, §4.7): per-request drop vs. + // whole-stream latch on a CRC_ERROR outcome. + bool rx_enforce_e2e = false; + + // Monotonic sequence-number check — orthogonal to the watchdog above; a + // stream can enforce either, both, or neither independently. + bool rx_enforce_seq = false; + bool rx_seq_safestate_enable = false; + + // Request-queue overrun is a distinct trigger from watchdog expiry that + // can also be configured to drive the endpoint into safe state. + bool rx_ovrflw_safestate_enable = false; + + // Which mechanism a safety-tagged request drives the endpoint through + // once in safe state, and (for RunSafeSequencer only) which sequencer + // and target state together define "safe". + RxSafetyMeasure rx_safety_measure = RxSafetyMeasure::ForceHighImpedance; + uint16_t rx_safestate_sequencer = 0; + SequencerState rx_safe_sequencer_state = 0; }; // ── EP-ID / byte_bus_id mapping table (extraction §3.9) ────────────────────── @@ -156,14 +225,6 @@ struct ResponseQueueConfig { uint16_t ack_queue_size = 0; }; -// ── Sequencer-state registers (extraction §3.11, §3.16) ────────────────────── -// Persistent 8-bit values; the sequencer behavior that reads and advances -// them is a v2.5.0 concern. This milestone only allocates durable, stably -// addressed storage for them so later milestones don't need a register-map -// layout change to introduce that behavior. - -using SequencerState = uint8_t; - // ── svr_implemented_options bitmask ────────────────────────────────────────── // Advertises which optional protocol features this server implements. Bits // are reserved here for features whose actual implementation lands in later diff --git a/include/rcp/sequencer.hpp b/include/rcp/sequencer.hpp index 10886a0..7f4935f 100644 --- a/include/rcp/sequencer.hpp +++ b/include/rcp/sequencer.hpp @@ -7,6 +7,9 @@ // fusa:req REQ-SEQ-007 // fusa:req REQ-SEQ-008 // fusa:req REQ-SEQ-009 +// fusa:req REQ-SEQ-010 +// fusa:req REQ-SEQ-011 +// fusa:req REQ-SEQ-012 // Conditional-request taxonomy and sequencer-state primitives — the // message_timestamp-repurposing decode, the five conditional request kinds @@ -28,6 +31,19 @@ // rules a real compound-wait dispatch loop would call once it decides a // compound-wait request is due; this header does not re-implement either. // +// ROADMAP.md milestone 50, "E2E CRC Safe Points & Safety-Request Variants +// (v2.6.0)", extends this taxonomy in place rather than superseding it: the +// three MSB-set (0x8x) safety-tagged opcodes (CompoundSafety, +// CompoundWaitSafety, TriggeredSafety), is_safety_variant(), and +// request_record_for() are new; everything else in this file — the +// lifecycle state machine, cancellation semantics (including +// cancel_all(non_safestate_only), which already implements v2.6.0's +// watchdog-overflow purge-normal/retain-safety rule), and the priority +// ordering — is unchanged. Safe-state execution eligibility itself +// (whether a safety-tagged request's endpoint is actually in its +// configured safe state right now) is evaluated by rcp/e2e.hpp, not here — +// this header only decodes and categorizes the opcode. +// // 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 opcode-to-byte @@ -119,15 +135,38 @@ inline std::error_code make_error_code(SequencerErrc e) noexcept { // no opcode here at all — it is always carried as ACF_ABB, which has no // message_timestamp slot to repurpose in the first place. +// ROADMAP.md milestone 50 (v2.6.0) adds three more: the MSB-set (`0x8x`) +// safety-tagged variants of compound, compound-wait, and triggered. Each +// shares its base opcode's low 7 bits with its normal counterpart +// (Compound 0x0F -> CompoundSafety 0x8F, CompoundWait 0x0B -> +// CompoundWaitSafety 0x8B, Triggered 0x0E -> TriggeredSafety 0x8E) — the +// MSB alone is what marks a request as safety-tagged (extraction §2.7, §6 +// item 4); is_safety_variant() below is the single place that bit gets +// interpreted, rather than every call site re-deriving it from the raw +// byte value. A safety-tagged request only actually executes once its +// endpoint is in its configured safe state — see rcp/e2e.hpp's +// endpoint_in_configured_safe_state()/may_execute_now(), which this header +// does not itself evaluate (no dependency on rcp/regmap.hpp's +// RequestStreamConfig watchdog/safe-state fields here, consistent with +// this file's existing "primitives, not policy" scope). Cancellation's +// existing non_safestate_only bundle (0x06 / cancel_all(true)) already +// implements the watchdog-overflow purge-normal/retain-safety queue +// behavior the roadmap calls for — see rcp/e2e.hpp's +// apply_watchdog_overflow(), which is a thin wrapper around that call, not +// a reimplementation of it. + enum class RequestTypeOpcode : uint8_t { - Chained = 0x01, - ClearAll = 0x05, - ClearNonSafestate = 0x06, - ClearSingle = 0x07, - Timed = 0x0A, - CompoundWait = 0x0B, - Triggered = 0x0E, - Compound = 0x0F, + Chained = 0x01, + ClearAll = 0x05, + ClearNonSafestate = 0x06, + ClearSingle = 0x07, + Timed = 0x0A, + CompoundWait = 0x0B, + Triggered = 0x0E, + Compound = 0x0F, + CompoundWaitSafety = 0x8B, + TriggeredSafety = 0x8E, + CompoundSafety = 0x8F, }; constexpr bool is_valid_request_type(uint8_t byte) noexcept { @@ -140,6 +179,25 @@ constexpr bool is_valid_request_type(uint8_t byte) noexcept { case RequestTypeOpcode::CompoundWait: case RequestTypeOpcode::Triggered: case RequestTypeOpcode::Compound: + case RequestTypeOpcode::CompoundWaitSafety: + case RequestTypeOpcode::TriggeredSafety: + case RequestTypeOpcode::CompoundSafety: + return true; + default: + return false; + } +} + +// is_safety_variant reports whether `type` is one of the three MSB-set +// (`0x8x`) safety-tagged opcodes above. This is the single source of truth +// RequestRecord::is_safety (below) is derived from — see +// request_record_for() — rather than something a caller sets by hand and +// might forget for a safety-tagged decode path. +constexpr bool is_safety_variant(RequestTypeOpcode type) noexcept { + switch (type) { + case RequestTypeOpcode::CompoundWaitSafety: + case RequestTypeOpcode::TriggeredSafety: + case RequestTypeOpcode::CompoundSafety: return true; default: return false; @@ -213,7 +271,11 @@ enum class RequestCategory : uint8_t { // category_of maps a decoded request_type opcode onto its priority category; // `std::nullopt` denotes the mandatory standard request kind (no opcode, per -// the header comment above). +// the header comment above). A safety-tagged (0x8x) opcode shares its base +// opcode's category — the MSB affects safe-state execution eligibility and +// watchdog-purge treatment (rcp/e2e.hpp), not where it sits in the +// cancellation > triggered > timed > compound > compound-wait > chained > +// standard priority ordering (extraction §3.14). constexpr RequestCategory category_of(std::optional kind) noexcept { if (!kind.has_value()) return RequestCategory::Standard; switch (*kind) { @@ -221,12 +283,21 @@ constexpr RequestCategory category_of(std::optional kind) noe case RequestTypeOpcode::ClearNonSafestate: case RequestTypeOpcode::ClearSingle: return RequestCategory::Cancellation; - case RequestTypeOpcode::Triggered: return RequestCategory::Triggered; - case RequestTypeOpcode::Timed: return RequestCategory::Timed; - case RequestTypeOpcode::Compound: return RequestCategory::Compound; - case RequestTypeOpcode::CompoundWait: return RequestCategory::CompoundWait; - case RequestTypeOpcode::Chained: return RequestCategory::Chained; - default: return RequestCategory::Standard; // unreachable given a validated opcode + case RequestTypeOpcode::Triggered: + case RequestTypeOpcode::TriggeredSafety: + return RequestCategory::Triggered; + case RequestTypeOpcode::Timed: + return RequestCategory::Timed; + case RequestTypeOpcode::Compound: + case RequestTypeOpcode::CompoundSafety: + return RequestCategory::Compound; + case RequestTypeOpcode::CompoundWait: + case RequestTypeOpcode::CompoundWaitSafety: + return RequestCategory::CompoundWait; + case RequestTypeOpcode::Chained: + return RequestCategory::Chained; + default: + return RequestCategory::Standard; // unreachable given a validated opcode } } @@ -413,7 +484,7 @@ struct RequestRecord { std::optional request_type; RequestState state = RequestState::Pending; bool cs = false; - bool is_safety = false; // always false until v2.6.0's 0x8x safety variants exist + bool is_safety = false; // true for a decoded 0x8x opcode — see request_record_for() size_t arrival_seq = 0; // assigned by RequestLedger::submit, FIFO tie-break key // Compound / compound-wait finalization target (unused by other kinds). @@ -427,6 +498,24 @@ struct RequestRecord { std::optional outcome; // set to request_canceled once state == Canceled }; +// request_record_for builds a RequestRecord with `is_safety` derived +// automatically from `type` via is_safety_variant() rather than left for +// the caller to set by hand — ROADMAP.md milestone 50 (v2.6.0)'s hook for +// the 0x8x decode path referenced in this header's own comments above. +// Compound/compound-wait/chained-specific fields (sequencer_index, +// expected_start_state, chained_predecessor, chained_successors) are left +// at their defaults; callers that need them still set them directly, same +// as before this helper existed. +inline RequestRecord request_record_for(uint8_t transaction_num, std::optional type, + bool cs) noexcept { + RequestRecord rec; + rec.transaction_num = transaction_num; + rec.request_type = type; + rec.cs = cs; + rec.is_safety = type.has_value() && is_safety_variant(*type); + return rec; +} + // RequestLedger tracks every in-flight request's lifecycle state and // implements the cancellation and chained-abort semantics that act across // records (extraction §2.7, §3.14). It does not itself decide *when* a diff --git a/include/rcp/version.hpp b/include/rcp/version.hpp index c2b0de0..03c05b7 100644 --- a/include/rcp/version.hpp +++ b/include/rcp/version.hpp @@ -6,6 +6,6 @@ namespace rcp { // Semantic version of the cpp-RCP implementation (matches the latest git tag). -constexpr std::string_view kVersion = "2.5.0"; +constexpr std::string_view kVersion = "2.6.0"; } // namespace rcp diff --git a/tests/test_e2e.cpp b/tests/test_e2e.cpp index 7764101..2606daa 100644 --- a/tests/test_e2e.cpp +++ b/tests/test_e2e.cpp @@ -6,79 +6,399 @@ // fusa:test REQ-E2E-006 // fusa:test REQ-E2E-007 // fusa:test REQ-E2E-008 +// fusa:test REQ-E2E-009 +// fusa:test REQ-E2E-010 +// fusa:test REQ-E2E-011 +// fusa:test REQ-E2E-012 +// fusa:test REQ-E2E-013 +// fusa:test REQ-E2E-014 + +// Tests for rcp/e2e.hpp — E2E CRC safe points and the per-request-stream +// watchdog/safe-state primitives (ROADMAP.md milestone 50, "E2E CRC Safe +// Points & Safety-Request Variants", v2.6.0). #include #include using namespace rcp::e2e; +using rcp::regmap::EndpointGenericConfig; +using rcp::regmap::RequestStreamConfig; +using rcp::regmap::RxSafetyMeasure; +using rcp::sequencer::RequestLedger; +using rcp::sequencer::RequestRecord; +using rcp::sequencer::RequestTypeOpcode; +using rcp::sequencer::SequencerTable; +using rcp::sequencer::request_record_for; +using rcp::wire::AcfMessageInfo; +using rcp::wire::StreamId; + +namespace { + +StreamId make_stream_id(uint8_t mac_seed, uint16_t suffix) { + StreamId id; + for (auto& b : id.mac) b = mac_seed++; + id.suffix = suffix; + return id; +} -// ── wrap / unwrap round-trip ────────────────────────────────────────────────── +} // namespace -TEST_CASE("wrap / unwrap round-trip preserves payload", "[e2e][REQ-E2E-001]") { - std::vector payload{1, 2, 3, 4, 5}; - auto frame = wrap(1, payload); - REQUIRE(frame.size() == payload.size() + HeaderLen); +// ── CRC32 primitive ─────────────────────────────────────────────────────────── - uint32_t seq_out; - std::vector payload_out; - REQUIRE_FALSE(unwrap(frame, seq_out, payload_out)); - REQUIRE(seq_out == 1); - REQUIRE(payload_out == payload); +TEST_CASE("crc32 of empty input is the all-ones init XORed with all-ones xorout", "[e2e][REQ-E2E-001]") { + // With init=0xFFFFFFFF and no bytes processed, the update loop never + // runs, so the result is init ^ xorout == 0xFFFFFFFF ^ 0xFFFFFFFF == 0. + REQUIRE(crc32(nullptr, 0) == 0u); } -TEST_CASE("wrap produces frame with E2E header prepended", "[e2e][REQ-E2E-002]") { - std::vector payload{0xAB, 0xCD}; - auto frame = wrap(0xCAFEBABE, payload); - REQUIRE(frame.size() == 2 + HeaderLen); - // First 4 bytes are big-endian seq - uint32_t seq = (uint32_t(frame[0]) << 24) | (uint32_t(frame[1]) << 16) - | (uint32_t(frame[2]) << 8) | uint32_t(frame[3]); - REQUIRE(seq == 0xCAFEBABE); +TEST_CASE("crc32 is deterministic and sensitive to every input byte", "[e2e][REQ-E2E-001]") { + std::vector a{0x01, 0x02, 0x03, 0x04}; + std::vector b{0x01, 0x02, 0x03, 0x05}; + REQUIRE(crc32(a) == crc32(a)); + REQUIRE(crc32(a) != crc32(b)); } -TEST_CASE("unwrap rejects truncated frame", "[e2e][REQ-E2E-003]") { - std::vector short_frame(HeaderLen - 1, 0); - uint32_t seq; - std::vector out; - REQUIRE(unwrap(short_frame, seq, out)); +TEST_CASE("crc32 known-vector regression check", "[e2e][REQ-E2E-001]") { + // Cross-checked against an independent reference implementation of the + // standard reflected-CRC construction (RefIn=true, RefOut=true, init + // 0xFFFFFFFF, xorout 0xFFFFFFFF) using this file's own polynomial + // (0xF4ACFB13) — not a vector taken from the confidential specification + // text, purely a regression guard against this implementation + // silently changing behavior. + std::vector data{'1', '2', '3', '4', '5', '6', '7', '8', '9'}; + REQUIRE(crc32(data) == 0x1697D06Au); } -TEST_CASE("unwrap rejects corrupt CRC", "[e2e][REQ-E2E-004]") { +// ── CRC coverage & length adjustment ────────────────────────────────────────── + +TEST_CASE("coverage_buffer zero-fills avtp_timestamp under NTSCF (nullopt)", "[e2e][REQ-E2E-002]") { + auto sid = make_stream_id(0x02, 0x1234); + AcfMessageInfo info; + info.byte_bus_id = 5; + std::vector payload{0xAA, 0xBB}; + + auto with_zero = coverage_buffer(sid, uint32_t{0}, info, payload); + auto with_nullopt = coverage_buffer(sid, std::nullopt, info, payload); + REQUIRE(with_zero == with_nullopt); +} + +TEST_CASE("coverage_buffer layout is stream_id + avtp_timestamp + ACF header + payload", + "[e2e][REQ-E2E-002]") { + auto sid = make_stream_id(0x02, 0x0001); + AcfMessageInfo info; + info.byte_bus_id = 7; std::vector payload{1, 2, 3}; - auto frame = wrap(1, payload); - frame.back() ^= 0xFF; // corrupt last byte (CRC) - uint32_t seq; - std::vector out; - REQUIRE(unwrap(frame, seq, out)); + + auto buf = coverage_buffer(sid, uint32_t{0xDEADBEEF}, info, payload); + REQUIRE(buf.size() == 8 + 4 + rcp::wire::kAcfCommonHeaderLen + payload.size()); + + // stream_id occupies the first 8 bytes, big-endian. + REQUIRE(buf[0] == sid.mac[0]); + // avtp_timestamp occupies the next 4 bytes, big-endian. + REQUIRE(buf[8] == 0xDE); + REQUIRE(buf[9] == 0xAD); + REQUIRE(buf[10] == 0xBE); + REQUIRE(buf[11] == 0xEF); + // Payload is the final bytes, unchanged. + REQUIRE(buf[buf.size() - 3] == 1); + REQUIRE(buf[buf.size() - 1] == 3); +} + +TEST_CASE("compute_crc changes when covered fields change", "[e2e][REQ-E2E-002]") { + auto sid = make_stream_id(0x02, 0x0001); + AcfMessageInfo info; + info.byte_bus_id = 1; + std::vector payload{9, 9, 9}; + + uint32_t base = compute_crc(sid, std::nullopt, info, payload); + + AcfMessageInfo different_info = info; + different_info.byte_bus_id = 2; + REQUIRE(compute_crc(sid, std::nullopt, different_info, payload) != base); + + std::vector different_payload{9, 9, 8}; + REQUIRE(compute_crc(sid, std::nullopt, info, different_payload) != base); + + REQUIRE(compute_crc(sid, uint32_t{1}, info, payload) != base); +} + +TEST_CASE("apply_acf_length_adjustment adds exactly one quadlet", "[e2e][REQ-E2E-003]") { + AcfMessageInfo info; + info.acf_msg_length = 10; + apply_acf_length_adjustment(info); + REQUIRE(info.acf_msg_length == 10 + kCrcLengthAdjustQuadlets); + REQUIRE(kCrcLengthAdjustQuadlets == 1); +} + +TEST_CASE("apply_frame_length_adjustment adds exactly four octets to NTSCF and TSCF headers", + "[e2e][REQ-E2E-003]") { + rcp::wire::NtscfHeader ntscf; + ntscf.control_data_length = 20; + apply_frame_length_adjustment(ntscf); + REQUIRE(ntscf.control_data_length == 20 + kCrcLengthAdjustOctets); + + rcp::wire::TscfHeader tscf; + tscf.control_data_length = 30; + apply_frame_length_adjustment(tscf); + REQUIRE(tscf.control_data_length == 30 + kCrcLengthAdjustOctets); + + REQUIRE(kCrcLengthAdjustOctets == 4); } -// ── ReplayGuard ─────────────────────────────────────────────────────────────── +// ── verify_crc / append_crc ──────────────────────────────────────────────────── -TEST_CASE("ReplayGuard accepts fresh sequence numbers", "[e2e][REQ-E2E-005]") { - ReplayGuard guard; - REQUIRE_FALSE(guard.check(1)); - REQUIRE_FALSE(guard.check(2)); - REQUIRE_FALSE(guard.check(3)); +TEST_CASE("verify_crc accepts a matching CRC and rejects a corrupted one", "[e2e][REQ-E2E-004]") { + auto sid = make_stream_id(0x02, 0x0002); + AcfMessageInfo info; + info.byte_bus_id = 3; + std::vector payload{1, 2, 3, 4}; + + uint32_t crc = compute_crc(sid, std::nullopt, info, payload); + REQUIRE_FALSE(verify_crc(sid, std::nullopt, info, payload, crc)); + REQUIRE(verify_crc(sid, std::nullopt, info, payload, crc ^ 0xFFFFFFFFu) == + make_error_code(E2eErrc::crc_error)); +} + +TEST_CASE("append_crc appends exactly 4 big-endian octets", "[e2e][REQ-E2E-004]") { + std::vector frame{0x11, 0x22}; + append_crc(frame, 0x01020304); + REQUIRE(frame.size() == 6); + REQUIRE(frame[2] == 0x01); + REQUIRE(frame[3] == 0x02); + REQUIRE(frame[4] == 0x03); + REQUIRE(frame[5] == 0x04); +} + +// ── Per-endpoint opt-in safe mode ───────────────────────────────────────────── + +TEST_CASE("crc_required reflects each independently-toggled endpoint config field", + "[e2e][REQ-E2E-005]") { + EndpointGenericConfig cfg; + REQUIRE_FALSE(crc_required(cfg, MessageRole::Request)); + REQUIRE_FALSE(crc_required(cfg, MessageRole::Acknowledge)); + REQUIRE_FALSE(crc_required(cfg, MessageRole::Response)); + + cfg.ep_req_crc_enable = true; + REQUIRE(crc_required(cfg, MessageRole::Request)); + REQUIRE_FALSE(crc_required(cfg, MessageRole::Acknowledge)); + REQUIRE_FALSE(crc_required(cfg, MessageRole::Response)); + + cfg.ep_ack_crc_enable = true; + cfg.ep_response_crc_enable = true; + REQUIRE(crc_required(cfg, MessageRole::Acknowledge)); + REQUIRE(crc_required(cfg, MessageRole::Response)); +} + +TEST_CASE("implemented_options_bit reports kOptSafetyRequests only when actually implemented", + "[e2e][REQ-E2E-005]") { + REQUIRE(implemented_options_bit(false) == 0u); + REQUIRE(implemented_options_bit(true) == rcp::regmap::kOptSafetyRequests); +} + +// ── RxStreamGuard — rx_enforce_e2e ──────────────────────────────────────────── + +TEST_CASE("RxStreamGuard drops only the failing request when rx_enforce_e2e is clear", + "[e2e][REQ-E2E-006]") { + RequestStreamConfig cfg; // rx_enforce_e2e defaults to false + RxStreamGuard guard; + + REQUIRE(guard.record_crc_result(cfg, /*ok=*/false) == make_error_code(E2eErrc::crc_error)); + REQUIRE_FALSE(guard.latched()); + REQUIRE_FALSE(guard.record_crc_result(cfg, /*ok=*/true)); // next request unaffected } -TEST_CASE("ReplayGuard rejects duplicates", "[e2e][REQ-E2E-006]") { - ReplayGuard guard; - REQUIRE_FALSE(guard.check(10)); - REQUIRE(guard.check(10)); // duplicate +TEST_CASE("RxStreamGuard latches the whole stream when rx_enforce_e2e is set", "[e2e][REQ-E2E-006]") { + RequestStreamConfig cfg; + cfg.rx_enforce_e2e = true; + RxStreamGuard guard; + + REQUIRE(guard.record_crc_result(cfg, /*ok=*/false) == make_error_code(E2eErrc::crc_error)); + REQUIRE(guard.latched()); + // Every subsequent request fails too, even one whose own CRC was fine. + REQUIRE(guard.record_crc_result(cfg, /*ok=*/true) == make_error_code(E2eErrc::crc_error)); + + guard.reset_latch(); + REQUIRE_FALSE(guard.latched()); + REQUIRE_FALSE(guard.record_crc_result(cfg, /*ok=*/true)); } -TEST_CASE("ReplayGuard rejects old seq numbers outside window", "[e2e][REQ-E2E-007]") { - ReplayGuard guard; - // Advance window past ReplayWindowSize - for (uint32_t i = 1; i <= ReplayWindowSize + 5; ++i) { - REQUIRE_FALSE(guard.check(i)); - } - // seq=1 is now outside the window - REQUIRE(guard.check(1)); +// ── RxSequenceGuard — rx_enforce_seq ─────────────────────────────────────────── + +TEST_CASE("RxSequenceGuard is a no-op when rx_enforce_seq is clear", "[e2e][REQ-E2E-007]") { + RequestStreamConfig cfg; // rx_enforce_seq defaults to false + RxSequenceGuard guard; + REQUIRE_FALSE(guard.check(cfg, 5)); + REQUIRE_FALSE(guard.check(cfg, 1)); // would violate monotonicity if enforced } -TEST_CASE("ReplayGuard accepts seq=0 exactly once", "[e2e][REQ-E2E-008]") { - ReplayGuard guard; - REQUIRE_FALSE(guard.check(0)); - REQUIRE(guard.check(0)); +TEST_CASE("RxSequenceGuard rejects a non-increasing sequence number when enforced", + "[e2e][REQ-E2E-007]") { + RequestStreamConfig cfg; + cfg.rx_enforce_seq = true; + RxSequenceGuard guard; + + REQUIRE_FALSE(guard.check(cfg, 10)); // bootstrap accepts the first value + REQUIRE_FALSE(guard.check(cfg, 11)); + REQUIRE(guard.check(cfg, 11) == make_error_code(E2eErrc::sequence_violation)); // repeat + REQUIRE(guard.check(cfg, 9) == make_error_code(E2eErrc::sequence_violation)); // regression +} + +// ── RxWatchdog — rx_wd_enable / rx_wd_timeout_interval ──────────────────────── + +TEST_CASE("RxWatchdog never overflows while disabled or before any kick", "[e2e][REQ-E2E-008]") { + RequestStreamConfig cfg; + cfg.rx_wd_timeout_interval = 100; + RxWatchdog wd; + REQUIRE_FALSE(wd.overflowed(cfg, /*now_ms=*/10'000)); // rx_wd_enable defaults to false + + cfg.rx_wd_enable = true; + REQUIRE_FALSE(wd.overflowed(cfg, /*now_ms=*/10'000)); // never kicked +} + +TEST_CASE("RxWatchdog overflows once the timeout interval elapses since the last kick", + "[e2e][REQ-E2E-008]") { + RequestStreamConfig cfg; + cfg.rx_wd_enable = true; + cfg.rx_wd_timeout_interval = 100; + RxWatchdog wd; + + wd.kick(1'000); + REQUIRE_FALSE(wd.overflowed(cfg, 1'050)); + REQUIRE(wd.overflowed(cfg, 1'101)); + + wd.kick(1'101); + REQUIRE_FALSE(wd.overflowed(cfg, 1'150)); +} + +// ── Watchdog/queue overflow purge-normal/retain-safety ──────────────────────── + +TEST_CASE("apply_watchdog_overflow purges normal requests but retains safety-tagged ones", + "[e2e][REQ-E2E-009]") { + RequestStreamConfig cfg; + cfg.rx_wd_safestate_enable = true; + + RequestLedger ledger; + REQUIRE_FALSE(ledger.submit(request_record_for(1, RequestTypeOpcode::Compound, /*cs=*/false))); + REQUIRE_FALSE(ledger.submit(request_record_for(2, RequestTypeOpcode::CompoundSafety, /*cs=*/false))); + + RxWatchdog wd; + size_t purged = apply_watchdog_overflow(cfg, wd, ledger); + + REQUIRE(purged == 1); + REQUIRE(wd.in_safe_state()); + REQUIRE(ledger.find(1)->state == rcp::sequencer::RequestState::Canceled); + REQUIRE(ledger.find(2)->state == rcp::sequencer::RequestState::Pending); +} + +TEST_CASE("apply_watchdog_overflow purges nothing and does not enter safe state when disabled", + "[e2e][REQ-E2E-009]") { + RequestStreamConfig cfg; // rx_wd_safestate_enable defaults to false + RequestLedger ledger; + REQUIRE_FALSE(ledger.submit(request_record_for(1, RequestTypeOpcode::Compound, /*cs=*/false))); + + RxWatchdog wd; + REQUIRE(apply_watchdog_overflow(cfg, wd, ledger) == 0); + REQUIRE_FALSE(wd.in_safe_state()); + REQUIRE(ledger.find(1)->state == rcp::sequencer::RequestState::Pending); +} + +TEST_CASE("apply_queue_overflow implements the same purge-normal/retain-safety rule via a " + "distinct trigger", + "[e2e][REQ-E2E-010]") { + RequestStreamConfig cfg; + cfg.rx_ovrflw_safestate_enable = true; + + RequestLedger ledger; + REQUIRE_FALSE(ledger.submit(request_record_for(1, RequestTypeOpcode::Triggered, /*cs=*/false))); + REQUIRE_FALSE(ledger.submit(request_record_for(2, RequestTypeOpcode::TriggeredSafety, /*cs=*/false))); + + RxWatchdog wd; + REQUIRE(apply_queue_overflow(cfg, wd, ledger) == 1); + REQUIRE(wd.in_safe_state()); + REQUIRE(ledger.find(1)->state == rcp::sequencer::RequestState::Canceled); + REQUIRE(ledger.find(2)->state == rcp::sequencer::RequestState::Pending); +} + +// ── Safe-state gating ───────────────────────────────────────────────────────── + +TEST_CASE("endpoint_in_configured_safe_state: ForceHighImpedance defers to the caller-supplied flag", + "[e2e][REQ-E2E-011]") { + RequestStreamConfig cfg; // rx_safety_measure defaults to ForceHighImpedance + std::vector states; + SequencerTable sequencers(states); + + REQUIRE_FALSE(endpoint_in_configured_safe_state(cfg, sequencers, /*force_high_impedance_asserted=*/false)); + REQUIRE(endpoint_in_configured_safe_state(cfg, sequencers, /*force_high_impedance_asserted=*/true)); +} + +TEST_CASE("endpoint_in_configured_safe_state: RunSafeSequencer checks the target sequencer's value", + "[e2e][REQ-E2E-011]") { + RequestStreamConfig cfg; + cfg.rx_safety_measure = RxSafetyMeasure::RunSafeSequencer; + cfg.rx_safestate_sequencer = 0; + cfg.rx_safe_sequencer_state = 3; + + std::vector states; + SequencerTable sequencers(states); + sequencers.ensure_size(1); // starts at SequencerTable::kDefaultState (1), not 3 + + REQUIRE_FALSE(endpoint_in_configured_safe_state(cfg, sequencers, /*force_high_impedance_asserted=*/true)); + + states[0] = 3; + REQUIRE(endpoint_in_configured_safe_state(cfg, sequencers, /*force_high_impedance_asserted=*/false)); +} + +TEST_CASE("endpoint_in_configured_safe_state: RunSafeSequencer with an out-of-range index is never safe", + "[e2e][REQ-E2E-011]") { + RequestStreamConfig cfg; + cfg.rx_safety_measure = RxSafetyMeasure::RunSafeSequencer; + cfg.rx_safestate_sequencer = 5; // no sequencer at this index + + std::vector states; + SequencerTable sequencers(states); + REQUIRE_FALSE(endpoint_in_configured_safe_state(cfg, sequencers, true)); +} + +TEST_CASE("may_execute_now: normal requests are always eligible; safety requests need safe state", + "[e2e][REQ-E2E-012]") { + RequestRecord normal = request_record_for(1, RequestTypeOpcode::Compound, false); + RequestRecord safety = request_record_for(2, RequestTypeOpcode::CompoundSafety, false); + + REQUIRE(may_execute_now(normal, /*endpoint_in_safe_state=*/false)); + REQUIRE(may_execute_now(normal, /*endpoint_in_safe_state=*/true)); + REQUIRE_FALSE(may_execute_now(safety, /*endpoint_in_safe_state=*/false)); + REQUIRE(may_execute_now(safety, /*endpoint_in_safe_state=*/true)); +} + +// ── Watchdog info notification ──────────────────────────────────────────────── + +TEST_CASE("RxWatchdog emits the info notification only while latched with rx_wd_info_enable set", + "[e2e][REQ-E2E-013]") { + RequestStreamConfig cfg; + RxWatchdog wd; + + REQUIRE_FALSE(wd.should_emit_info_notification(cfg)); // not latched yet + + wd.enter_safe_state(); + REQUIRE_FALSE(wd.should_emit_info_notification(cfg)); // latched, but feature disabled + + cfg.rx_wd_info_enable = true; + REQUIRE(wd.should_emit_info_notification(cfg)); + + wd.clear_safe_state(); + REQUIRE_FALSE(wd.should_emit_info_notification(cfg)); +} + +// ── Error category ──────────────────────────────────────────────────────────── + +TEST_CASE("E2eErrc is a distinct error category with non-empty, distinct messages", + "[e2e][REQ-E2E-014]") { + std::error_code crc = make_error_code(E2eErrc::crc_error); + std::error_code seq = make_error_code(E2eErrc::sequence_violation); + + REQUIRE(crc.category() == e2e_category()); + REQUIRE(seq.category() == e2e_category()); + REQUIRE_FALSE(crc.message().empty()); + REQUIRE_FALSE(seq.message().empty()); + REQUIRE(crc.message() != seq.message()); } diff --git a/tests/test_regmap.cpp b/tests/test_regmap.cpp index 3f88776..9ddc776 100644 --- a/tests/test_regmap.cpp +++ b/tests/test_regmap.cpp @@ -12,6 +12,7 @@ // fusa:test REQ-REGMAP-012 // fusa:test REQ-REGMAP-013 // fusa:test REQ-REGMAP-014 +// fusa:test REQ-REGMAP-015 // Tests for rcp/regmap.hpp — the RC Server register-map data model and EP0 // pseudo-endpoint (ROADMAP.md milestone 45, "RC Server Lifecycle & @@ -229,19 +230,63 @@ TEST_CASE("HW pin-map table entries are stored and retrievable", "[regmap][REQ-R REQUIRE(m.hw_pin_map[1].function == 2); } -// ── Request-stream config, including inert rx_* fields ────────────────────────── +// ── Request-stream config: the full v2.6.0 watchdog/safe-state field set ──────── -TEST_CASE("RequestStreamConfig carries the rx_* fields needed later for watchdog/safe-state", +TEST_CASE("RequestStreamConfig carries the full watchdog/safe-state register set (v2.6.0)", "[regmap][REQ-REGMAP-009]") { RequestStreamConfig rsc; - rsc.queue_size = 4; - rsc.rx_wd_timeout_s = 5; - rsc.rx_wd_action = 1; - rsc.rx_safety_measure = 2; - - REQUIRE(rsc.rx_wd_timeout_s == 5); - REQUIRE(rsc.rx_wd_action == 1); - REQUIRE(rsc.rx_safety_measure == 2); + rsc.queue_size = 4; + rsc.rx_wd_timeout_interval = 500; + rsc.rx_wd_enable = true; + rsc.rx_wd_safestate_enable = true; + rsc.rx_wd_info_enable = true; + rsc.rx_enforce_e2e = true; + rsc.rx_enforce_seq = true; + rsc.rx_seq_safestate_enable = true; + rsc.rx_ovrflw_safestate_enable = true; + rsc.rx_safety_measure = RxSafetyMeasure::RunSafeSequencer; + rsc.rx_safestate_sequencer = 2; + rsc.rx_safe_sequencer_state = 7; + + REQUIRE(rsc.rx_wd_timeout_interval == 500); + REQUIRE(rsc.rx_wd_enable); + REQUIRE(rsc.rx_wd_safestate_enable); + REQUIRE(rsc.rx_wd_info_enable); + REQUIRE(rsc.rx_enforce_e2e); + REQUIRE(rsc.rx_enforce_seq); + REQUIRE(rsc.rx_seq_safestate_enable); + REQUIRE(rsc.rx_ovrflw_safestate_enable); + REQUIRE(rsc.rx_safety_measure == RxSafetyMeasure::RunSafeSequencer); + REQUIRE(rsc.rx_safestate_sequencer == 2); + REQUIRE(rsc.rx_safe_sequencer_state == 7); +} + +TEST_CASE("RequestStreamConfig watchdog/safe-state fields default to disabled/ForceHighImpedance", + "[regmap][REQ-REGMAP-009]") { + RequestStreamConfig rsc; + REQUIRE_FALSE(rsc.rx_wd_enable); + REQUIRE_FALSE(rsc.rx_wd_safestate_enable); + REQUIRE_FALSE(rsc.rx_wd_info_enable); + REQUIRE_FALSE(rsc.rx_enforce_e2e); + REQUIRE_FALSE(rsc.rx_enforce_seq); + REQUIRE_FALSE(rsc.rx_seq_safestate_enable); + REQUIRE_FALSE(rsc.rx_ovrflw_safestate_enable); + REQUIRE(rsc.rx_safety_measure == RxSafetyMeasure::ForceHighImpedance); +} + +// ── EndpointGenericConfig: per-endpoint E2E CRC safe-mode toggles (v2.6.0) ─────── + +TEST_CASE("EndpointGenericConfig's ep_*_crc_enable toggles default false and are independently settable", + "[regmap][REQ-REGMAP-015]") { + EndpointGenericConfig cfg; + REQUIRE_FALSE(cfg.ep_req_crc_enable); + REQUIRE_FALSE(cfg.ep_ack_crc_enable); + REQUIRE_FALSE(cfg.ep_response_crc_enable); + + cfg.ep_ack_crc_enable = true; + REQUIRE_FALSE(cfg.ep_req_crc_enable); + REQUIRE(cfg.ep_ack_crc_enable); + REQUIRE_FALSE(cfg.ep_response_crc_enable); } // ── EP-ID / byte_bus_id mapping table ──────────────────────────────────────────── diff --git a/tests/test_sequencer.cpp b/tests/test_sequencer.cpp index 07400bd..6e43071 100644 --- a/tests/test_sequencer.cpp +++ b/tests/test_sequencer.cpp @@ -7,6 +7,9 @@ // fusa:test REQ-SEQ-007 // fusa:test REQ-SEQ-008 // fusa:test REQ-SEQ-009 +// fusa:test REQ-SEQ-010 +// fusa:test REQ-SEQ-011 +// fusa:test REQ-SEQ-012 // Tests for rcp/sequencer.hpp — conditional-request taxonomy and // sequencer-state primitives (ROADMAP.md milestone 49, @@ -89,6 +92,10 @@ TEST_CASE("category_of maps every opcode to its documented category", "[sequence REQUIRE(category_of(RequestTypeOpcode::Compound) == RequestCategory::Compound); REQUIRE(category_of(RequestTypeOpcode::CompoundWait) == RequestCategory::CompoundWait); REQUIRE(category_of(RequestTypeOpcode::Chained) == RequestCategory::Chained); + // Safety-tagged (0x8x) variants share their base opcode's category. + REQUIRE(category_of(RequestTypeOpcode::CompoundSafety) == RequestCategory::Compound); + REQUIRE(category_of(RequestTypeOpcode::CompoundWaitSafety) == RequestCategory::CompoundWait); + REQUIRE(category_of(RequestTypeOpcode::TriggeredSafety) == RequestCategory::Triggered); } TEST_CASE("priority_rank orders categories cancellation..standard", "[sequencer][REQ-SEQ-002]") { @@ -551,3 +558,76 @@ TEST_CASE("every SequencerErrc value has a distinct, non-empty message", "[seque for (size_t j = i + 1; j < messages.size(); ++j) REQUIRE(messages[i] != messages[j]); } + +// ── Safety-tagged (0x8x) request variants (ROADMAP.md milestone 50, v2.6.0) ── + +TEST_CASE("decode_request_type accepts the three 0x8x safety-tagged opcodes", "[sequencer][REQ-SEQ-010]") { + std::array params{1, 2, 3, 4, 5, 6, 7}; + for (auto opcode : {RequestTypeOpcode::CompoundSafety, RequestTypeOpcode::CompoundWaitSafety, + RequestTypeOpcode::TriggeredSafety}) { + const uint64_t ts = encode_request_type(opcode, params); + RequestTypeOpcode type{}; + std::array out_params{}; + auto ec = decode_request_type(/*mtv=*/false, ts, type, out_params); + REQUIRE_FALSE(ec); + REQUIRE(type == opcode); + REQUIRE(out_params == params); + } +} + +TEST_CASE("decode_request_type rejects an MSB-set byte that is not one of the three defined " + "safety opcodes", + "[sequencer][REQ-SEQ-010]") { + RequestTypeOpcode type{}; + std::array out_params{}; + // 0x80 has the MSB set but is not CompoundWaitSafety/TriggeredSafety/CompoundSafety. + auto ec = decode_request_type(/*mtv=*/false, uint64_t{0x80} << 56, type, out_params); + REQUIRE(ec == make_error_code(SequencerErrc::unknown_request_type)); +} + +TEST_CASE("is_safety_variant identifies exactly the three 0x8x opcodes", "[sequencer][REQ-SEQ-011]") { + REQUIRE(is_safety_variant(RequestTypeOpcode::CompoundSafety)); + REQUIRE(is_safety_variant(RequestTypeOpcode::CompoundWaitSafety)); + REQUIRE(is_safety_variant(RequestTypeOpcode::TriggeredSafety)); + + REQUIRE_FALSE(is_safety_variant(RequestTypeOpcode::Compound)); + REQUIRE_FALSE(is_safety_variant(RequestTypeOpcode::CompoundWait)); + REQUIRE_FALSE(is_safety_variant(RequestTypeOpcode::Triggered)); + REQUIRE_FALSE(is_safety_variant(RequestTypeOpcode::Chained)); + REQUIRE_FALSE(is_safety_variant(RequestTypeOpcode::ClearAll)); +} + +TEST_CASE("category_of maps each safety-tagged opcode to its base opcode's priority category", + "[sequencer][REQ-SEQ-011]") { + REQUIRE(category_of(RequestTypeOpcode::CompoundSafety) == category_of(RequestTypeOpcode::Compound)); + REQUIRE(category_of(RequestTypeOpcode::CompoundWaitSafety) == + category_of(RequestTypeOpcode::CompoundWait)); + REQUIRE(category_of(RequestTypeOpcode::TriggeredSafety) == category_of(RequestTypeOpcode::Triggered)); +} + +TEST_CASE("request_record_for derives is_safety from the opcode automatically", "[sequencer][REQ-SEQ-012]") { + auto normal = request_record_for(/*transaction_num=*/1, RequestTypeOpcode::Compound, /*cs=*/true); + REQUIRE_FALSE(normal.is_safety); + REQUIRE(normal.transaction_num == 1); + REQUIRE(normal.request_type == RequestTypeOpcode::Compound); + REQUIRE(normal.cs); + + auto safety = request_record_for(/*transaction_num=*/2, RequestTypeOpcode::CompoundSafety, /*cs=*/false); + REQUIRE(safety.is_safety); + + auto standard = request_record_for(/*transaction_num=*/3, std::nullopt, /*cs=*/false); + REQUIRE_FALSE(standard.is_safety); + REQUIRE_FALSE(standard.request_type.has_value()); +} + +TEST_CASE("request_record_for-built safety records survive cancel_all(non_safestate_only=true)", + "[sequencer][REQ-SEQ-012]") { + RequestLedger ledger; + REQUIRE_FALSE(ledger.submit(request_record_for(1, RequestTypeOpcode::Triggered, false))); + REQUIRE_FALSE(ledger.submit(request_record_for(2, RequestTypeOpcode::TriggeredSafety, false))); + + size_t canceled = ledger.cancel_all(/*non_safestate_only=*/true); + REQUIRE(canceled == 1); + REQUIRE(ledger.find(1)->state == RequestState::Canceled); + REQUIRE(ledger.find(2)->state == RequestState::Pending); +}