diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 817b2b5..87dbdc6 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -2767,6 +2767,166 @@ "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" + }, + { + "id": "REQ-LIFECYCLE-001", + "title": "ServerState byte encoding", + "text": "rcp::lifecycle::ServerState shall encode HwUnconfigured as 0x00, HwConfigured as 0x55, and RcpConfigured as 0xAA, and a freshly constructed ServerLifecycle shall report HwUnconfigured as its initial state.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-LIFECYCLE-002", + "title": "Forward-only, single-step state transitions", + "text": "ServerLifecycle::advance shall accept only a request for the single next state in the HwUnconfigured -> HwConfigured -> RcpConfigured sequence, and shall return LifecycleErrc::invalid_transition without changing state for any request to skip a state, move backward, or repeat the current state.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-LIFECYCLE-003", + "title": "HW_CFG_INCONSISTENT plausibility check", + "text": "ServerLifecycle::advance to HwConfigured shall invoke the configured HW plausibility check and return LifecycleErrc::hw_cfg_inconsistent without advancing state when that check returns false.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-LIFECYCLE-004", + "title": "RCP_CFG_INCONSISTENT plausibility check", + "text": "ServerLifecycle::advance to RcpConfigured shall invoke the configured RCP plausibility check and return LifecycleErrc::rcp_cfg_inconsistent without advancing state when that check returns false.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-LIFECYCLE-005", + "title": "Register-locking tied to lifecycle state", + "text": "ServerLifecycle::generic_config_locked shall report true from HwConfigured onward, and ServerLifecycle::functional_config_locked shall report true only at RcpConfigured, with both reporting false at HwUnconfigured.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-LIFECYCLE-006", + "title": "Explicit deconfigure() backward path", + "text": "ServerLifecycle::deconfigure shall reset state to HwUnconfigured and clear both generic_config_locked and functional_config_locked regardless of the state deconfigure was called from, after which advance shall again accept the normal forward sequence.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-REGMAP-001", + "title": "Generic vs. functional endpoint config split", + "text": "rcp::regmap shall represent an endpoint's server-owned generic config (EndpointGenericConfig: HW pin indices, request/response queue sizes) and its endpoint-type-specific functional config (EndpointFunctionalConfig) as distinct, independently settable types.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-REGMAP-002", + "title": "EP0 whole-register-map read is unrestricted", + "text": "Ep0::check_read_access shall return no error for any in-range target, including EP0 itself, regardless of which client is asking, and Ep0::read_whole_map shall return the current RegisterMap contents to any caller.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-REGMAP-003", + "title": "EP0 whole-register-map write requires the root client", + "text": "Ep0::write_whole_map shall return RegMapErrc::unauthorized_access and leave the RegisterMap unchanged when called by a client that is not the currently claimed root client (svr_root_client_index), and shall apply the write when called by the root client.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-REGMAP-004", + "title": "Root-client claim is exclusive", + "text": "Ep0::claim_root_client shall return RegMapErrc::request_rejected when a client distinct from the currently claimed root client attempts to claim the slot, shall treat the current root client re-claiming its own slot as a no-op success, and shall allow a new claim once Ep0::release_root_client has been called.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-REGMAP-005", + "title": "Per-endpoint write restriction for non-root clients", + "text": "Ep0::check_write_access (and the write_generic_config/write_functional_config calls built on it) shall permit a non-root client to write only the endpoint config it owns per Ep0::set_endpoint_owner, shall return RegMapErrc::unauthorized_access for any other endpoint, shall always permit the root client, and shall return RegMapErrc::invalid_parameter for an out-of-range endpoint id.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-REGMAP-006", + "title": "Config-block locks are enforced independently by EP0", + "text": "Ep0::write_generic_config shall return RegMapErrc::locked_mem_access once ServerLifecycle::generic_config_locked is true, and Ep0::write_functional_config shall return RegMapErrc::locked_mem_access once ServerLifecycle::functional_config_locked is true, even when called by an otherwise-authorized owning or root client.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-REGMAP-007", + "title": "General bootstrap register-map fields", + "text": "RegisterMap shall hold magic, protocol_version_major/minor, vendor_id, device_id, endpoint_count, max_streams, max_queue_depth, svr_implemented_options, and a TablePointer (offset+capacity) for each of the HW pin-map, request-stream, response-stream, EP-ID mapping, and functional-config tables, all independently settable and retrievable.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-REGMAP-008", + "title": "HW pin-mapping config storage", + "text": "RegisterMap::hw_pin_map shall store an ordered list of HwPinMapEntry (pin_id, function) values and return them unchanged.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "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.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-REGMAP-010", + "title": "EP-ID/byte_bus_id mapping preserves client insertion order", + "text": "RegisterMap::ep_id_mapping shall preserve EpIdMappingEntry values in the exact order a client inserted them, without the server re-deriving, validating, or re-sorting an ordering of its own.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-REGMAP-011", + "title": "Response/ack queue config fields", + "text": "ResponseQueueConfig shall provide independently settable response_queue_size and ack_queue_size fields.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-REGMAP-012", + "title": "Sequencer-state registers persist independent 8-bit values", + "text": "RegisterMap::sequencer_states shall store one persistent 8-bit SequencerState value per configured slot, each independently settable and retrievable, ahead of the sequencing behavior introduced in v2.5.0.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-REGMAP-013", + "title": "INVALID_PARAMETER on out-of-range register-map requests", + "text": "Ep0::check_read_access, Ep0::check_write_access, and Ep0::set_endpoint_owner shall return RegMapErrc::invalid_parameter for a target endpoint id outside the configured endpoint_count range, or (for set_endpoint_owner) for a request to assign an owner to EP0 itself.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-REGMAP-014", + "title": "The four mandatory register-map error codes are distinct", + "text": "rcp::regmap::RegMapErrc shall define UNAUTHORIZED_ACCESS, LOCKED_MEM_ACCESS, REQUEST_REJECTED, and INVALID_PARAMETER as pairwise-distinct error codes in their own error_category, each with a non-empty diagnostic message.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" } ] } diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f52d5a..7b9d1ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.21) project(cpp-rcp - VERSION 2.0.0 + VERSION 2.1.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 c3fe5ee..ee70859 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -200,6 +200,35 @@ incrementally through v2.6.0 per the Phase 13 introduction above. ### 45. RC Server Lifecycle & Register-Map Model (v2.1.0) +**Done (v2.1.0):** `rcp/lifecycle.hpp` implements the 3-state +`ServerState` machine (`HW_UNCONFIGURED`/`HW_CONFIGURED`/`RCP_CONFIGURED`) +via `ServerLifecycle`, with forward-only single-step `advance()` +transitions, the `HW_CFG_INCONSISTENT`/`RCP_CFG_INCONSISTENT` plausibility +checks gating each transition, an explicit `deconfigure()` backward path, +and independent generic/functional config-block locking queries. +`rcp/regmap.hpp` implements the register-map data model — the +generic/functional endpoint config split (`EndpointGenericConfig` / +`EndpointFunctionalConfig`), the general bootstrap fields (magic, +protocol version, vendor/device ID, endpoint count, stream/queue capacity, +`svr_implemented_options`, and the five table pointer/capacity fields), HW +pin-map config, request-stream config (including the inert `rx_wd_*`/ +`rx_safety_measure` fields deferred to v2.6.0), the EP-ID/`byte_bus_id` +mapping table (with its client-ordering risk flagged in comments, +deliberately not server-enforced), response/ack queue config, and +persistent 8-bit sequencer-state storage — plus `Ep0`, the RC Server +pseudo-endpoint implementing whole-register-map read (unrestricted) and +write (root-client-only via `claim_root_client`/`svr_root_client_index`), +and per-endpoint write restriction for every other client. The four +mandatory error codes (`UNAUTHORIZED_ACCESS`, `LOCKED_MEM_ACCESS`, +`REQUEST_REJECTED`, `INVALID_PARAMETER`) are `rcp::regmap::RegMapErrc`. New +coverage lives in `tests/test_lifecycle.cpp` (REQ-LIFECYCLE-001..006) and +`tests/test_regmap.cpp` (REQ-REGMAP-001..014). `rcp/rcp.hpp`'s +pre-replacement Zone/Command/Controller/Registry model is left in place, +unmodified in behavior — its header comment now points at this milestone's +replacement headers — since roughly three dozen other headers still build +against it and are not rebound until their own later milestones (v2.9.0 +onward); see the header comment in `rcp.hpp` and the disposition table. + - Implement the 3-state lifecycle machine: `HW_UNCONFIGURED` (`0x00`), `HW_CONFIGURED` (`0x55`), `RCP_CONFIGURED` (`0xAA`), with their documented transition guards, register-locking behavior, and the diff --git a/include/rcp/lifecycle.hpp b/include/rcp/lifecycle.hpp new file mode 100644 index 0000000..f633fa1 --- /dev/null +++ b/include/rcp/lifecycle.hpp @@ -0,0 +1,175 @@ +// fusa:req REQ-LIFECYCLE-001 +// fusa:req REQ-LIFECYCLE-002 +// fusa:req REQ-LIFECYCLE-003 +// fusa:req REQ-LIFECYCLE-004 +// fusa:req REQ-LIFECYCLE-005 +// fusa:req REQ-LIFECYCLE-006 + +// RC Server lifecycle state machine — the 3-state HW_UNCONFIGURED / +// HW_CONFIGURED / RCP_CONFIGURED progression an OPEN Alliance TC18 Remote +// Control Protocol Specification v0.5.1_RC server advances through as it +// gets configured, plus the plausibility checks and register-locking +// behavior tied to that progression (extraction §3.2). +// +// ROADMAP.md milestone 45, "RC Server Lifecycle & Register-Map Model +// (v2.1.0)": this header, together with rcp/regmap.hpp, begins the +// stream/endpoint/register-map model that supersedes rcp/rcp.hpp's +// pre-replacement Zone/Command/Controller/Registry model per the +// Satellite Package Disposition table. rcp.hpp itself is left in place +// for now — roughly three dozen other headers still build against its +// Controller/Registry types and are not rebound until their own later +// milestones (v2.9.0 onward per the Release Plan), so removing it here +// would break the whole tree for no benefit to this milestone's scope. +// +// 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 transition +// rules and locking policy chosen in this file are this implementation's +// own encoding of that behavior — full bit-for-bit register-map +// conformance against other TC18 implementations is a later-milestone +// concern, same as rcp/wire.hpp's disclaimer for the wire codec. +#pragma once + +#include +#include +#include +#include + +namespace rcp { +namespace lifecycle { + +// ── ServerState ─────────────────────────────────────────────────────────────── +// The three states an RC Server progresses through, in order, as hardware +// and then protocol-level configuration is applied. Values match the +// extraction's own numeric encoding (extraction §3.2) so they can be read +// or written directly as a register byte. + +enum class ServerState : uint8_t { + HwUnconfigured = 0x00, // reset/default state; no configuration accepted yet + HwConfigured = 0x55, // HW pin-map / generic endpoint config has been accepted + RcpConfigured = 0xAA, // functional config has been accepted; server is fully live +}; + +inline std::string to_string(ServerState s) { + switch (s) { + case ServerState::HwUnconfigured: return "HW_UNCONFIGURED"; + case ServerState::HwConfigured: return "HW_CONFIGURED"; + case ServerState::RcpConfigured: return "RCP_CONFIGURED"; + default: return "unknown"; + } +} + +// ── Errors ──────────────────────────────────────────────────────────────────── + +enum class LifecycleErrc : int { + invalid_transition = 1, // advance() target is not the single next state in sequence + hw_cfg_inconsistent = 2, // HW_UNCONFIGURED -> HW_CONFIGURED plausibility check failed + rcp_cfg_inconsistent = 3, // HW_CONFIGURED -> RCP_CONFIGURED plausibility check failed +}; + +inline const std::error_category& lifecycle_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.lifecycle"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case LifecycleErrc::invalid_transition: + return "rcp/lifecycle: requested state is not the next state in sequence"; + case LifecycleErrc::hw_cfg_inconsistent: + return "rcp/lifecycle: HW_CFG_INCONSISTENT — hardware configuration failed its plausibility check"; + case LifecycleErrc::rcp_cfg_inconsistent: + return "rcp/lifecycle: RCP_CFG_INCONSISTENT — protocol configuration failed its plausibility check"; + default: + return "rcp/lifecycle: unknown error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(LifecycleErrc e) noexcept { + return {static_cast(e), lifecycle_category()}; +} + +// ── ServerLifecycle ─────────────────────────────────────────────────────────── +// PlausibilityCheck lets the embedding application supply its own notion of +// "is the configuration applied so far internally consistent" without this +// header needing to know concrete register layouts — the register-map types +// that would typically back such a check live in rcp/regmap.hpp, one layer +// above this one. +using PlausibilityCheck = std::function; + +// ServerLifecycle owns the current ServerState and enforces: +// - forward-only, single-step transitions via advance() (extraction §3.2: +// a state-advance request always targets the next state in sequence; +// this implementation treats a request for any other state, including a +// repeat of the current one, as an invalid transition rather than a +// silent no-op, since a client asking to "advance" into its own current +// state most likely indicates a client-side bug worth surfacing); +// - the HW_CFG_INCONSISTENT / RCP_CFG_INCONSISTENT plausibility checks +// that gate each forward transition; +// - register-locking: generic (server-owned, pin-mapping/queue-size) +// config is locked from HW_CONFIGURED onward, and functional config is +// additionally locked once RCP_CONFIGURED — modeling the general +// principle that configuration a later stage depends on should not +// change out from under it (this locking policy is this +// implementation's own design choice for how to realize "register- +// locking behavior", not a verbatim rule copied from the specification). +class ServerLifecycle { +public: + explicit ServerLifecycle(PlausibilityCheck hw_cfg_check = {}, + PlausibilityCheck rcp_cfg_check = {}) noexcept + : hw_cfg_check_(std::move(hw_cfg_check)), + rcp_cfg_check_(std::move(rcp_cfg_check)) {} + + ServerState state() const noexcept { return state_; } + + // advance requests a transition to `target`. Only the single next state + // in the HwUnconfigured -> HwConfigured -> RcpConfigured sequence is + // accepted; anything else (skipping a state, repeating the current one, + // or moving backward) returns invalid_transition. Going backward is only + // possible through the explicit deconfigure() call below. + std::error_code advance(ServerState target) noexcept { + if (state_ == ServerState::HwUnconfigured && target == ServerState::HwConfigured) { + if (hw_cfg_check_ && !hw_cfg_check_()) + return make_error_code(LifecycleErrc::hw_cfg_inconsistent); + state_ = ServerState::HwConfigured; + return {}; + } + if (state_ == ServerState::HwConfigured && target == ServerState::RcpConfigured) { + if (rcp_cfg_check_ && !rcp_cfg_check_()) + return make_error_code(LifecycleErrc::rcp_cfg_inconsistent); + state_ = ServerState::RcpConfigured; + return {}; + } + return make_error_code(LifecycleErrc::invalid_transition); + } + + // deconfigure is the one sanctioned backward path: an explicit reset to + // HW_UNCONFIGURED (e.g. on a hardware reset or an operator-triggered + // reconfiguration), unlocking every register block again. + void deconfigure() noexcept { state_ = ServerState::HwUnconfigured; } + + // Register-locking queries (extraction §3.2's locking behavior, this + // implementation's own realization of it — see class comment above). + bool generic_config_locked() const noexcept { + return state_ == ServerState::HwConfigured || state_ == ServerState::RcpConfigured; + } + bool functional_config_locked() const noexcept { + return state_ == ServerState::RcpConfigured; + } + +private: + ServerState state_ = ServerState::HwUnconfigured; + PlausibilityCheck hw_cfg_check_; + PlausibilityCheck rcp_cfg_check_; +}; + +} // namespace lifecycle +} // namespace rcp + +// Enable std::error_code construction from rcp::lifecycle::LifecycleErrc. +namespace std { +template <> +struct is_error_code_enum : true_type {}; +} // namespace std diff --git a/include/rcp/rcp.hpp b/include/rcp/rcp.hpp index 1b9214b..160d6d2 100644 --- a/include/rcp/rcp.hpp +++ b/include/rcp/rcp.hpp @@ -51,6 +51,20 @@ // // RELAY conformance: include for relay:: namespace types, and // for Adapt() which wraps a Controller as a relay::Caller. +// +// Pre-replacement model notice: this Zone/Command/Controller/Registry +// design predates cpp-RCP's alignment to the OPEN Alliance TC18 Remote +// Control Protocol Specification v0.5.1_RC (see ROADMAP.md's Satellite +// Package Disposition table, which marks this file REPLACE). It remains in +// place, unmodified in behavior, purely because roughly three dozen other +// headers under include/rcp/ still build against it, and their own rebind +// milestones (v2.9.0 onward per the Release Plan) haven't landed yet — see +// ROADMAP.md milestone 45 ("RC Server Lifecycle & Register-Map Model, +// v2.1.0") for why this file could not simply be deleted at that milestone. +// The new stream/endpoint/register-map model that supersedes the types +// below lives in rcp/wire.hpp (v2.0.0), rcp/lifecycle.hpp, and +// rcp/regmap.hpp (both v2.1.0); do not build new functionality on top of +// Zone/Command/Controller/Registry going forward. #pragma once #include diff --git a/include/rcp/regmap.hpp b/include/rcp/regmap.hpp new file mode 100644 index 0000000..205183c --- /dev/null +++ b/include/rcp/regmap.hpp @@ -0,0 +1,369 @@ +// fusa:req REQ-REGMAP-001 +// fusa:req REQ-REGMAP-002 +// fusa:req REQ-REGMAP-003 +// fusa:req REQ-REGMAP-004 +// fusa:req REQ-REGMAP-005 +// fusa:req REQ-REGMAP-006 +// fusa:req REQ-REGMAP-007 +// fusa:req REQ-REGMAP-008 +// fusa:req REQ-REGMAP-009 +// fusa:req REQ-REGMAP-010 +// fusa:req REQ-REGMAP-011 +// fusa:req REQ-REGMAP-012 +// fusa:req REQ-REGMAP-013 +// fusa:req REQ-REGMAP-014 + +// RC Server register-map data model and EP0 pseudo-endpoint — the +// whole-device configuration surface an OPEN Alliance TC18 Remote Control +// Protocol Specification v0.5.1_RC server exposes once it exists, addressed +// the same way any other endpoint's config is (extraction §3.6-§3.11, +// §4.1-§4.2, §5.1). +// +// ROADMAP.md milestone 45, "RC Server Lifecycle & Register-Map Model +// (v2.1.0)": this header defines the register-map fields and tables needed +// to bootstrap discovery (v2.2.0) and every endpoint milestone from v2.3.0 +// onward, plus EP0 itself — the RC Server acting as a pseudo-endpoint with +// whole-map read/write and root-client arbitration. It rides on top of +// rcp/wire.hpp's StreamId/ByteBusId addressing (v2.0.0) and rcp/lifecycle.hpp's +// state machine (also v2.1.0); it does not depend on rcp/rcp.hpp's +// pre-replacement Zone/Command/Controller/Registry model at all. +// +// 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. Concrete field widths, the +// magic-number value, and table layout are this implementation's own +// encoding of that behavior for milestone 45 — full bit-for-bit register-map +// conformance against other TC18 implementations is not claimed, same as +// rcp/wire.hpp's equivalent disclaimer for the wire codec. Endpoint *types* +// (GPIO, SPI, I2C, ...) and their functional config contents are out of +// 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. +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace rcp { +namespace regmap { + +// ── EndpointId ──────────────────────────────────────────────────────────────── +// Identifies one configured endpoint slot in the register map. EP0 (below) +// is the reserved id for the RC Server's own pseudo-endpoint; real endpoints +// (GPIO, SPI, ...) are assigned ids starting at 1 by later milestones. + +using EndpointId = uint16_t; +constexpr EndpointId kEp0 = 0; + +// ── Errors ──────────────────────────────────────────────────────────────────── +// The four mandatory error codes needed once register access exists +// (extraction §3.15). + +enum class RegMapErrc : int { + unauthorized_access = 1, // client is neither the root client nor this endpoint's owner + locked_mem_access = 2, // target config block is locked by the current lifecycle state + request_rejected = 3, // request is well-formed but the server declines it (e.g. root already claimed) + invalid_parameter = 4, // request references an out-of-range endpoint/table entry or bad value +}; + +inline const std::error_category& regmap_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.regmap"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case RegMapErrc::unauthorized_access: return "rcp/regmap: UNAUTHORIZED_ACCESS"; + case RegMapErrc::locked_mem_access: return "rcp/regmap: LOCKED_MEM_ACCESS"; + case RegMapErrc::request_rejected: return "rcp/regmap: REQUEST_REJECTED"; + case RegMapErrc::invalid_parameter: return "rcp/regmap: INVALID_PARAMETER"; + default: return "rcp/regmap: unknown error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(RegMapErrc e) noexcept { + return {static_cast(e), regmap_category()}; +} + +// ── Generic vs. functional endpoint config split ────────────────────────────── +// New relative to the pre-replacement design (extraction §4.2 vs. §4.4/§5.x): +// every configured endpoint has two independent config blocks. The generic +// block is server-owned and endpoint-type-agnostic (which HW pins it claims, +// how deep its request/response queues are); the functional block is +// endpoint-type-specific and is left as an opaque byte blob here since no +// endpoint type is implemented until v2.3.0 onward. Every later endpoint +// milestone depends on this split already existing (extraction §6 item 5). + +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; +}; + +struct EndpointFunctionalConfig { + std::vector data; // endpoint-type-specific; interpreted starting at v2.3.0 +}; + +// ── HW pin-mapping config (extraction §3.7) ────────────────────────────────── + +struct HwPinMapEntry { + uint16_t pin_id = 0; // physical pin identifier; numbering is implementation-defined + uint8_t function = 0; // pin function/mode selector; meaning is endpoint-type-defined +}; + +// ── 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. + +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 +}; + +// ── EP-ID / byte_bus_id mapping table (extraction §3.9) ────────────────────── +// Risk, flagged explicitly per the roadmap: the *order* in which a client +// populates this table is client-guaranteed, not something this server +// implementation re-derives or verifies on its own — a client that writes +// entries out of the order it intends them to be interpreted in will not be +// corrected here. Callers that need order-independence must encode an +// explicit ordering key inside EpIdMappingEntry themselves; this milestone +// does not add one, since no endpoint type exists yet to define what +// "correct order" would even mean. + +struct EpIdMappingEntry { + EndpointId ep_id = 0; + wire::ByteBusId byte_bus_id = 0; +}; + +// ── Response / ack queue config (extraction §3.10) ─────────────────────────── + +struct ResponseQueueConfig { + uint16_t response_queue_size = 0; + 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 +// milestones; a server MUST NOT set a bit for a feature it does not yet +// implement. + +constexpr uint32_t kOptConditionalRequests = 0x0000'0001; // compound/triggered/timed/chained requests (v2.5.0) +constexpr uint32_t kOptSafetyRequests = 0x0000'0002; // E2E CRC safe points + safety-request variants (v2.6.0) +constexpr uint32_t kOptFragmentation = 0x0000'0004; // multi-segment requests (v2.8.0, go/no-go pending) + +// ── TablePointer ────────────────────────────────────────────────────────────── +// The pointer/capacity pair pattern used for each of the five bootstrap +// tables below (extraction §3.6). `offset` is this implementation's own +// choice of addressing unit (byte offset into the register map); `capacity` +// is the number of entries the table can hold, not necessarily how many are +// populated. + +struct TablePointer { + uint32_t offset = 0; + uint16_t capacity = 0; +}; + +// ── RegisterMap ─────────────────────────────────────────────────────────────── +// The general register-map fields needed to bootstrap everything else +// (extraction §3.6), plus the in-memory contents of the tables those +// pointer/capacity pairs describe. A real server would serialize this to +// and from the wire on demand; this milestone models the data, not the wire +// encoding of the whole map (individual field reads/writes ride on the +// wire::ByteBusId-addressed standard request already implemented in +// rcp/wire.hpp). + +constexpr uint32_t kRegisterMapMagic = 0x52435030; // "RCP0" — this implementation's own placeholder value + +struct RegisterMap { + // General / bootstrap fields. + uint32_t magic = kRegisterMapMagic; + uint8_t protocol_version_major = 0; + uint8_t protocol_version_minor = 0; + uint16_t vendor_id = 0; + uint16_t device_id = 0; + uint16_t endpoint_count = 0; + uint16_t max_streams = 0; + uint16_t max_queue_depth = 0; + uint32_t svr_implemented_options = 0; + + // Pointer/capacity fields for the five bootstrap tables. + TablePointer hw_pin_map_table{}; + TablePointer request_stream_table{}; + TablePointer response_stream_table{}; + TablePointer ep_id_mapping_table{}; + TablePointer functional_config_table{}; + + // Table contents. + std::vector hw_pin_map; + std::vector request_streams; + std::vector response_streams; + std::vector ep_id_mapping; + std::vector functional_configs; + + // Per-endpoint generic config, indexed in parallel with functional_configs + // (index i is EndpointId i+1 — EP0 is not stored here, see Ep0 below). + std::vector generic_configs; + + // Persistent sequencer-state storage (extraction §3.11, §3.16). + std::vector sequencer_states; +}; + +// ── EP0 — RC Server as a pseudo-endpoint ───────────────────────────────────── +// extraction §5.1, §4.1: the RC Server itself is addressable like any other +// endpoint (as EP0) for whole-register-map reads and writes. Every client may +// read the whole map. Only one client — the root client, identified by +// svr_root_client_index — may write the whole map through EP0; every other +// client is restricted to writing the generic/functional config of the +// endpoint(s) it owns. +// +// "Client" here is identified by an opaque index the embedding transport +// assigns per connected stream; this header does not know or care how that +// index maps to a wire::StreamId, only that it is stable for the lifetime of +// the connection. +class Ep0 { +public: + // ConfigBlock distinguishes which of an endpoint's two config blocks a + // write targets, since the two lock at different lifecycle states (see + // ServerLifecycle::generic_config_locked/functional_config_locked). + enum class ConfigBlock { Generic, Functional }; + + Ep0(RegisterMap& regs, lifecycle::ServerLifecycle& lc) noexcept + : regs_(regs), lifecycle_(lc), endpoint_owner_(regs.endpoint_count) {} + + // claim_root_client implements the exclusive root-client rule: once a + // stream holds the root-client slot, a different stream's claim is + // rejected until release_root_client() is called (e.g. on disconnect). + // The same stream re-claiming its own slot is a harmless no-op. + std::error_code claim_root_client(size_t client) noexcept { + if (root_client_.has_value() && *root_client_ != client) + return make_error_code(RegMapErrc::request_rejected); + root_client_ = client; + return {}; + } + + void release_root_client() noexcept { root_client_.reset(); } + + bool is_root_client(size_t client) const noexcept { + return root_client_.has_value() && *root_client_ == client; + } + + std::optional root_client() const noexcept { return root_client_; } + + // set_endpoint_owner assigns which client's writes are accepted for a + // given non-EP0 endpoint's config (assigned during discovery/config in + // earlier real deployments; exposed directly here since discovery itself + // is a later milestone, v2.2.0). + std::error_code set_endpoint_owner(EndpointId ep, size_t client) noexcept { + if (ep == kEp0 || ep > endpoint_owner_.size()) + return make_error_code(RegMapErrc::invalid_parameter); + endpoint_owner_[static_cast(ep - 1)] = client; + return {}; + } + + // check_read_access: reads of the whole map (EP0) or of any single + // endpoint's config are unrestricted — every connected client may read + // (extraction §5.1's "whole-register-map read" is granted to any + // client, not just root). + std::error_code check_read_access(EndpointId target) const noexcept { + if (target != kEp0 && target > endpoint_owner_.size()) + return make_error_code(RegMapErrc::invalid_parameter); + return {}; + } + + // check_write_access enforces: EP0 (whole-map) writes require the root + // client; a non-EP0 endpoint's write requires either the root client or + // that endpoint's assigned owner; and a write to whichever config block + // is targeted is rejected while the current lifecycle state has that + // specific block locked, regardless of who is asking. + std::error_code check_write_access(size_t client, EndpointId target, + ConfigBlock block = ConfigBlock::Generic) const noexcept { + if (target == kEp0) { + return is_root_client(client) ? std::error_code{} + : make_error_code(RegMapErrc::unauthorized_access); + } + if (target > endpoint_owner_.size()) + return make_error_code(RegMapErrc::invalid_parameter); + const auto& owner = endpoint_owner_[static_cast(target - 1)]; + if (!is_root_client(client) && (!owner.has_value() || *owner != client)) + return make_error_code(RegMapErrc::unauthorized_access); + const bool locked = (block == ConfigBlock::Generic) + ? lifecycle_.generic_config_locked() + : lifecycle_.functional_config_locked(); + if (locked) { + return make_error_code(RegMapErrc::locked_mem_access); + } + return {}; + } + + // read_whole_map is the unrestricted-read half of EP0's whole-register- + // map read/write behavior (extraction §5.1) — any connected client may + // call this. + const RegisterMap& read_whole_map() const noexcept { return regs_; } + + // write_whole_map is the root-client-only half of the same rule. + std::error_code write_whole_map(size_t client, RegisterMap new_map) noexcept { + if (!is_root_client(client)) + return make_error_code(RegMapErrc::unauthorized_access); + regs_ = std::move(new_map); + return {}; + } + + // write_generic_config / write_functional_config apply the per-endpoint + // write-access and per-block lock checks above before mutating the + // targeted endpoint's config block in the underlying RegisterMap. + std::error_code write_generic_config(size_t client, EndpointId target, + EndpointGenericConfig cfg) noexcept { + if (target == kEp0) return make_error_code(RegMapErrc::invalid_parameter); + auto ec = check_write_access(client, target, ConfigBlock::Generic); + if (ec) return ec; + regs_.generic_configs[static_cast(target - 1)] = std::move(cfg); + return {}; + } + + std::error_code write_functional_config(size_t client, EndpointId target, + EndpointFunctionalConfig cfg) noexcept { + if (target == kEp0) return make_error_code(RegMapErrc::invalid_parameter); + auto ec = check_write_access(client, target, ConfigBlock::Functional); + if (ec) return ec; + regs_.functional_configs[static_cast(target - 1)] = std::move(cfg); + return {}; + } + +private: + RegisterMap& regs_; + lifecycle::ServerLifecycle& lifecycle_; + std::optional root_client_; + std::vector> endpoint_owner_; +}; + +} // namespace regmap +} // namespace rcp + +// Enable std::error_code construction from rcp::regmap::RegMapErrc. +namespace std { +template <> +struct is_error_code_enum : true_type {}; +} // namespace std diff --git a/include/rcp/version.hpp b/include/rcp/version.hpp index 424bb41..c7ccc18 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.0.0"; +constexpr std::string_view kVersion = "2.1.0"; } // namespace rcp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8994a7e..61cf275 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,6 +14,14 @@ add_executable(test_legacy_wire test_legacy_wire.cpp) target_link_libraries(test_legacy_wire PRIVATE rcp::rcp Catch2::Catch2WithMain) add_test(NAME rcp_legacy_wire COMMAND test_legacy_wire) +add_executable(test_lifecycle test_lifecycle.cpp) +target_link_libraries(test_lifecycle PRIVATE rcp::rcp Catch2::Catch2WithMain) +add_test(NAME rcp_lifecycle COMMAND test_lifecycle) + +add_executable(test_regmap test_regmap.cpp) +target_link_libraries(test_regmap PRIVATE rcp::rcp Catch2::Catch2WithMain) +add_test(NAME rcp_regmap COMMAND test_regmap) + add_executable(test_e2e test_e2e.cpp) target_link_libraries(test_e2e PRIVATE rcp::rcp Catch2::Catch2WithMain) add_test(NAME rcp_e2e COMMAND test_e2e) diff --git a/tests/test_lifecycle.cpp b/tests/test_lifecycle.cpp new file mode 100644 index 0000000..15e2858 --- /dev/null +++ b/tests/test_lifecycle.cpp @@ -0,0 +1,132 @@ +// fusa:test REQ-LIFECYCLE-001 +// fusa:test REQ-LIFECYCLE-002 +// fusa:test REQ-LIFECYCLE-003 +// fusa:test REQ-LIFECYCLE-004 +// fusa:test REQ-LIFECYCLE-005 +// fusa:test REQ-LIFECYCLE-006 + +// Tests for rcp/lifecycle.hpp — the RC Server 3-state lifecycle machine +// (ROADMAP.md milestone 45, "RC Server Lifecycle & Register-Map Model", +// v2.1.0). + +#include +#include + +using namespace rcp::lifecycle; + +// ── State encoding ───────────────────────────────────────────────────────────── + +TEST_CASE("ServerState values match the documented byte encoding", "[lifecycle][REQ-LIFECYCLE-001]") { + REQUIRE(static_cast(ServerState::HwUnconfigured) == 0x00); + REQUIRE(static_cast(ServerState::HwConfigured) == 0x55); + REQUIRE(static_cast(ServerState::RcpConfigured) == 0xAA); +} + +TEST_CASE("A fresh ServerLifecycle starts HW_UNCONFIGURED", "[lifecycle][REQ-LIFECYCLE-001]") { + ServerLifecycle lc; + REQUIRE(lc.state() == ServerState::HwUnconfigured); +} + +// ── Forward transition guard ──────────────────────────────────────────────────── + +TEST_CASE("advance() walks HW_UNCONFIGURED -> HW_CONFIGURED -> RCP_CONFIGURED", "[lifecycle][REQ-LIFECYCLE-002]") { + ServerLifecycle lc; + REQUIRE_FALSE(lc.advance(ServerState::HwConfigured)); + REQUIRE(lc.state() == ServerState::HwConfigured); + REQUIRE_FALSE(lc.advance(ServerState::RcpConfigured)); + REQUIRE(lc.state() == ServerState::RcpConfigured); +} + +TEST_CASE("advance() rejects skipping HW_CONFIGURED entirely", "[lifecycle][REQ-LIFECYCLE-002]") { + ServerLifecycle lc; + auto ec = lc.advance(ServerState::RcpConfigured); + REQUIRE(ec); + REQUIRE(ec == make_error_code(LifecycleErrc::invalid_transition)); + REQUIRE(lc.state() == ServerState::HwUnconfigured); +} + +TEST_CASE("advance() rejects moving backward", "[lifecycle][REQ-LIFECYCLE-002]") { + ServerLifecycle lc; + REQUIRE_FALSE(lc.advance(ServerState::HwConfigured)); + auto ec = lc.advance(ServerState::HwUnconfigured); + REQUIRE(ec); + REQUIRE(ec == make_error_code(LifecycleErrc::invalid_transition)); +} + +TEST_CASE("advance() rejects re-requesting the current state", "[lifecycle][REQ-LIFECYCLE-002]") { + ServerLifecycle lc; + auto ec = lc.advance(ServerState::HwUnconfigured); + REQUIRE(ec); + REQUIRE(ec == make_error_code(LifecycleErrc::invalid_transition)); +} + +// ── HW_CFG_INCONSISTENT plausibility check ────────────────────────────────────── + +TEST_CASE("A failing HW config plausibility check yields HW_CFG_INCONSISTENT and blocks the transition", + "[lifecycle][REQ-LIFECYCLE-003]") { + ServerLifecycle lc(/*hw_cfg_check=*/[] { return false; }); + auto ec = lc.advance(ServerState::HwConfigured); + REQUIRE(ec); + REQUIRE(ec == make_error_code(LifecycleErrc::hw_cfg_inconsistent)); + REQUIRE(lc.state() == ServerState::HwUnconfigured); +} + +TEST_CASE("A passing HW config plausibility check allows the transition", "[lifecycle][REQ-LIFECYCLE-003]") { + ServerLifecycle lc(/*hw_cfg_check=*/[] { return true; }); + REQUIRE_FALSE(lc.advance(ServerState::HwConfigured)); + REQUIRE(lc.state() == ServerState::HwConfigured); +} + +// ── RCP_CFG_INCONSISTENT plausibility check ───────────────────────────────────── + +TEST_CASE("A failing RCP config plausibility check yields RCP_CFG_INCONSISTENT and blocks the transition", + "[lifecycle][REQ-LIFECYCLE-004]") { + ServerLifecycle lc(/*hw_cfg_check=*/{}, /*rcp_cfg_check=*/[] { return false; }); + REQUIRE_FALSE(lc.advance(ServerState::HwConfigured)); + auto ec = lc.advance(ServerState::RcpConfigured); + REQUIRE(ec); + REQUIRE(ec == make_error_code(LifecycleErrc::rcp_cfg_inconsistent)); + REQUIRE(lc.state() == ServerState::HwConfigured); +} + +TEST_CASE("A passing RCP config plausibility check allows the transition", "[lifecycle][REQ-LIFECYCLE-004]") { + ServerLifecycle lc(/*hw_cfg_check=*/{}, /*rcp_cfg_check=*/[] { return true; }); + REQUIRE_FALSE(lc.advance(ServerState::HwConfigured)); + REQUIRE_FALSE(lc.advance(ServerState::RcpConfigured)); + REQUIRE(lc.state() == ServerState::RcpConfigured); +} + +// ── Register-locking behavior ─────────────────────────────────────────────────── + +TEST_CASE("Generic config locks from HW_CONFIGURED onward; functional config locks only at RCP_CONFIGURED", + "[lifecycle][REQ-LIFECYCLE-005]") { + ServerLifecycle lc; + REQUIRE_FALSE(lc.generic_config_locked()); + REQUIRE_FALSE(lc.functional_config_locked()); + + REQUIRE_FALSE(lc.advance(ServerState::HwConfigured)); + REQUIRE(lc.generic_config_locked()); + REQUIRE_FALSE(lc.functional_config_locked()); + + REQUIRE_FALSE(lc.advance(ServerState::RcpConfigured)); + REQUIRE(lc.generic_config_locked()); + REQUIRE(lc.functional_config_locked()); +} + +// ── deconfigure() explicit backward path ──────────────────────────────────────── + +TEST_CASE("deconfigure() resets to HW_UNCONFIGURED and unlocks both config blocks", + "[lifecycle][REQ-LIFECYCLE-006]") { + ServerLifecycle lc; + REQUIRE_FALSE(lc.advance(ServerState::HwConfigured)); + REQUIRE_FALSE(lc.advance(ServerState::RcpConfigured)); + + lc.deconfigure(); + REQUIRE(lc.state() == ServerState::HwUnconfigured); + REQUIRE_FALSE(lc.generic_config_locked()); + REQUIRE_FALSE(lc.functional_config_locked()); + + // And the state machine can walk forward again from scratch. + REQUIRE_FALSE(lc.advance(ServerState::HwConfigured)); + REQUIRE(lc.state() == ServerState::HwConfigured); +} diff --git a/tests/test_regmap.cpp b/tests/test_regmap.cpp new file mode 100644 index 0000000..3f88776 --- /dev/null +++ b/tests/test_regmap.cpp @@ -0,0 +1,327 @@ +// fusa:test REQ-REGMAP-001 +// fusa:test REQ-REGMAP-002 +// fusa:test REQ-REGMAP-003 +// fusa:test REQ-REGMAP-004 +// fusa:test REQ-REGMAP-005 +// fusa:test REQ-REGMAP-006 +// fusa:test REQ-REGMAP-007 +// fusa:test REQ-REGMAP-008 +// fusa:test REQ-REGMAP-009 +// fusa:test REQ-REGMAP-010 +// fusa:test REQ-REGMAP-011 +// fusa:test REQ-REGMAP-012 +// fusa:test REQ-REGMAP-013 +// fusa:test REQ-REGMAP-014 + +// Tests for rcp/regmap.hpp — the RC Server register-map data model and EP0 +// pseudo-endpoint (ROADMAP.md milestone 45, "RC Server Lifecycle & +// Register-Map Model", v2.1.0). + +#include +#include + +using namespace rcp::regmap; +using rcp::lifecycle::ServerLifecycle; +using rcp::lifecycle::ServerState; + +namespace { + +// make_map builds a RegisterMap with `n` endpoints and pre-sized per-endpoint +// tables, as a real EP0 owner would before handing it to Ep0. +RegisterMap make_map(uint16_t n) { + RegisterMap m; + m.endpoint_count = n; + m.generic_configs.resize(n); + m.functional_configs.resize(n); + return m; +} + +} // namespace + +// ── Generic vs. functional config split ───────────────────────────────────────── + +TEST_CASE("EndpointGenericConfig and EndpointFunctionalConfig are distinct, independently settable types", + "[regmap][REQ-REGMAP-001]") { + EndpointGenericConfig generic; + generic.hw_pin_indices = {0, 1, 2}; + generic.request_queue_size = 4; + generic.response_queue_size = 4; + + EndpointFunctionalConfig functional; + functional.data = {0xAA, 0xBB}; + + REQUIRE(generic.hw_pin_indices.size() == 3); + REQUIRE(functional.data.size() == 2); +} + +// ── EP0 whole-map read ─────────────────────────────────────────────────────────── + +TEST_CASE("Any client may read the whole register map through EP0", "[regmap][REQ-REGMAP-002]") { + auto map = make_map(2); + map.vendor_id = 0x1234; + ServerLifecycle lc; + Ep0 ep0(map, lc); + + REQUIRE_FALSE(ep0.check_read_access(kEp0)); + REQUIRE_FALSE(ep0.check_read_access(1)); + REQUIRE(ep0.read_whole_map().vendor_id == 0x1234); +} + +// ── EP0 whole-map write is root-client-only ───────────────────────────────────── + +TEST_CASE("Only the root client may write the whole register map through EP0", "[regmap][REQ-REGMAP-003]") { + auto map = make_map(1); + ServerLifecycle lc; + Ep0 ep0(map, lc); + + RegisterMap replacement = make_map(1); + replacement.vendor_id = 0x99; + + // No root client claimed yet: even the would-be client is unauthorized. + auto ec = ep0.write_whole_map(/*client=*/1, replacement); + REQUIRE(ec); + REQUIRE(ec == make_error_code(RegMapErrc::unauthorized_access)); + + REQUIRE_FALSE(ep0.claim_root_client(1)); + REQUIRE_FALSE(ep0.write_whole_map(1, replacement)); + REQUIRE(ep0.read_whole_map().vendor_id == 0x99); + + // A different client is still refused. + auto ec2 = ep0.write_whole_map(/*client=*/2, replacement); + REQUIRE(ec2); + REQUIRE(ec2 == make_error_code(RegMapErrc::unauthorized_access)); +} + +// ── Root-client claim is exclusive ────────────────────────────────────────────── + +TEST_CASE("A second, distinct client cannot claim the root-client slot while it is held", + "[regmap][REQ-REGMAP-004]") { + auto map = make_map(1); + ServerLifecycle lc; + Ep0 ep0(map, lc); + + REQUIRE_FALSE(ep0.claim_root_client(1)); + auto ec = ep0.claim_root_client(2); + REQUIRE(ec); + REQUIRE(ec == make_error_code(RegMapErrc::request_rejected)); + REQUIRE(ep0.is_root_client(1)); + REQUIRE_FALSE(ep0.is_root_client(2)); + + // The same client re-claiming its own slot is a harmless no-op. + REQUIRE_FALSE(ep0.claim_root_client(1)); + + // Once released, a different client may claim it. + ep0.release_root_client(); + REQUIRE_FALSE(ep0.claim_root_client(2)); + REQUIRE(ep0.is_root_client(2)); +} + +// ── Per-endpoint write restriction ────────────────────────────────────────────── + +TEST_CASE("A non-root client may only write the endpoint config it owns", "[regmap][REQ-REGMAP-005]") { + auto map = make_map(2); + ServerLifecycle lc; + Ep0 ep0(map, lc); + REQUIRE_FALSE(ep0.set_endpoint_owner(1, /*client=*/10)); + REQUIRE_FALSE(ep0.set_endpoint_owner(2, /*client=*/20)); + + EndpointGenericConfig cfg; + cfg.request_queue_size = 8; + + REQUIRE_FALSE(ep0.write_generic_config(/*client=*/10, /*target=*/1, cfg)); + REQUIRE(ep0.read_whole_map().generic_configs[0].request_queue_size == 8); + + auto ec = ep0.write_generic_config(/*client=*/10, /*target=*/2, cfg); + REQUIRE(ec); + REQUIRE(ec == make_error_code(RegMapErrc::unauthorized_access)); + + // The root client, once claimed, may write any endpoint's config. + REQUIRE_FALSE(ep0.claim_root_client(/*client=*/99)); + REQUIRE_FALSE(ep0.write_generic_config(/*client=*/99, /*target=*/2, cfg)); + REQUIRE(ep0.read_whole_map().generic_configs[1].request_queue_size == 8); +} + +TEST_CASE("Writing to an out-of-range endpoint id is rejected as INVALID_PARAMETER", "[regmap][REQ-REGMAP-005]") { + auto map = make_map(1); + ServerLifecycle lc; + Ep0 ep0(map, lc); + REQUIRE_FALSE(ep0.claim_root_client(1)); + + EndpointFunctionalConfig cfg; + auto ec = ep0.write_functional_config(1, /*target=*/5, cfg); + REQUIRE(ec); + REQUIRE(ec == make_error_code(RegMapErrc::invalid_parameter)); +} + +// ── Register-locking interacts with EP0 write access ──────────────────────────── + +TEST_CASE("Generic config writes are refused once the lifecycle locks the generic block", + "[regmap][REQ-REGMAP-006]") { + auto map = make_map(1); + ServerLifecycle lc; + Ep0 ep0(map, lc); + REQUIRE_FALSE(ep0.set_endpoint_owner(1, 10)); + REQUIRE_FALSE(lc.advance(ServerState::HwConfigured)); + + EndpointGenericConfig cfg; + auto ec = ep0.write_generic_config(10, 1, cfg); + REQUIRE(ec); + REQUIRE(ec == make_error_code(RegMapErrc::locked_mem_access)); +} + +TEST_CASE("Functional config remains writable at HW_CONFIGURED but locks at RCP_CONFIGURED", + "[regmap][REQ-REGMAP-006]") { + auto map = make_map(1); + ServerLifecycle lc; + Ep0 ep0(map, lc); + REQUIRE_FALSE(ep0.set_endpoint_owner(1, 10)); + REQUIRE_FALSE(lc.advance(ServerState::HwConfigured)); + + EndpointFunctionalConfig cfg; + cfg.data = {0x01}; + REQUIRE_FALSE(ep0.write_functional_config(10, 1, cfg)); + + REQUIRE_FALSE(lc.advance(ServerState::RcpConfigured)); + auto ec = ep0.write_functional_config(10, 1, cfg); + REQUIRE(ec); + REQUIRE(ec == make_error_code(RegMapErrc::locked_mem_access)); +} + +// ── General bootstrap register fields ─────────────────────────────────────────── + +TEST_CASE("RegisterMap bootstrap fields hold the values assigned to them", "[regmap][REQ-REGMAP-007]") { + RegisterMap m; + m.magic = kRegisterMapMagic; + m.protocol_version_major = 1; + m.protocol_version_minor = 0; + m.vendor_id = 0x00AB; + m.device_id = 0x00CD; + m.endpoint_count = 4; + m.max_streams = 8; + m.max_queue_depth = 16; + m.svr_implemented_options = kOptConditionalRequests | kOptSafetyRequests; + + m.hw_pin_map_table = {0x100, 32}; + m.request_stream_table = {0x200, 8}; + m.response_stream_table = {0x280, 8}; + m.ep_id_mapping_table = {0x300, 4}; + m.functional_config_table = {0x400, 4}; + + REQUIRE(m.magic == kRegisterMapMagic); + REQUIRE(m.vendor_id == 0x00AB); + REQUIRE(m.device_id == 0x00CD); + REQUIRE(m.endpoint_count == 4); + REQUIRE((m.svr_implemented_options & kOptConditionalRequests) != 0); + REQUIRE((m.svr_implemented_options & kOptFragmentation) == 0); + REQUIRE(m.hw_pin_map_table.capacity == 32); + REQUIRE(m.functional_config_table.offset == 0x400); +} + +// ── HW pin-mapping config ─────────────────────────────────────────────────────── + +TEST_CASE("HW pin-map table entries are stored and retrievable", "[regmap][REQ-REGMAP-008]") { + RegisterMap m; + m.hw_pin_map.push_back({/*pin_id=*/3, /*function=*/1}); + m.hw_pin_map.push_back({/*pin_id=*/4, /*function=*/2}); + + REQUIRE(m.hw_pin_map.size() == 2); + REQUIRE(m.hw_pin_map[0].pin_id == 3); + REQUIRE(m.hw_pin_map[1].function == 2); +} + +// ── Request-stream config, including inert rx_* fields ────────────────────────── + +TEST_CASE("RequestStreamConfig carries the rx_* fields needed later for watchdog/safe-state", + "[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); +} + +// ── EP-ID / byte_bus_id mapping table ──────────────────────────────────────────── + +TEST_CASE("EP-ID mapping table preserves client insertion order without re-sorting it", + "[regmap][REQ-REGMAP-010]") { + RegisterMap m; + m.ep_id_mapping.push_back({/*ep_id=*/3, /*byte_bus_id=*/9}); + m.ep_id_mapping.push_back({/*ep_id=*/1, /*byte_bus_id=*/7}); + m.ep_id_mapping.push_back({/*ep_id=*/2, /*byte_bus_id=*/8}); + + // The table is exactly what the client wrote, in the order it wrote it — + // this server implementation does not re-derive or verify an ordering. + REQUIRE(m.ep_id_mapping[0].ep_id == 3); + REQUIRE(m.ep_id_mapping[1].ep_id == 1); + REQUIRE(m.ep_id_mapping[2].ep_id == 2); +} + +// ── Response / ack queue config ────────────────────────────────────────────────── + +TEST_CASE("ResponseQueueConfig fields exist and are settable", "[regmap][REQ-REGMAP-011]") { + ResponseQueueConfig rqc; + rqc.response_queue_size = 6; + rqc.ack_queue_size = 6; + REQUIRE(rqc.response_queue_size == 6); + REQUIRE(rqc.ack_queue_size == 6); +} + +// ── Sequencer-state persistence ────────────────────────────────────────────────── + +TEST_CASE("Sequencer-state registers persist independent 8-bit values", "[regmap][REQ-REGMAP-012]") { + RegisterMap m; + m.sequencer_states = {0, 0, 0}; + m.sequencer_states[1] = 42; + + REQUIRE(m.sequencer_states[0] == 0); + REQUIRE(m.sequencer_states[1] == 42); + REQUIRE(m.sequencer_states[2] == 0); +} + +// ── INVALID_PARAMETER ───────────────────────────────────────────────────────────── + +TEST_CASE("An out-of-range read target is rejected as INVALID_PARAMETER", "[regmap][REQ-REGMAP-013]") { + auto map = make_map(1); + ServerLifecycle lc; + Ep0 ep0(map, lc); + + auto ec = ep0.check_read_access(/*target=*/7); + REQUIRE(ec); + REQUIRE(ec == make_error_code(RegMapErrc::invalid_parameter)); +} + +TEST_CASE("Assigning an owner to EP0 itself is rejected as INVALID_PARAMETER", "[regmap][REQ-REGMAP-013]") { + auto map = make_map(1); + ServerLifecycle lc; + Ep0 ep0(map, lc); + + auto ec = ep0.set_endpoint_owner(kEp0, 1); + REQUIRE(ec); + REQUIRE(ec == make_error_code(RegMapErrc::invalid_parameter)); +} + +// ── The four mandatory error codes are distinct ───────────────────────────────── + +TEST_CASE("The four mandatory register-map error codes are distinct values in their own category", + "[regmap][REQ-REGMAP-014]") { + auto unauthorized = make_error_code(RegMapErrc::unauthorized_access); + auto locked = make_error_code(RegMapErrc::locked_mem_access); + auto rejected = make_error_code(RegMapErrc::request_rejected); + auto invalid = make_error_code(RegMapErrc::invalid_parameter); + + REQUIRE(unauthorized.category() == regmap_category()); + REQUIRE(unauthorized != locked); + REQUIRE(locked != rejected); + REQUIRE(rejected != invalid); + REQUIRE(invalid != unauthorized); + + // Every code has a non-empty, distinguishing message. + REQUIRE_FALSE(unauthorized.message().empty()); + REQUIRE_FALSE(locked.message().empty()); + REQUIRE_FALSE(rejected.message().empty()); + REQUIRE_FALSE(invalid.message().empty()); +}