diff --git a/include/rcp/mock.hpp b/include/rcp/mock.hpp index 986eb8c..aa46122 100644 --- a/include/rcp/mock.hpp +++ b/include/rcp/mock.hpp @@ -440,7 +440,7 @@ class Server final { static regmap::RegisterMap make_initial_register_map() { regmap::RegisterMap regs; - regs.endpoint_count = 10; + regs.general.svr_ep_count = 10; regs.generic_configs.resize(10); regs.functional_configs.resize(10); regs.ep_id_mapping = { @@ -471,7 +471,7 @@ class Server final { auto ec = ep0_.check_read_access(regmap::kEp0); if (ec) return set_error_response(req, ec, out_resp, out_resp_payload); out_resp_payload.resize(kEp0PartialReadLen); - avtp::detail::put_u32(out_resp_payload.data(), regs_.magic); + avtp::detail::put_u32(out_resp_payload.data(), regs_.general.magic); out_resp = acf::make_response(req, acf::ResponseKind::ReadResponse); return {}; } diff --git a/include/rcp/regmap.hpp b/include/rcp/regmap.hpp index 7403add..a84336f 100644 --- a/include/rcp/regmap.hpp +++ b/include/rcp/regmap.hpp @@ -13,6 +13,46 @@ // fusa:req REQ-REGMAP-013 // fusa:req REQ-REGMAP-014 // fusa:req REQ-REGMAP-015 +// +// c-RCP-derived content ported in this batch (Phase 17 / cpp-RCP issue #129, +// "Phase 4 batch A" — see this file's own "Phase 4 batch A" banner below): +// fusa:req REQ-RMAP-001 +// fusa:req REQ-RMAP-003 +// fusa:req REQ-RMAP-009 +// fusa:req REQ-RMAP-010 +// fusa:req REQ-RMAP-011 +// fusa:req REQ-RMAP-012 +// fusa:req REQ-RMAP-016 +// fusa:req REQ-RMAP-023 +// fusa:req REQ-RMAP-024 +// fusa:req REQ-RMAP-025 +// fusa:req REQ-RMAP-026 +// fusa:req REQ-RMAP-027 +// fusa:req REQ-RMAP-028 +// fusa:req REQ-RMAP-029 +// fusa:req REQ-RMAP-030 +// fusa:req REQ-RMAP-031 +// fusa:req REQ-RMAP-032 +// fusa:req REQ-RMAP-033 +// fusa:req REQ-RMAP-034 +// fusa:req REQ-RMAP-035 +// fusa:req REQ-RMAP-036 +// fusa:req REQ-RMAP-037 +// fusa:req REQ-RMAP-038 +// fusa:req REQ-RMAP-039 +// fusa:req REQ-RMAP-066 +// fusa:req REQ-RMAP-067 +// fusa:req REQ-RMAP-070 +// fusa:req REQ-RMAP-073 +// fusa:req REQ-RMAP-074 +// fusa:req REQ-RMAP-075 +// fusa:req REQ-RMAP-076 +// fusa:req REQ-RMAP-077 +// fusa:req REQ-RMAP-078 +// fusa:req REQ-RMAP-079 +// fusa:req REQ-RMAP-081 +// fusa:req REQ-RMAP-086 +// fusa:req REQ-RMAP-087 // RC Server register-map data model and EP0 pseudo-endpoint — the // whole-device configuration surface an OPEN Alliance TC18 Remote Control @@ -29,37 +69,65 @@ // 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/avtp.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. +// ── Phase 4 batch A rewrite (cpp-RCP issue #129, ROADMAP.md "Phase 17") ───── +// c-RCP (this project's RC5-conformant reference implementation) models +// roughly 7x more of TC18's actual register-map content than this header's +// pre-rewrite v2.x design did (ROADMAP.md's own Phase 17 rationale). This +// pass re-derives, from c-RCP's CURRENT src/regmap.c/include/rcp/regmap.h, +// exactly the subset ROADMAP.md Phase 4 batch A scopes: EP0/root-client +// concepts, the sub-table pointer/capacity pattern, svr_implemented_options +// (REQ-RMAP-030), the general register map (GeneralMap, c-RCP's +// rcp_regmap_general_t) with its Table 20 wire codec, the root-client/ +// per-EP-restricted-client writer_ctx() derivation, the RC Server's own +// functional-configuration content (SvrEpCfg, TC18 §13.7.1.2), and the +// generic-vs-functional per-endpoint config split's *generic* half +// (EndpointGenericConfig, c-RCP's rcp_regmap_ep_generic_cfg_t) including its +// wire codec and the ep_delay_time/ep_req_storage_size boundary conversions. +// +// Batch B (a separate, later PR) ports the rest: HW pin mapping, the +// named-signal index, request-stream-cfg, response-queue-cfg, the EP-ID/ +// byte_bus_id map, and the optional-subsystem (Network/PHY/time-synch/ +// security) config sections. Every type below already in this file's scope +// for THOSE sections — HwPinMapEntry, RequestStreamConfig, +// ResponseQueueConfig, EpIdMappingEntry, SequencerState, RxSafetyMeasure, +// EndpointFunctionalConfig (the opaque-blob placeholder), and RegisterMap's +// own hw_pin_map_table/request_stream_table/response_stream_table/ +// ep_id_mapping_table/functional_config_table pointer/capacity pairs — is +// deliberately left exactly as this codebase's own pre-rewrite v2.x design +// already had it: those sections are batch B's job, not this pass's, and +// e2e.hpp/watchdog.hpp/deadline.hpp/sim.hpp/request.hpp/respqueue.hpp/ +// gpio.hpp (and their own tests) already depend on their current shape. // -// 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/request.hpp, not -// here — same config-vs-behavior split as everything else in this header. +// A genuine cross-cutting collision, found and resolved deliberately rather +// than silently: c-RCP's own rcp_regmap_ep_generic_cfg_t has NO per-role E2E +// CRC-enable fields at all (that content, in c-RCP, belongs to the +// *functional* config's ep_req_crc_enable, ONE field, not per-role) — but +// this codebase's own pre-rewrite EndpointGenericConfig already carries +// THREE (ep_req_crc_enable/ep_ack_crc_enable/ep_response_crc_enable), read +// by rcp/e2e.hpp's crc_required() and exercised by tests/test_e2e.cpp. Since +// rcp_regmap_ep_functional_cfg_t itself is NOT in this batch's scope (see +// the exclusion list above — it isn't named in ROADMAP.md Phase 4 batch A +// either) relocating those three fields to a functional-config struct that +// does not exist yet in this header would both be out of scope and break +// e2e.hpp today. They are kept on EndpointGenericConfig, unchanged, clearly +// marked below as this codebase's own pre-existing placement rather than +// c-RCP content — a documented deferral, not a silent inconsistency. // -// ROADMAP.md milestone 54, "Watchdog & Liveness Rebuild (v2.10.0)": -// ResponseQueueConfig gains flush_time — a small, explicitly-scoped -// addition rcp/deadline.hpp's rebuilt liveness monitor needs as a -// heartbeat-cadence signal, in place of the Status-subscription concept -// that does not exist in this model. Behavior lives in rcp/deadline.hpp, -// same split as the watchdog fields above. +// Field names and behavior below implement TC18's *behavior* as described +// in an internal structured extraction of the specification named above (or, +// where explicitly cited via a REQ-RMAP-* tag, directly re-derived from +// c-RCP's own primary-source-verified field-by-field Table 20 citations); no +// text from that document is reproduced here. Full bit-for-bit register-map +// conformance against other TC18 implementations is not claimed — same +// disclaimer as rcp/avtp.hpp's/rcp/acf.hpp's own wire codecs. #pragma once +#include #include #include +#include +#include #include #include #include @@ -69,14 +137,23 @@ namespace rcp { namespace regmap { -// ── EndpointId ──────────────────────────────────────────────────────────────── +// ── EndpointId / EP0 ────────────────────────────────────────────────────────── // 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. +// +// EP0 is deliberately the same numeric value c-RCP's own +// RCP_LIFECYCLE_DISCOVERY_BYTE_BUS_ID uses (see discovery.hpp's own +// kDiscoveryByteBusId) — discovery and the general register map are both +// reached through the same address (c-RCP regmap.h's own file header). using EndpointId = uint16_t; constexpr EndpointId kEp0 = 0; +// is_ep0 — REQ-RMAP-001, ported from c-RCP's rcp_regmap_is_ep0() +// (src/regmap.c). True iff ep_index is EP0. +constexpr bool is_ep0(EndpointId ep_index) noexcept { return ep_index == kEp0; } + // ── Errors ──────────────────────────────────────────────────────────────────── // The four mandatory error codes needed once register access exists // (extraction §3.15). @@ -109,26 +186,126 @@ inline std::error_code make_error_code(RegMapErrc e) noexcept { return {static_cast(e), regmap_category()}; } +// GeneralMapErrc — REQ-RMAP-024, ported from c-RCP's rcp_regmap_general_errc_t +// (include/rcp/regmap.h). Errors decoding a Table 20 general-register-map +// wire message (see GeneralMap's own "Table 20 wire codec" section below). +// This project's convention (Errc + std::error_category::message(), not a +// separate strerror() function) replaces c-RCP's rcp_regmap_general_strerror(); +// the four-value error taxonomy itself is preserved unchanged. +enum class GeneralMapErrc : int { + short_frame = 1, // b/len shorter than the ACF_ABB fixed header or its declared payload length + bad_msg_type = 2, // b is not an ACF_ABB message + wrong_bus = 3, // byte_bus_id is not EP0 + wrong_op = 4, // op does not match the direction this function expects +}; + +inline const std::error_category& general_map_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.regmap.general"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case GeneralMapErrc::short_frame: return "rcp/regmap: frame too short"; + case GeneralMapErrc::bad_msg_type: return "rcp/regmap: unexpected ACF message type"; + case GeneralMapErrc::wrong_bus: return "rcp/regmap: wrong byte_bus_id"; + case GeneralMapErrc::wrong_op: return "rcp/regmap: wrong ACF op"; + default: return "rcp/regmap: unknown error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(GeneralMapErrc e) noexcept { + return {static_cast(e), general_map_category()}; +} + +// EpGenericCfgReconfigErrc — REQ-RMAP-079, ported from c-RCP's +// rcp_regmap_ep_generic_cfg_reconfig_errc_t (include/rcp/regmap.h). Errors +// applying an incoming write to an EndpointGenericConfig row (see +// ep_generic_cfg::apply_reconfig() below). +enum class EpGenericCfgReconfigErrc : int { + short_write = 1, // data_len == 0 + out_of_range = 2, // relative_start_address + data_len exceeds count * row length +}; + +inline const std::error_category& ep_generic_cfg_reconfig_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.regmap.ep_generic_cfg_reconfig"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case EpGenericCfgReconfigErrc::short_write: + return "rcp/regmap: ep_generic_cfg write has no data"; + case EpGenericCfgReconfigErrc::out_of_range: + return "rcp/regmap: ep_generic_cfg write extends past the table's own current extent"; + default: + return "rcp/regmap: ep_generic_cfg unknown configuration-write error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(EpGenericCfgReconfigErrc e) noexcept { + return {static_cast(e), ep_generic_cfg_reconfig_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). +// Every configured endpoint has two independent config blocks, deliberately +// not merged into one blob: the generic block is server-owned and +// endpoint-type-agnostic; the functional block is endpoint-type-specific. +// c-RCP's own file header (regmap.h) frames this the same way: "every +// concrete endpoint type ... composes rcp_regmap_ep_functional_cfg_t as its +// own first member", the generic block staying server-owned regardless. +// EndpointGenericConfig — REQ-RMAP-016, ported from c-RCP's +// rcp_regmap_ep_generic_cfg_t (include/rcp/regmap.h:1310-1418, +// src/regmap.c). Server-owned generic per-endpoint config: fields a +// client's functional configuration is never allowed to touch, however +// write-access to the functional block evolves as the server's lifecycle +// state changes (see Ep0::check_write_access below). +// +// Zero-initialization (every field 0/false) is this port's equivalent of +// c-RCP's rcp_regmap_ep_generic_cfg_init() — a separate init() free function +// is not needed here since C++ default member initializers already give +// every instance that same zeroed starting state. 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. + uint8_t ep_type = 0; // concrete meaning assigned by each endpoint type added in Phase 16/19; TC18 §13.2 Table 28/31 relative address 0x0000, 8 bit, R (read-only — see ep_generic_cfg::apply_reconfig() below) + bool ep_used = false; // TC18 §13.2 Table 28/31 relative address 0x0001 bit 0, R/W* — EP0's own row (index 0) is fixed to true; see apply_reconfig()'s own doc comment + + // ep_delay_time — TC18 §13.2 Table 28/31 relative address 0x0001.4:5, 2 + // bit, R/W*, restricted to exactly {1, 10, 20, 50} microseconds. This + // field's own internal representation deliberately stays a free + // microsecond value rather than the register's own 2-bit enum, since it + // is consumed as a scheduling-tick unit by rcp/request.hpp, not by any + // caller that needs to know the register's own packed encoding — + // ep_delay_time_us_to_reg()/_reg_to_us() below are the boundary + // conversion pair a wire codec needs (c-RCP issue #311 batch 2). + uint32_t ep_delay_time = 0; + + // ep_req_storage_size — octets of request-payload storage reserved for + // this endpoint. TC18's own register (relative 0x0002, 16 bit, R/W*) is + // in 32-bit WORDS, not octets — this field stays octets internally (the + // register's own maximum representable value, 65535 words, is 262140 + // octets, which would not fit a uint16_t octet count); see + // ep_req_storage_size_words_to_octets()/_octets_to_words() below for the + // boundary conversion. + uint32_t ep_req_storage_size = 0; + + uint32_t ep_description = 0; // TC18 §13.2 Table 28/31 relative address 0x0004, 32 bit, R/W*: user-defined description, no further structure given by TC18 + uint16_t ep_tx_buffer_size = 0; // TC18 §13.2 Table 28/31 relative address 0x0008, 16 bit, R/W*, 32-bit words (matches ep_req_storage_size's own unit, not octets); 0x0000 if the endpoint has no tx buffer + uint16_t ep_rx_buffer_size = 0; // TC18 §13.2 Table 28/31 relative address 0x000A, 16 bit, R/W*, same shape as ep_tx_buffer_size for the endpoint's rx buffer + + // ── Pre-existing (pre-Phase-4), NOT c-RCP content ────────────────────── + // rcp/e2e.hpp's crc_required() and tests/test_e2e.cpp already depend on + // these three independently-settable per-role E2E CRC toggles + // (ROADMAP.md milestone 50, v2.6.0) — kept here unchanged rather than + // relocated, since c-RCP's own rcp_regmap_ep_functional_cfg_t (the + // struct these conceptually belong to in c-RCP, as a single + // ep_req_crc_enable field, not three per-role ones) is explicitly out + // of this batch's scope. See this file's own header comment for the + // full rationale. bool ep_req_crc_enable = false; bool ep_ack_crc_enable = false; bool ep_response_crc_enable = false; @@ -138,157 +315,761 @@ struct EndpointFunctionalConfig { std::vector data; // endpoint-type-specific; interpreted starting at v2.3.0 }; -// ── HW pin-mapping config (extraction §3.7) ────────────────────────────────── +// ── ep_generic_cfg boundary conversions, wire codec (c-RCP issue #311 +// batches 2-4; REQ-RMAP-076/077/078/079) ───────────────────────────────── +namespace ep_generic_cfg { + +// ep_delay_time_us_to_reg — REQ-RMAP-076. Converts a microsecond value into +// TC18's own 2-bit register encoding (00b=1us, 01b=10us, 10b=20us, +// 11b=50us — Table 28/31's own definitive register definition). Returns +// false (leaving out_reg unchanged) for any microsecond value NOT exactly +// one of the 4 allowed ones — deliberately REJECTS rather than rounds, +// since this is a real R/W* configuration input, not a saturating-is-safe +// case (a silently-substituted delay would misconfigure the endpoint's own +// scheduling timing). +// +// NOTE: TC18's own separate prose (request_compound/_triggered/_chained's +// own field descriptions) instead reads "[1us, 20us, 20us, 50us]" — a +// duplicated 20us where the table's own 10us belongs, almost certainly a +// copy-paste typo in TC18's own text (c-RCP regmap.h's own investigation); +// the table, not the repeated prose, is treated as authoritative here. +inline bool ep_delay_time_us_to_reg(uint32_t delay_us, uint8_t& out_reg) noexcept { + switch (delay_us) { + case 1u: out_reg = 0u; return true; + case 10u: out_reg = 1u; return true; + case 20u: out_reg = 2u; return true; + case 50u: out_reg = 3u; return true; + default: return false; + } +} + +// ep_delay_time_reg_to_us — REQ-RMAP-076. The inverse conversion. reg is +// masked to its own 2 bits internally, so any input is well-defined and +// this can never fail — all 4 possible 2-bit register values are valid +// TC18 encodings with no reserved/undefined combination. +inline uint32_t ep_delay_time_reg_to_us(uint8_t reg) noexcept { + static constexpr uint32_t kUsByReg[4] = {1u, 10u, 20u, 50u}; + return kUsByReg[reg & 0x3u]; +} + +// ep_req_storage_size_words_to_octets — REQ-RMAP-077. The read-side +// conversion: always exact, always fits uint32_t (max register value 65535 +// words = 262140 octets), cannot fail. +constexpr uint32_t ep_req_storage_size_words_to_octets(uint16_t words) noexcept { + return static_cast(words) * 4u; +} + +// ep_req_storage_size_octets_to_words — REQ-RMAP-077. The write-side +// inverse: octets -> words. Returns false (leaving out_words unchanged) if +// octets is not an exact multiple of 4 (no lossy rounding of a +// configuration input) or if the resulting word count would not fit the +// register's own 16-bit width. +inline bool ep_req_storage_size_octets_to_words(uint32_t octets, uint16_t& out_words) noexcept { + if ((octets % 4u) != 0u) return false; + const uint32_t words = octets / 4u; + if (words > static_cast(UINT16_MAX)) return false; + out_words = static_cast(words); + return true; +} + +// kRowLen — REQ-RMAP-078: each row's own 12-octet TC18-cited stride +// (relative address 12*N onward per endpoint N). +constexpr size_t kRowLen = 12; + +// kMaxEntries is not itself a TC18-derived value (TC18 defines no fixed +// endpoint count) — matches every sibling table's own identical, +// not-spec-derived bound (c-RCP's RCP_REGMAP_*_MAX_ENTRIES family). +constexpr size_t kMaxEntries = 64; + +// render — REQ-RMAP-078, ported from c-RCP's rcp_regmap_ep_generic_cfg_render() +// (src/regmap.c). Serializes entries[0..count) into out at each row's own +// 12-octet stride — out must have room for at least kRowLen*count octets. +// +// ep_type, ep_description, ep_tx_buffer_size, and ep_rx_buffer_size are +// serialized directly (already stored in their own wire-native unit or as a +// plain octet passthrough). ep_used (bit 0) and ep_delay_time (bits 4:5) are +// packed into the same octet (relative 0x0001); the reserved 3-bit/2-bit +// spans either side of ep_delay_time have no corresponding field and are +// left 0. +// +// DEFENSIVE FALLBACK (matches c-RCP exactly): if a row's internal +// ep_delay_time is not exactly one of TC18's 4 allowed values, this +// function does not fail or assert — it falls back to register value 0 +// (1us, the shortest/smallest valid delay). Every freshly-default- +// constructed, not-yet-configured EndpointGenericConfig hits this fallback +// (ep_delay_time defaults to 0us, itself not one of the 4 allowed values) +// until something explicitly sets a valid value — consistent with a +// "never grant more delay than configured" bias. Likewise, an +// ep_req_storage_size that is not an exact multiple of 4, or whose word +// count exceeds the register's own 16-bit width, is clamped DOWN to the +// nearest representable word count (never rounded up). +inline void render(const EndpointGenericConfig* entries, size_t count, uint8_t* out) noexcept { + for (size_t i = 0; i < count; ++i) { + uint8_t delay_reg = 0; + if (!ep_delay_time_us_to_reg(entries[i].ep_delay_time, delay_reg)) delay_reg = 0u; + + uint16_t req_storage_words = 0; + if (!ep_req_storage_size_octets_to_words(entries[i].ep_req_storage_size, req_storage_words)) { + uint32_t clamped = entries[i].ep_req_storage_size; + if (clamped > 0xFFFFu * 4u) clamped = 0xFFFFu * 4u; // max representable octets + req_storage_words = static_cast(clamped / 4u); // floor: rounds down, never up + } + + const uint8_t octet1 = static_cast((entries[i].ep_used ? 0x01u : 0x00u) | + static_cast((delay_reg & 0x3u) << 4)); + + uint8_t* row = out + kRowLen * i; + row[0] = entries[i].ep_type; + row[1] = octet1; + avtp::detail::put_u16(&row[2], req_storage_words); + avtp::detail::put_u32(&row[4], entries[i].ep_description); + avtp::detail::put_u16(&row[8], entries[i].ep_tx_buffer_size); + avtp::detail::put_u16(&row[10], entries[i].ep_rx_buffer_size); + } +} + +// apply_reconfig — REQ-RMAP-079/087, ported from c-RCP's +// rcp_regmap_ep_generic_cfg_apply_reconfig() (src/regmap.c). Applies an +// incoming write of data[0..data_len) at relative_start_address to +// entries[0..count) — entries[i] is row i's own 12-octet stride, matching +// render()'s own layout. +// +// NOT the render()-then-patch-then-reparse-the-whole-buffer idiom c-RCP's +// own sibling tables use for THEIR apply_reconfig() — that idiom is safe +// only because their own render() is a lossless 1:1 round-trip. render() +// above is NOT lossless (its own defensive ep_delay_time fallback and +// ep_req_storage_size clamp): reparsing a whole rendered-then-patched row +// would silently "launder" any already-invalid field through its own +// fallback/clamp on every write, even for a field the write never touched +// at all. This function is therefore PER-FIELD, not per-buffer: each of the +// 5 writable fields in a row is updated ONLY if the write's own byte span +// FULLY covers that field's own octet range within the row — a write that +// only partially covers a multi-octet field leaves that field entirely +// unchanged. +// +// ep_type (relative 0x0000 within each row) is NEVER updated, regardless of +// whether the write's own byte span covers it: TC18 §13.7.1.2 states, in +// general terms, that writing to read-only registers "has no effect and +// request is confirmed normally" — ep_type is R, not R/W*, the one +// read-only field mixed into this otherwise fully-writable row. A write +// touching only ep_type's own byte still returns success (not an error). +// +// ep_used (bit 0 of octet 0x0001) has its own narrower, row-0-only override +// (REQ-RMAP-087) on top of its otherwise general R/W* status: TC18 Table 31 +// states EP0's own bit is "fixed to 1 as EP0 needs to be always +// implemented". A write to row 0 that covers this bit never clears it — +// entries[0].ep_used is forced to true regardless of the incoming bit. +// Every other row honors the incoming bit normally; row 0's own +// ep_delay_time (bits 4:5 of the same octet) is unaffected by this override. +// +// Returns EpGenericCfgReconfigErrc::short_write if data_len is 0, or +// ::out_of_range if the write's own span exceeds count*kRowLen — entries is +// left entirely unchanged in either error case. +inline std::error_code apply_reconfig(EndpointGenericConfig* entries, size_t count, + uint16_t relative_start_address, + const uint8_t* data, size_t data_len) noexcept { + if (data_len == 0u) return make_error_code(EpGenericCfgReconfigErrc::short_write); + + const size_t touched_start = relative_start_address; + const size_t touched_end = touched_start + data_len; + if (touched_end > count * kRowLen) return make_error_code(EpGenericCfgReconfigErrc::out_of_range); + + const size_t row_start_idx = touched_start / kRowLen; + const size_t row_end_idx = (touched_end - 1u) / kRowLen; // inclusive + + for (size_t row_i = row_start_idx; row_i <= row_end_idx; ++row_i) { + const size_t row_base = row_i * kRowLen; + + if (touched_start <= row_base + 1u && row_base + 1u + 1u <= touched_end) { + const uint8_t octet1 = data[(row_base + 1u) - touched_start]; + + if (row_i == 0u) { + entries[row_i].ep_used = true; // forced -- TC18 Table 31: EP0's ep_used + // bit is "fixed to 1", never clearable + } else { + entries[row_i].ep_used = (octet1 & 0x01u) != 0u; + } + entries[row_i].ep_delay_time = + ep_delay_time_reg_to_us(static_cast((octet1 >> 4) & 0x3u)); + } + + if (touched_start <= row_base + 2u && row_base + 2u + 2u <= touched_end) { + const uint16_t words = avtp::detail::get_u16(&data[(row_base + 2u) - touched_start]); + entries[row_i].ep_req_storage_size = ep_req_storage_size_words_to_octets(words); + } + + if (touched_start <= row_base + 4u && row_base + 4u + 4u <= touched_end) { + entries[row_i].ep_description = avtp::detail::get_u32(&data[(row_base + 4u) - touched_start]); + } + + if (touched_start <= row_base + 8u && row_base + 8u + 2u <= touched_end) { + entries[row_i].ep_tx_buffer_size = avtp::detail::get_u16(&data[(row_base + 8u) - touched_start]); + } + + if (touched_start <= row_base + 10u && row_base + 10u + 2u <= touched_end) { + entries[row_i].ep_rx_buffer_size = avtp::detail::get_u16(&data[(row_base + 10u) - touched_start]); + } + } + + return {}; +} + +} // namespace ep_generic_cfg + +// ── HW pin-mapping config (extraction §3.7) — batch B ──────────────────────── +// Left exactly as this codebase's own pre-rewrite v2.x design already had +// it; c-RCP's own richer HW_config table (Table 21/22) is batch B's job. 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 }; -// ── Sequencer-state registers (extraction §3.11, §3.16) ────────────────────── +// ── Sequencer-state registers (extraction §3.11, §3.16) — batch B ──────────── // Persistent 8-bit values; behavior lives in rcp/request.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. +// ── rx_safety_measure selector (extraction §3.8) — batch B ─────────────────── 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) ────────────────────────────────── -// 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/request.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. +// ── Request-stream config (extraction §3.8) — batch B ──────────────────────── +// Left exactly as this codebase's own pre-rewrite v2.x design already had +// it; rcp/e2e.hpp, rcp/watchdog.hpp, rcp/sim.hpp, and their own tests +// already depend on this exact shape. c-RCP's own request-stream-cfg wire +// codec (rcp_regmap_request_stream_cfg_render()/_apply_reconfig(), and its +// own documented TC18 0.5.1_RC5 terminology-drift reconciliation) is batch +// B's job. struct RequestStreamConfig { avtp::StreamId stream_id{}; 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) ────────────────────── -// 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. +// ── EP-ID / byte_bus_id mapping table (extraction §3.9) — batch B ──────────── +// Left exactly as this codebase's own pre-rewrite v2.x design already had +// it. c-RCP's own rcp_regmap_ep_id_map_entry_t additionally carries a +// request_stream_index field this struct does not yet have — needed for a +// real rcp_regmap_ep_id_map_is_valid_association() (see writer_ctx() below, +// which fails closed until that lands) — batch B's job. struct EpIdMappingEntry { EndpointId ep_id = 0; avtp::ByteBusId byte_bus_id = 0; }; -// ── Response / ack queue config (extraction §3.10) ─────────────────────────── +// ── Response / ack queue config (extraction §3.10) — batch B ───────────────── struct ResponseQueueConfig { uint16_t response_queue_size = 0; uint16_t ack_queue_size = 0; - - // Periodic response/ack queue flush cadence, in milliseconds (extraction - // §3.10). ROADMAP.md milestone 54 (v2.10.0) reuses this field as a - // liveness heartbeat signal: rcp/deadline.hpp treats each flush as - // evidence the owning RC Server is alive, since there is no Status- - // subscription concept to poll in this model. This field's unit is this - // implementation's own choice, same as every other concrete-width - // decision in this header; behavior that reads and acts on it lives in - // rcp/deadline.hpp, not here — same config-vs-behavior split as - // RequestStreamConfig's watchdog fields above. uint32_t flush_time = 0; }; -// ── 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 — bit reserved, never set: ROADMAP.md milestone 52's already-decided fragmentation no-go (v2.8.0) +// ── svr_implemented_options bitmask ─────────────────────────────────────────── +// +// Legacy, pre-Phase-4 bitmask constants (uint32_t) — retained ONLY because +// rcp/request.hpp's implemented_options_bits()/timed_feature_enabled() and +// rcp/e2e.hpp's implemented_options_bit() still reference +// kOptConditionalRequests/kOptSafetyRequests, and neither header is in this +// batch's scope. c-RCP's own REQ-RMAP-004..008 investigation (regmap.c — +// "primary-source verification ... found this citation incorrect: §12.9.1.1 +// ... says nothing about svr_implemented_options, feature advertisement, or +// any bit-pairing rule") found that the all-or-nothing-pair grouping this +// design implies has no TC18 basis at all — "safety requests" and +// "fragmentation" bits in particular do not exist anywhere in the real +// Table 20 register (see the five REAL bits below, REQ-RMAP-030). Do not +// add new callers of these three legacy constants; new code should read/ +// write GeneralMap::svr_implemented_options directly against +// kOptCompoundWait/kOptTrigger/kOptChained/kOptTimeSync/kOptEnhCancel. +constexpr uint32_t kOptConditionalRequests = 0x0000'0001; +constexpr uint32_t kOptSafetyRequests = 0x0000'0002; +constexpr uint32_t kOptFragmentation = 0x0000'0004; -// ── 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. +// REQ-RMAP-030 (TC18 §12.7.5 Table 20, absolute address 0x0016, 8 bit, R): +// five independent bits, one per optional feature, "abcdefgh" with bits +// f/g/h reserved — verified directly against the primary-source PDF (c-RCP +// regmap.h/regmap.c, ported unchanged): +// a: compound & wait requests +// b: trigger requests +// c: chained requests +// d: time synch and timed requests +// e: enhanced request cancellation +// These are GeneralMap::svr_implemented_options's real bits (uint8_t, +// matching the register's own 8-bit width) — distinct from, and not +// interchangeable with, the legacy uint32_t constants above. +constexpr uint8_t kOptCompoundWait = 1u << 0; // a +constexpr uint8_t kOptTrigger = 1u << 1; // b +constexpr uint8_t kOptChained = 1u << 2; // c +constexpr uint8_t kOptTimeSync = 1u << 3; // d +constexpr uint8_t kOptEnhCancel = 1u << 4; // e +// ── Sub-table pointer/capacity pairs ────────────────────────────────────────── +// The pointer/capacity pair pattern c-RCP's own regmap.h calls +// rcp_regmap_table_ref_t. `offset` is this implementation's own choice of +// addressing unit; `capacity` is the number of entries the table can hold, +// not necessarily how many are populated. GeneralMap's own Table 20 fields +// (below) do NOT use this shared type for most sub-tables: c-RCP's own +// REQ-RMAP-033/034/036/038 investigation found TC18 defines each pointer/ +// capacity pair as two SEPARATE, independently-addressed, non-adjacent +// registers of their own specific widths (not a bundled offset+uint16_t +// capacity) — GeneralMap models each such pair as its own pair of scalar +// fields instead, matching TC18 exactly. TablePointer remains the right +// shape for the batch-B sub-tables below that still use it (this +// codebase's own pre-existing pointer/capacity fields for HW_config/ +// request-stream/response-stream/EP-ID-map/functional-config). 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 -// avtp::ByteBusId-addressed standard request already implemented in -// rcp/acf.hpp). +// ── The general register map (GeneralMap) ───────────────────────────────────── +// REQ-RMAP-003/023/026..039, ported from c-RCP's rcp_regmap_general_t +// (include/rcp/regmap.h:416-984, src/regmap.c). The one register block +// every RC Server exposes regardless of which endpoint types it implements. +// +// Zero-initialization plus svr_root_client_index defaulting to +// kNoRootClient is this port's equivalent of c-RCP's +// rcp_regmap_general_init() — no separate init() function is needed here +// since default member initializers already give every instance that same +// starting state (svr_lifecycle_state == 0 == HW_UNCONFIGURED, the correct +// default, matches every real server's own starting lifecycle::ServerState). + +constexpr uint32_t kRegisterMapMagic = 0x52435030; // "RCP0" -- this implementation's own placeholder value + +// kNoRootClient — sentinel for GeneralMap::svr_root_client_index meaning "no +// stream currently holds the root-client grant" — the natural state while +// HW_UNCONFIGURED, before any client has been promoted. Matches c-RCP's +// RCP_REGMAP_NO_ROOT_CLIENT. +constexpr uint16_t kNoRootClient = 0xFFFFu; + +struct GeneralMap { + uint32_t magic = kRegisterMapMagic; // vendor/device-defined; this module carries the field only + uint32_t svr_version = 0; // 32 bit wide on the wire, not 16 (c-RCP regmap.h) + uint16_t vendor_id = 0; + uint16_t device_id = 0; + uint16_t svr_ep_count = 0; + + // REQ-RMAP-023 (TC18 §12.3.1.1/§12.3.1.2): the server's own + // lifecycle::ServerState, cast to its wire representation. Content + // modeling only: making this field actually readable over the wire is + // its own still-open general register-read-dispatch gap (this field is + // deliberately excluded from render() below — see render()'s own doc + // comment) — a caller such as a future mock server keeps this in sync + // after every successful lifecycle::ServerLifecycle::advance(). + uint8_t svr_lifecycle_state = 0; + + uint8_t svr_req_stream_max = 0; // REQ-RMAP-026 (Table 20, 0x000E, 8 bit, R): max request streams usable to access this server + uint8_t svr_responder_streams_max = 0; // REQ-RMAP-026 (Table 20, 0x000F, 8 bit, R): max supported responder queues + uint16_t svr_responder_mem_size = 0; // REQ-RMAP-027 (Table 20, 0x0010, 16 bit, R): max responder-queue memory, in 32-bit words + uint16_t svr_req_mem_size = 0; // REQ-RMAP-027 (Table 20, 0x0012, 16 bit, R): max memory for EP request queues, in 32-bit words + uint8_t svr_sequencers_max = 0; // REQ-RMAP-028 (Table 20, 0x0014, 8 bit, R): 0 = sequencer operation not supported; 1..n = available sequencer state registers + uint8_t svr_configuration_lock = 0; // REQ-RMAP-029 (Table 20, 0x0015, 8 bit, R): 0x00 permits write access to R/W+ parameters; any other value rejects it + + // REQ-RMAP-030: RCP_REGMAP_OPT_*-equivalent bitmask (kOptCompoundWait/ + // kOptTrigger/kOptChained/kOptTimeSync/kOptEnhCancel above), 8 bit on + // the wire. + uint8_t svr_implemented_options = 0; + + uint8_t reserved_0x17 = 0; // REQ-RMAP-031 (Table 20, 0x0017, 8 bit): reserved, must read 0x00 + uint16_t svr_io_pin_count = 0; // REQ-RMAP-032 (Table 20, 0x0018, 16 bit, R): number of assignable I/O pins + + // svr_root_client_index — passive Table-20 storage (kNoRootClient if + // unset). Not rendered on the wire by render() below (see its own doc + // comment) — a caller composing a real RC Server keeps this field and + // Ep0::claim_root_client()'s own session-scoped root-client tracking in + // sync itself; this header does not wire the two together automatically + // (Ep0's own `client` identifiers are opaque, transport-assigned + // indices, not necessarily this field's own uint16_t stream-index + // width). + uint16_t svr_root_client_index = kNoRootClient; + + uint16_t svr_hw_cfg_ptr = 0; // REQ-RMAP-033 (Table 20, 0x001A, 16 bit, R): address of the HW_config register map (batch B) + + uint8_t svr_request_stream_cfg_capacity = 0; // REQ-RMAP-034 (Table 20, 0x001C, 8 bit, R) + uint8_t svr_response_stream_cfg_capacity = 0; // REQ-RMAP-034 (Table 20, 0x001D, 8 bit, R) + uint16_t svr_request_stream_cfg_ptr = 0; // REQ-RMAP-034 (Table 20, 0x001E, 16 bit, R) + uint16_t svr_response_stream_cfg_ptr = 0; // REQ-RMAP-034 (Table 20, 0x0020, 16 bit, R) + + uint16_t reserved_0x22 = 0; // REQ-RMAP-035 (Table 20, 0x0022, 16 bit): reserved, must read 0x00 + + uint16_t svr_ep_generic_cfg_ptr = 0; // REQ-RMAP-036 (Table 20, 0x0024, 16 bit, R): address of the EP_config register map (§13.2) + uint16_t svr_ep_generic_cfg_capacity = 0; // REQ-RMAP-036 (Table 20, 0x0026, 16 bit, R): LENGTH OF THE EP CONFIG REGISTER SECTION IN BYTES -- a byte length, not an entry count + + uint16_t svr_ep_bytebus_id_map_ptr = 0; // REQ-RMAP-037 (Table 20, 0x0028, 16 bit, R): address of the EP - byte_bus_id mapping table (batch B, §12.7.8) + uint8_t svr_ep_bytebus_id_map_capacity = 0; // REQ-RMAP-037 (Table 20, 0x002A, 8 bit, R): max entries in that table + + // 0x002B: inferred, unconfirmed one-octet alignment gap between + // svr_ep_bytebus_id_map_capacity and svr_ep_functional_cfg_ptr -- TC18's + // own table has no explicit "reserved" row there. Not modeled as a + // field (render() below writes 0x00 there directly); see render()'s own + // doc comment. + + uint16_t svr_ep_functional_cfg_ptr = 0; // REQ-RMAP-038 (Table 20, 0x002C, 16 bit, R): address of the EP_FUNC_config register map (§13.7.1.2) + uint16_t svr_sequencer_state_ptr = 0; // REQ-RMAP-038 (Table 20, 0x002E, 16 bit, R): address of the Sequencer_config register map (§12.7.10, batch B) + + // REQ-RMAP-039 (Table 20 continued -- every address in this group is + // INFERRED, not directly read: TC18's own "Absolute address" column is + // blank for this whole continuation page. See c-RCP regmap.h's own + // svr_network_interface_cfg_ptr comment for the full derivation. + uint16_t svr_network_interface_cfg_ptr = 0; // 0x0030 (inferred): address of the Network_config register map; 0 = not supported + uint16_t svr_network_interface_cfg_capacity = 0; // 0x0032 (inferred) + uint16_t svr_physical_layer_cfg_ptr = 0; // 0x0034 (inferred): address of the physical-layer configuration register map; 0 = not supported + uint16_t svr_physical_layer_cfg_capacity = 0; // 0x0036 (inferred) + uint16_t svr_time_synch_cfg_ptr = 0; // 0x0038 (inferred): address of the PTP_config register map; 0 = not supported + uint16_t svr_time_synch_cfg_capacity = 0; // 0x003A (inferred) + uint16_t svr_security_cfg_ptr = 0; // 0x003C (inferred): address of the security configuration register map; 0 = not supported + uint16_t svr_security_cfg_capacity = 0; // 0x003E (inferred) + + // REQ-RMAP-039 (issue #429): Table 20's own true final pair, immediately + // following svr_security_cfg_capacity. TC18's own printed description + // text for this specific pair is genuinely swapped between the two rows + // (a known, still-open drafting issue in the primary source itself, + // "051RC5 - proposal to solve TI_032") -- named _ptr first, _capacity + // second here to match every other Group 1 pair's own address-order + // convention regardless. + uint16_t svr_device_specific_cfg_ptr = 0; // 0x0040 (inferred) + uint16_t svr_device_specific_cfg_capacity = 0; // 0x0042 (inferred) +}; + +// ── Table 20 wire codec (REQ-RMAP-024) ──────────────────────────────────────── +// Every field above is documented against its own TC18 §12.7.5 Table 20 +// absolute address. The functions below are the same wire mechanism +// discovery.hpp already uses for its own narrower identity slice (a plain +// ACF_ABB read addressed to byte_bus_id 0 / EP0), generalized to serve +// GeneralMap's full Table 20 extent — ported from c-RCP's +// rcp_regmap_general_render()/_encode_read_response()/_decode_read_response()/ +// _decode_write_request() (src/regmap.c). + +// kGeneralMapLen — total wire length (bytes) of GeneralMap's TC18 §12.7.5 +// Table 20 extent, absolute address 0x0000 through 0x0043 inclusive. +constexpr size_t kGeneralMapLen = 0x0044u; +using GeneralMapImage = std::array; + +// render — REQ-RMAP-024/039. Serializes map's Table 20 fields into the +// returned image at each field's own TC18-documented absolute address. +// +// Deliberately excludes svr_lifecycle_state and svr_root_client_index: +// neither has a genuine Table 20 address -- both are this struct's own +// convenience placement of what TC18 §13.7.1.2 Table 33/36 (the RC +// Server's own EP_func block, reached via svr_ep_functional_cfg_ptr, a +// *different*, pointer-addressed mechanism) actually owns. Rendering +// either field into this Table 20 image at an invented address would be a +// real conformance defect, not a harmless placeholder (c-RCP regmap.h's +// own REQ-RMAP-023 doc comment). +// +// The one-byte gap at absolute address 0x002B (between +// svr_ep_bytebus_id_map_capacity's own 0x002A and svr_ep_functional_cfg_ +// ptr's own 0x002C) is written as 0x00: TC18's own table has no explicit +// "reserved" row there the way 0x0017 and 0x0022 both do -- an +// unconfirmed, inferred single-octet alignment gap, not a directly-cited +// reserved register. +inline GeneralMapImage render(const GeneralMap& map) noexcept { + GeneralMapImage out{}; + + avtp::detail::put_u32(&out[0x0000], map.magic); + avtp::detail::put_u32(&out[0x0004], map.svr_version); + avtp::detail::put_u16(&out[0x0008], map.vendor_id); + avtp::detail::put_u16(&out[0x000A], map.device_id); + avtp::detail::put_u16(&out[0x000C], map.svr_ep_count); + // 0x000E..0x000F: svr_lifecycle_state deliberately NOT rendered -- see + // this function's own doc comment above. + out[0x000E] = map.svr_req_stream_max; + out[0x000F] = map.svr_responder_streams_max; + avtp::detail::put_u16(&out[0x0010], map.svr_responder_mem_size); + avtp::detail::put_u16(&out[0x0012], map.svr_req_mem_size); + out[0x0014] = map.svr_sequencers_max; + out[0x0015] = map.svr_configuration_lock; + out[0x0016] = map.svr_implemented_options; + out[0x0017] = map.reserved_0x17; + avtp::detail::put_u16(&out[0x0018], map.svr_io_pin_count); + // svr_root_client_index deliberately NOT rendered -- same exclusion as + // svr_lifecycle_state above. + avtp::detail::put_u16(&out[0x001A], map.svr_hw_cfg_ptr); + out[0x001C] = map.svr_request_stream_cfg_capacity; + out[0x001D] = map.svr_response_stream_cfg_capacity; + avtp::detail::put_u16(&out[0x001E], map.svr_request_stream_cfg_ptr); + avtp::detail::put_u16(&out[0x0020], map.svr_response_stream_cfg_ptr); + avtp::detail::put_u16(&out[0x0022], map.reserved_0x22); + avtp::detail::put_u16(&out[0x0024], map.svr_ep_generic_cfg_ptr); + avtp::detail::put_u16(&out[0x0026], map.svr_ep_generic_cfg_capacity); + avtp::detail::put_u16(&out[0x0028], map.svr_ep_bytebus_id_map_ptr); + out[0x002A] = map.svr_ep_bytebus_id_map_capacity; + // 0x002B: inferred, unconfirmed one-octet alignment gap -- left 0x00 by + // GeneralMapImage's own zero-initialization above. + avtp::detail::put_u16(&out[0x002C], map.svr_ep_functional_cfg_ptr); + avtp::detail::put_u16(&out[0x002E], map.svr_sequencer_state_ptr); + avtp::detail::put_u16(&out[0x0030], map.svr_network_interface_cfg_ptr); + avtp::detail::put_u16(&out[0x0032], map.svr_network_interface_cfg_capacity); + avtp::detail::put_u16(&out[0x0034], map.svr_physical_layer_cfg_ptr); + avtp::detail::put_u16(&out[0x0036], map.svr_physical_layer_cfg_capacity); + avtp::detail::put_u16(&out[0x0038], map.svr_time_synch_cfg_ptr); + avtp::detail::put_u16(&out[0x003A], map.svr_time_synch_cfg_capacity); + avtp::detail::put_u16(&out[0x003C], map.svr_security_cfg_ptr); + avtp::detail::put_u16(&out[0x003E], map.svr_security_cfg_capacity); + avtp::detail::put_u16(&out[0x0040], map.svr_device_specific_cfg_ptr); + avtp::detail::put_u16(&out[0x0042], map.svr_device_specific_cfg_capacity); + + return out; +} -constexpr uint32_t kRegisterMapMagic = 0x52435030; // "RCP0" — this implementation's own placeholder value +// encode_read_response — REQ-RMAP-024. Encodes an ACF_ABB read RESPONSE +// addressed to byte_bus_id 0 (EP0), carrying min(read_size, kGeneralMapLen) +// octets of map's own render() image starting at absolute address 0, with +// any remaining requested octets (up to read_size) zero-filled -- the same +// "response spans exactly read_size octets" convention discovery.hpp +// already establishes for its own narrower slice. +inline std::vector encode_read_response(const GeneralMap& map, uint8_t read_size, + uint8_t transaction_num) { + const GeneralMapImage image = render(map); + std::vector payload(read_size, 0); + const size_t copy_len = std::min(read_size, image.size()); + std::copy(image.begin(), image.begin() + static_cast(copy_len), payload.begin()); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = static_cast(kEp0); + hdr.op = false; // read + hdr.rsp = true; + hdr.read_size_or_segment_num = read_size; + hdr.transaction_num = transaction_num; + + return acf::encode_acf_abb(hdr, payload); +} + +// decode_read_response — REQ-RMAP-024/039. Decodes and validates an +// ACF_ABB general-register-map read RESPONSE from b[0..len). On success, +// out_map has every Table 20 field render() populates overwritten from +// whichever prefix of kGeneralMapLen the response payload actually carries +// -- a short response (fewer octets than the full extent) leaves the +// remaining, un-carried fields of out_map untouched past the first +// unconditional 14-octet (magic/svr_version/vendor_id/device_id/ +// svr_ep_count) group, so a caller must default-construct (or otherwise +// define) out_map before calling this. svr_lifecycle_state and +// svr_root_client_index are never touched, for the same reason render() +// never renders them. +// +// Faithfully reproduces c-RCP's own single-checkpoint short-frame handling +// (rcp_regmap_general_decode_read_response(), src/regmap.c): there is +// exactly one early-return length check (at absolute address 0x000E, +// immediately after the first 5 fields), not a per-field one -- a response +// whose payload covers anything past 0x000E populates every remaining +// field, reading zero for any octet range beyond what the payload actually +// carried (out_map's own zero-padded local image, not out_map's own prior +// value). This matches c-RCP's real, currently-shipping behavior, not just +// its doc comment's own narrower framing of it -- ported unchanged rather +// than silently "fixed" against the reference implementation. +inline std::error_code decode_read_response(const uint8_t* b, size_t len, GeneralMap& out_map) noexcept { + acf::AcfMessageInfo hdr; + std::vector payload; + const std::error_code acf_ec = acf::decode_acf_abb(b, len, hdr, payload); + if (acf_ec) { + if (acf_ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) + return make_error_code(GeneralMapErrc::short_frame); + return make_error_code(GeneralMapErrc::bad_msg_type); + } + + if (hdr.byte_bus_id != static_cast(kEp0)) return make_error_code(GeneralMapErrc::wrong_bus); + if (hdr.op) return make_error_code(GeneralMapErrc::wrong_op); // op=true is write; a read response is expected + + GeneralMapImage image{}; + const size_t have = std::min(payload.size(), image.size()); + std::copy(payload.begin(), payload.begin() + static_cast(have), image.begin()); + + out_map.magic = avtp::detail::get_u32(&image[0x0000]); + out_map.svr_version = avtp::detail::get_u32(&image[0x0004]); + out_map.vendor_id = avtp::detail::get_u16(&image[0x0008]); + out_map.device_id = avtp::detail::get_u16(&image[0x000A]); + out_map.svr_ep_count = avtp::detail::get_u16(&image[0x000C]); + if (have <= 0x000E) return {}; + + out_map.svr_req_stream_max = image[0x000E]; + out_map.svr_responder_streams_max = image[0x000F]; + out_map.svr_responder_mem_size = avtp::detail::get_u16(&image[0x0010]); + out_map.svr_req_mem_size = avtp::detail::get_u16(&image[0x0012]); + out_map.svr_sequencers_max = image[0x0014]; + out_map.svr_configuration_lock = image[0x0015]; + out_map.svr_implemented_options = image[0x0016]; + out_map.reserved_0x17 = image[0x0017]; + out_map.svr_io_pin_count = avtp::detail::get_u16(&image[0x0018]); + out_map.svr_hw_cfg_ptr = avtp::detail::get_u16(&image[0x001A]); + out_map.svr_request_stream_cfg_capacity = image[0x001C]; + out_map.svr_response_stream_cfg_capacity = image[0x001D]; + out_map.svr_request_stream_cfg_ptr = avtp::detail::get_u16(&image[0x001E]); + out_map.svr_response_stream_cfg_ptr = avtp::detail::get_u16(&image[0x0020]); + out_map.reserved_0x22 = avtp::detail::get_u16(&image[0x0022]); + out_map.svr_ep_generic_cfg_ptr = avtp::detail::get_u16(&image[0x0024]); + out_map.svr_ep_generic_cfg_capacity = avtp::detail::get_u16(&image[0x0026]); + out_map.svr_ep_bytebus_id_map_ptr = avtp::detail::get_u16(&image[0x0028]); + out_map.svr_ep_bytebus_id_map_capacity = image[0x002A]; + out_map.svr_ep_functional_cfg_ptr = avtp::detail::get_u16(&image[0x002C]); + out_map.svr_sequencer_state_ptr = avtp::detail::get_u16(&image[0x002E]); + out_map.svr_network_interface_cfg_ptr = avtp::detail::get_u16(&image[0x0030]); + out_map.svr_network_interface_cfg_capacity = avtp::detail::get_u16(&image[0x0032]); + out_map.svr_physical_layer_cfg_ptr = avtp::detail::get_u16(&image[0x0034]); + out_map.svr_physical_layer_cfg_capacity = avtp::detail::get_u16(&image[0x0036]); + out_map.svr_time_synch_cfg_ptr = avtp::detail::get_u16(&image[0x0038]); + out_map.svr_time_synch_cfg_capacity = avtp::detail::get_u16(&image[0x003A]); + out_map.svr_security_cfg_ptr = avtp::detail::get_u16(&image[0x003C]); + out_map.svr_security_cfg_capacity = avtp::detail::get_u16(&image[0x003E]); + out_map.svr_device_specific_cfg_ptr = avtp::detail::get_u16(&image[0x0040]); + out_map.svr_device_specific_cfg_capacity = avtp::detail::get_u16(&image[0x0042]); + return {}; +} + +// decode_write_request — REQ-RMAP-025. Decodes an ACF_ABB WRITE request +// from b[0..len) addressed to byte_bus_id 0 (EP0) targeting the Table 20 +// general register map. Every one of Table 20's registers is TC18-defined +// access type R (read-only), so this never applies a write -- it exists +// only to recognize a genuine write attempt and report the correct wire +// error for it: out_error is always acf::WireErrorCode::LockedMemAccess +// (Table 20 has no writable field at all, so this outcome does not depend +// on lifecycle state) and out_transaction_num is populated; the caller +// builds the actual error response via acf::make_response()/ +// acf::encode_error_payload(out_error). +inline std::error_code decode_write_request(const uint8_t* b, size_t len, acf::WireErrorCode& out_error, + uint8_t& out_transaction_num) noexcept { + acf::AcfMessageInfo hdr; + std::vector payload; + const std::error_code acf_ec = acf::decode_acf_abb(b, len, hdr, payload); + if (acf_ec) { + if (acf_ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) + return make_error_code(GeneralMapErrc::short_frame); + return make_error_code(GeneralMapErrc::bad_msg_type); + } + + if (hdr.byte_bus_id != static_cast(kEp0)) return make_error_code(GeneralMapErrc::wrong_bus); + if (!hdr.op) return make_error_code(GeneralMapErrc::wrong_op); // op=false is read; a write request is expected + + out_error = acf::WireErrorCode::LockedMemAccess; + out_transaction_num = hdr.transaction_num; + return {}; +} + +// ── Root-client / per-EP-restricted-client model ────────────────────────────── +// REQ-RMAP-009/010/011/012/070/086, ported from c-RCP's +// rcp_regmap_ep_client_t and rcp_regmap_writer_ctx() (include/rcp/ +// regmap.h:1152-1211, src/regmap.c). + +// EpClient — one endpoint's write-restriction: the single stream (if any) +// authorized to write that endpoint's functional config directly through +// its own registered request stream, per lifecycle::WriterCtx:: +// via_owning_stream. has_owning_stream distinguishes "no owning stream +// configured" from stream index 0, which is itself a valid index. +struct EpClient { + bool has_owning_stream = false; + uint16_t owning_stream_index = 0; +}; + +// writer_ctx — derives a lifecycle::WriterCtx from this register map's +// root-client/owning-stream data for a request arriving on +// requesting_stream_index. via_ep0 must be true iff the request actually +// arrived through EP0; via_unicast must be true iff the request's frame had +// a unicast destination MAC; via_discovery_stream must be true iff the +// request arrived via the discovery stream -- this function does not +// re-derive any of the three from an address or stream role itself, +// matching acf.hpp/avtp.hpp's convention of taking already-classified +// inputs rather than re-parsing a frame. ep_client may be null, meaning +// "this endpoint has no owning stream on record" (via_owning_stream is +// then always false). +// +// TODO(phase4-batch-b): TC18 §12.3.1.2's own "any valid stream_id/ +// byte_bus_id combination is accepted, but only when no root client is +// configured at all" rule needs a real EP-ID/byte_bus_id association check +// (c-RCP's rcp_regmap_ep_id_map_is_valid_association(), keyed off each +// row's own request_stream_index) to evaluate for real. EpIdMappingEntry +// (batch B's own type, left unchanged above) does not yet carry that +// field, so via_valid_stream_association is conservatively, always false +// here -- fail-closed (never a false "yes") until batch B lands the full +// association table. requesting_byte_bus_id/ep_id_map/ep_id_map_count are +// already accepted (matching c-RCP's own signature exactly) so batch B's +// own fix is a pure function-body change, not a signature change. +inline lifecycle::WriterCtx writer_ctx(const GeneralMap& map, const EpClient* ep_client, + uint16_t requesting_stream_index, bool via_ep0, bool via_unicast, + bool via_discovery_stream, + avtp::ByteBusId requesting_byte_bus_id, + const EpIdMappingEntry* ep_id_map, size_t ep_id_map_count) noexcept { + (void)requesting_byte_bus_id; + (void)ep_id_map; + (void)ep_id_map_count; + + lifecycle::WriterCtx ctx; + + ctx.via_root_client_ep0 = via_ep0 && map.svr_root_client_index != kNoRootClient && + requesting_stream_index == map.svr_root_client_index; + + ctx.via_owning_stream = ep_client != nullptr && ep_client->has_owning_stream && + requesting_stream_index == ep_client->owning_stream_index; + + ctx.via_non_unicast_frame = !via_unicast; + + // REQ-RMAP-070: pass through, matching via_ep0/via_unicast's own + // already-classified-input convention. + ctx.via_discovery_stream = via_discovery_stream; + + // See this function's own TODO(phase4-batch-b) doc comment above. + ctx.via_valid_stream_association = false; + + return ctx; +} + +// ── RC Server's own functional-configuration content (TC18 §13.7.1.2) ──────── +// REQ-RMAP-066/067, ported from c-RCP's rcp_regmap_svr_ep_cfg_t +// (include/rcp/regmap.h:1211-1303, src/regmap.c). +// +// TC18 §13.7.1.2 describes "Table 33/36: RC Server functional +// configuration" with two confirmed primary-source defects c-RCP's own +// investigation found and did NOT force-resolve: (1) the table's 8 register +// rows share only 5 distinct relative addresses (an address collision), and +// (2) §13.7.1.1's own prose states the RC Server "is not included in the +// EP_FUNC_config register maps" yet Table 33/36 lists it anyway. Given no +// evidence this codebase's own server ever dispatches the RC Server through +// the generic per-endpoint EP_FUNC mechanism, this struct models ONLY the +// two fields free of both defects: svr_discovery_timeout and svr_ep_status +// -- svr_root_client_index/svr_lifecycle_state are NOT duplicated here (see +// GeneralMap above for their own, separate content-modeling-only home). +// +// Zero-initialization plus svr_discovery_timeout defaulting to TC18's own +// stated power-on default is this port's equivalent of c-RCP's +// rcp_regmap_svr_ep_cfg_init(). +struct SvrEpCfg { + // svr_discovery_timeout -- REQ-RMAP-066: microseconds; TC18's own + // stated default is 20000 (20 ms). + uint16_t svr_discovery_timeout = 20000u; + uint16_t svr_ep_status = 0; // REQ-RMAP-067; TC18 gives no further bit-level breakdown at this citation +}; + +// ── RegisterMap ─────────────────────────────────────────────────────────────── +// The general register-map fields needed to bootstrap everything else, plus +// the in-memory contents of the tables those pointer/capacity pairs +// describe. general/svr_ep_cfg are this batch's own GeneralMap/SvrEpCfg +// content (see above); the pointer/capacity fields and table-content +// vectors below them are batch B's unchanged, pre-existing shape. 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. + GeneralMap general{}; // TC18 §12.7.5 Table 20 -- rcp_regmap_general_t + SvrEpCfg svr_ep_cfg{}; // TC18 §13.7.1.2 -- rcp_regmap_svr_ep_cfg_t + + // Pointer/capacity fields for the batch-B bootstrap tables. TablePointer hw_pin_map_table{}; TablePointer request_stream_table{}; TablePointer response_stream_table{}; @@ -296,34 +1077,41 @@ struct RegisterMap { 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 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). + // (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 ───────────────────────────────────── +// ── 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 *functional* config of the endpoint(s) -// it owns. The generic config block is the RC Server's own and is writable by -// the root client alone, no matter who owns the endpoint (TC18 §13.1/§13.2, -// cpp-RCP-D2 — see check_write_access below). +// read the whole map. Only one client — the root client — may write the +// whole map through EP0; every other client is restricted to writing the +// *functional* config of the endpoint(s) it owns. The generic config block is +// the RC Server's own and is writable by the root client alone, no matter +// who owns the endpoint (TC18 §13.1/§13.2, cpp-RCP-D2 — see +// check_write_access below). // -// "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 avtp::StreamId, only that it is stable for the lifetime of -// the connection. +// This class is this codebase's own original, richer session-tracking +// design layered on top of GeneralMap's own passive svr_root_client_index/ +// EpClient data (see c-RCP's own file header: "rcp_regmap_writer_ctx() +// derives server.h's rcp_lifecycle_writer_ctx_t from this register data, +// without duplicating rcp_lifecycle_field_writable()'s already-built +// authorization logic" — this class is the analogous idea one layer up, +// scoped to EP0's own access-control decisions specifically). "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 an avtp::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 @@ -335,28 +1123,29 @@ class Ep0 { // bounds a target endpoint against, but the writes those checks gate // land in regs.generic_configs/regs.functional_configs instead — two // vectors this class does not own and cannot force the caller to keep - // in sync with endpoint_count. If they are not the same length as - // endpoint_count when this object is constructed, size endpoint_owner_ - // to 0 rather than to the untrustworthy endpoint_count: every non-EP0 - // access check then fails closed with invalid_parameter instead of - // handing out an in-range verdict that a subsequent write turns into an - // out-of-bounds write on the shorter/empty config vector (cpp-RCP-N2-01, - // issue #64). write_generic_config/write_functional_config additionally + // in sync with general.svr_ep_count. If they are not the same length as + // general.svr_ep_count when this object is constructed, size + // endpoint_owner_ to 0 rather than to the untrustworthy + // general.svr_ep_count: every non-EP0 access check then fails closed + // with invalid_parameter instead of handing out an in-range verdict + // that a subsequent write turns into an out-of-bounds write on the + // shorter/empty config vector (cpp-RCP-N2-01, issue #64). + // write_generic_config/write_functional_config additionally // bounds-check against the actual vector being indexed as a second, // independent layer of defense (see below). Ep0(RegisterMap& regs, lifecycle::ServerLifecycle& lc) noexcept : regs_(regs), lifecycle_(lc), - endpoint_owner_(size_invariant_holds(regs) ? regs.endpoint_count : 0) {} + endpoint_owner_(size_invariant_holds(regs) ? regs.general.svr_ep_count : 0) {} // is_endpoint_table_consistent reports whether the size invariant this // class's access checks rely on currently holds (regs_.generic_configs - // .size() == regs_.functional_configs.size() == regs_.endpoint_count). + // .size() == regs_.functional_configs.size() == regs_.general.svr_ep_count). // It is false only when the RegisterMap this Ep0 was constructed with // violated the invariant and per-endpoint access has therefore been // fail-closed (endpoint_owner_ sized to 0) until a valid replacement // map is installed via write_whole_map. bool is_endpoint_table_consistent() const noexcept { - return size_invariant_holds(regs_) && endpoint_owner_.size() == regs_.endpoint_count; + return size_invariant_holds(regs_) && endpoint_owner_.size() == regs_.general.svr_ep_count; } // claim_root_client implements the exclusive root-client rule: once a @@ -452,22 +1241,22 @@ class Ep0 { // access check in this class depends on intact (cpp-RCP-N2-02, issue // #65): a root client could otherwise install a replacement map whose // generic_configs/functional_configs disagree with its own - // endpoint_count, or that simply differs in endpoint_count from the map - // endpoint_owner_ was last sized against, either of which lets a - // subsequent per-endpoint write pass a stale/incorrect bounds check and - // land out of bounds on the new vectors. So: reject an internally - // inconsistent replacement map outright, and resize endpoint_owner_ to - // match the (now validated) new endpoint_count as part of the same - // operation, discarding prior per-endpoint owner assignments — they - // describe endpoints in the map being replaced, not necessarily the - // same endpoints in the new one. + // general.svr_ep_count, or that simply differs in general.svr_ep_count + // from the map endpoint_owner_ was last sized against, either of which + // lets a subsequent per-endpoint write pass a stale/incorrect bounds + // check and land out of bounds on the new vectors. So: reject an + // internally inconsistent replacement map outright, and resize + // endpoint_owner_ to match the (now validated) new general.svr_ep_count + // as part of the same operation, discarding prior per-endpoint owner + // assignments — they describe endpoints in the map being replaced, not + // necessarily the same endpoints in the new one. 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); if (!size_invariant_holds(new_map)) return make_error_code(RegMapErrc::invalid_parameter); regs_ = std::move(new_map); - endpoint_owner_.assign(regs_.endpoint_count, std::nullopt); + endpoint_owner_.assign(regs_.general.svr_ep_count, std::nullopt); return {}; } @@ -510,12 +1299,12 @@ class Ep0 { // size_invariant_holds is the shared root-invariant check both the // constructor and write_whole_map enforce: every access-control check // above bounds a target endpoint against endpoint_owner_ (sized from - // endpoint_count), but the writes those checks gate land in + // general.svr_ep_count), but the writes those checks gate land in // regs.generic_configs/regs.functional_configs — this must hold for // that to be safe (cpp-RCP-N2-01/N2-02, issues #64/#65). static bool size_invariant_holds(const RegisterMap& regs) noexcept { - return regs.generic_configs.size() == regs.endpoint_count && - regs.functional_configs.size() == regs.endpoint_count; + return regs.generic_configs.size() == regs.general.svr_ep_count && + regs.functional_configs.size() == regs.general.svr_ep_count; } RegisterMap& regs_; @@ -527,8 +1316,13 @@ class Ep0 { } // namespace regmap } // namespace rcp -// Enable std::error_code construction from rcp::regmap::RegMapErrc. +// Enable std::error_code construction from rcp::regmap::RegMapErrc/ +// GeneralMapErrc/EpGenericCfgReconfigErrc. namespace std { template <> struct is_error_code_enum : true_type {}; +template <> +struct is_error_code_enum : true_type {}; +template <> +struct is_error_code_enum : true_type {}; } // namespace std diff --git a/tests/test_mock.cpp b/tests/test_mock.cpp index 657fda0..ce7dff3 100644 --- a/tests/test_mock.cpp +++ b/tests/test_mock.cpp @@ -54,7 +54,7 @@ TEST_CASE("Server starts HW_UNCONFIGURED with a ten-endpoint register map", "[mock][REQ-MOCK-001]") { mock::Server server; REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); - REQUIRE(server.registers().endpoint_count == 10); + REQUIRE(server.registers().general.svr_ep_count == 10); REQUIRE(server.registers().generic_configs.size() == 10); REQUIRE(server.registers().functional_configs.size() == 10); REQUIRE(server.registers().ep_id_mapping.size() == 10); @@ -100,7 +100,7 @@ TEST_CASE("EP0 read answers the register map's magic number for any client", REQUIRE_FALSE(ec); REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ReadResponse); REQUIRE(resp_payload.size() == mock::kEp0PartialReadLen); - REQUIRE(avtp::detail::get_u32(resp_payload.data()) == server.registers().magic); + REQUIRE(avtp::detail::get_u32(resp_payload.data()) == server.registers().general.magic); } } @@ -126,12 +126,12 @@ TEST_CASE("write_whole_map requires the root client", "[mock][REQ-MOCK-005]") { REQUIRE(claim_ec == regmap::make_error_code(regmap::RegMapErrc::request_rejected)); regmap::RegisterMap replacement = server.registers(); - replacement.vendor_id = 0x1234; + replacement.general.vendor_id = 0x1234; auto write_ec = server.ep0().write_whole_map(/*client=*/2, replacement); REQUIRE(write_ec == regmap::make_error_code(regmap::RegMapErrc::unauthorized_access)); REQUIRE_FALSE(server.ep0().write_whole_map(/*client=*/1, replacement)); - REQUIRE(server.registers().vendor_id == 0x1234); + REQUIRE(server.registers().general.vendor_id == 0x1234); } // ── Operational-request gating ─────────────────────────────────────────────── diff --git a/tests/test_regmap.cpp b/tests/test_regmap.cpp index 3e4dc59..15d926c 100644 --- a/tests/test_regmap.cpp +++ b/tests/test_regmap.cpp @@ -13,14 +13,41 @@ // fusa:test REQ-REGMAP-013 // fusa:test REQ-REGMAP-014 // fusa:test REQ-REGMAP-015 +// +// c-RCP-derived test coverage added in this batch (Phase 17 / cpp-RCP issue +// #129, "Phase 4 batch A"), ported from c-RCP's tests/test_regmap.c and the +// relevant slice of tests/test_tc18_gaps_regmap.c: +// fusa:test REQ-RMAP-001 +// fusa:test REQ-RMAP-003 +// fusa:test REQ-RMAP-009 +// fusa:test REQ-RMAP-010 +// fusa:test REQ-RMAP-011 +// fusa:test REQ-RMAP-012 +// fusa:test REQ-RMAP-016 +// fusa:test REQ-RMAP-024 +// fusa:test REQ-RMAP-025 +// fusa:test REQ-RMAP-030 +// fusa:test REQ-RMAP-066 +// fusa:test REQ-RMAP-067 +// fusa:test REQ-RMAP-070 +// fusa:test REQ-RMAP-076 +// fusa:test REQ-RMAP-077 +// fusa:test REQ-RMAP-078 +// fusa:test REQ-RMAP-079 +// fusa:test REQ-RMAP-081 +// fusa:test REQ-RMAP-086 +// fusa:test REQ-RMAP-087 // 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). +// Register-Map Model", v2.1.0; content-expanded in ROADMAP.md Phase 17 +// "Phase 4 batch A" against c-RCP's current src/regmap.c/include/rcp/regmap.h). #include +#include #include +using namespace rcp; using namespace rcp::regmap; using rcp::lifecycle::ServerLifecycle; using rcp::lifecycle::ServerState; @@ -31,7 +58,7 @@ namespace { // 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.general.svr_ep_count = n; m.generic_configs.resize(n); m.functional_configs.resize(n); return m; @@ -39,19 +66,28 @@ RegisterMap make_map(uint16_t n) { } // namespace +// ── EP0 / is_ep0 ───────────────────────────────────────────────────────────── + +TEST_CASE("is_ep0 is true only for EP0's own index", "[regmap][REQ-RMAP-001]") { + REQUIRE(is_ep0(kEp0)); + REQUIRE_FALSE(is_ep0(1)); + REQUIRE_FALSE(is_ep0(42)); + REQUIRE_FALSE(is_ep0(0xFFFF)); +} + // ── 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; + generic.ep_type = 0x03; // SPI, per c-RCP's Table 29/30 ep_type enum + generic.ep_description = 0x11223344; + generic.ep_tx_buffer_size = 4; EndpointFunctionalConfig functional; functional.data = {0xAA, 0xBB}; - REQUIRE(generic.hw_pin_indices.size() == 3); + REQUIRE(generic.ep_type == 0x03); REQUIRE(functional.data.size() == 2); } @@ -59,13 +95,13 @@ TEST_CASE("EndpointGenericConfig and EndpointFunctionalConfig are distinct, inde 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; + map.general.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); + REQUIRE(ep0.read_whole_map().general.vendor_id == 0x1234); } // ── EP0 whole-map write is root-client-only ───────────────────────────────────── @@ -76,7 +112,7 @@ TEST_CASE("Only the root client may write the whole register map through EP0", " Ep0 ep0(map, lc); RegisterMap replacement = make_map(1); - replacement.vendor_id = 0x99; + replacement.general.vendor_id = 0x99; // No root client claimed yet: even the would-be client is unauthorized. auto ec = ep0.write_whole_map(/*client=*/1, replacement); @@ -85,7 +121,7 @@ TEST_CASE("Only the root client may write the whole register map through EP0", " REQUIRE_FALSE(ep0.claim_root_client(1)); REQUIRE_FALSE(ep0.write_whole_map(1, replacement)); - REQUIRE(ep0.read_whole_map().vendor_id == 0x99); + REQUIRE(ep0.read_whole_map().general.vendor_id == 0x99); // A different client is still refused. auto ec2 = ep0.write_whole_map(/*client=*/2, replacement); @@ -161,15 +197,15 @@ TEST_CASE("Owning an endpoint does not grant a non-root client write access to i REQUIRE_FALSE(ep0.set_endpoint_owner(2, /*client=*/20)); EndpointGenericConfig cfg; - cfg.request_queue_size = 8; - cfg.hw_pin_indices = {7}; + cfg.ep_tx_buffer_size = 8; + cfg.ep_description = 7; // The owner of endpoint 1 is refused on its *own* endpoint's generic block. auto ec_own = ep0.write_generic_config(/*client=*/10, /*target=*/1, cfg); REQUIRE(ec_own); REQUIRE(ec_own == make_error_code(RegMapErrc::unauthorized_access)); - REQUIRE(ep0.read_whole_map().generic_configs[0].request_queue_size != 8); - REQUIRE(ep0.read_whole_map().generic_configs[0].hw_pin_indices.empty()); + REQUIRE(ep0.read_whole_map().generic_configs[0].ep_tx_buffer_size != 8); + REQUIRE(ep0.read_whole_map().generic_configs[0].ep_description == 0); // ...and, as before, on an endpoint owned by somebody else. auto ec_other = ep0.write_generic_config(/*client=*/10, /*target=*/2, cfg); @@ -187,9 +223,9 @@ TEST_CASE("Owning an endpoint does not grant a non-root client write access to i // Only the root client may write the generic block. REQUIRE_FALSE(ep0.claim_root_client(/*client=*/99)); REQUIRE_FALSE(ep0.write_generic_config(/*client=*/99, /*target=*/1, cfg)); - REQUIRE(ep0.read_whole_map().generic_configs[0].request_queue_size == 8); + REQUIRE(ep0.read_whole_map().generic_configs[0].ep_tx_buffer_size == 8); REQUIRE_FALSE(ep0.write_generic_config(/*client=*/99, /*target=*/2, cfg)); - REQUIRE(ep0.read_whole_map().generic_configs[1].request_queue_size == 8); + REQUIRE(ep0.read_whole_map().generic_configs[1].ep_tx_buffer_size == 8); } TEST_CASE("Writing to an out-of-range endpoint id is rejected as INVALID_PARAMETER", "[regmap][REQ-REGMAP-005]") { @@ -242,19 +278,54 @@ TEST_CASE("Functional config remains writable at HW_CONFIGURED but locks at RCP_ REQUIRE(ec == make_error_code(RegMapErrc::locked_mem_access)); } -// ── General bootstrap register fields ─────────────────────────────────────────── +// ── General bootstrap register fields (GeneralMap) ────────────────────────────── + +TEST_CASE("GeneralMap default-constructs zeroed, with the no-root-client sentinel", + "[regmap][REQ-RMAP-003]") { + GeneralMap map; + + REQUIRE(map.magic == kRegisterMapMagic); + REQUIRE(map.svr_version == 0); + REQUIRE(map.vendor_id == 0); + REQUIRE(map.device_id == 0); + REQUIRE(map.svr_ep_count == 0); + REQUIRE(map.svr_req_stream_max == 0); + REQUIRE(map.svr_responder_streams_max == 0); + REQUIRE(map.svr_sequencers_max == 0); + REQUIRE(map.svr_configuration_lock == 0); + REQUIRE(map.svr_responder_mem_size == 0); + REQUIRE(map.svr_req_mem_size == 0); + REQUIRE(map.svr_implemented_options == 0); + REQUIRE(map.svr_root_client_index == kNoRootClient); + REQUIRE(map.svr_hw_cfg_ptr == 0); + REQUIRE(map.svr_request_stream_cfg_capacity == 0); + REQUIRE(map.svr_response_stream_cfg_capacity == 0); + REQUIRE(map.svr_ep_generic_cfg_ptr == 0); + REQUIRE(map.svr_ep_generic_cfg_capacity == 0); + REQUIRE(map.svr_ep_functional_cfg_ptr == 0); + REQUIRE(map.svr_ep_bytebus_id_map_ptr == 0); + REQUIRE(map.svr_ep_bytebus_id_map_capacity == 0); + REQUIRE(map.svr_sequencer_state_ptr == 0); + REQUIRE(map.svr_network_interface_cfg_ptr == 0); + REQUIRE(map.svr_network_interface_cfg_capacity == 0); + REQUIRE(map.svr_physical_layer_cfg_ptr == 0); + REQUIRE(map.svr_physical_layer_cfg_capacity == 0); + REQUIRE(map.svr_time_synch_cfg_ptr == 0); + REQUIRE(map.svr_time_synch_cfg_capacity == 0); + REQUIRE(map.svr_security_cfg_ptr == 0); + REQUIRE(map.svr_security_cfg_capacity == 0); + REQUIRE(map.svr_device_specific_cfg_ptr == 0); + REQUIRE(map.svr_device_specific_cfg_capacity == 0); +} 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.general.magic = kRegisterMapMagic; + m.general.svr_version = 0x00010000; + m.general.vendor_id = 0x00AB; + m.general.device_id = 0x00CD; + m.general.svr_ep_count = 4; + m.general.svr_implemented_options = kOptCompoundWait | kOptChained; m.hw_pin_map_table = {0x100, 32}; m.request_stream_table = {0x200, 8}; @@ -262,73 +333,288 @@ TEST_CASE("RegisterMap bootstrap fields hold the values assigned to them", "[reg 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.general.magic == kRegisterMapMagic); + REQUIRE(m.general.vendor_id == 0x00AB); + REQUIRE(m.general.device_id == 0x00CD); + REQUIRE(m.general.svr_ep_count == 4); + REQUIRE((m.general.svr_implemented_options & kOptCompoundWait) != 0); + REQUIRE((m.general.svr_implemented_options & kOptTrigger) == 0); REQUIRE(m.hw_pin_map_table.capacity == 32); REQUIRE(m.functional_config_table.offset == 0x400); } -// ── HW pin-mapping config ─────────────────────────────────────────────────────── +// ── svr_implemented_options: five independent bits (REQ-RMAP-030) ─────────────── -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: the full v2.6.0 watchdog/safe-state field set ──────── - -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_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("The five REQ-RMAP-030 option bits are pairwise distinct", "[regmap][REQ-RMAP-030]") { + const uint8_t bits[5] = {kOptCompoundWait, kOptTrigger, kOptChained, kOptTimeSync, kOptEnhCancel}; + for (size_t i = 0; i < 5; ++i) { + REQUIRE(bits[i] != 0); + for (size_t j = 0; j < i; ++j) REQUIRE(bits[i] != bits[j]); + } +} + +TEST_CASE("Each option bit is independently settable with no sibling requirement", + "[regmap][REQ-RMAP-030]") { + GeneralMap map; + + map.svr_implemented_options = kOptTrigger; // just one bit -- no sibling needed + REQUIRE(map.svr_implemented_options == kOptTrigger); + + map.svr_implemented_options = static_cast(kOptCompoundWait | kOptChained); + REQUIRE(map.svr_implemented_options == static_cast(kOptCompoundWait | kOptChained)); + + map.svr_implemented_options = + static_cast(kOptCompoundWait | kOptTrigger | kOptChained | kOptTimeSync | kOptEnhCancel); + REQUIRE(map.svr_implemented_options == 0x1Fu); // all five bits, 0b00011111 +} + +// ── GeneralMap Table 20 wire codec (REQ-RMAP-024/025) ──────────────────────────── + +TEST_CASE("GeneralMap render() places each field at its own TC18-cited absolute address", + "[regmap][REQ-RMAP-024]") { + GeneralMap map; + map.magic = 0x11223344; + map.svr_version = 0x00010203; + map.vendor_id = 0xAABB; + map.device_id = 0xCCDD; + map.svr_ep_count = 0x0005; + map.svr_req_stream_max = 0x06; + map.svr_responder_streams_max = 0x07; + map.svr_responder_mem_size = 0x0809; + map.svr_req_mem_size = 0x0A0B; + map.svr_sequencers_max = 0x0C; + map.svr_configuration_lock = 0x00; + map.svr_implemented_options = kOptCompoundWait; + map.svr_io_pin_count = 0x0D0E; + + const auto image = render(map); + REQUIRE(image.size() == kGeneralMapLen); + REQUIRE(image[0x0000] == 0x11); + REQUIRE(image[0x0003] == 0x44); + REQUIRE(image[0x0008] == 0xAA); + REQUIRE(image[0x0009] == 0xBB); + REQUIRE(image[0x000A] == 0xCC); + REQUIRE(image[0x000C] == 0x00); + REQUIRE(image[0x000D] == 0x05); + REQUIRE(image[0x000E] == 0x06); + REQUIRE(image[0x000F] == 0x07); + REQUIRE(image[0x0016] == kOptCompoundWait); + REQUIRE(image[0x0018] == 0x0D); + REQUIRE(image[0x0019] == 0x0E); +} + +TEST_CASE("GeneralMap render() never places svr_lifecycle_state or svr_root_client_index on the wire", + "[regmap][REQ-RMAP-023]") { + GeneralMap map; + map.svr_lifecycle_state = 0xFF; // no genuine Table 20 address exists for this field + map.svr_root_client_index = 42; + + const auto image = render(map); + // 0x000E/0x000F are svr_req_stream_max/svr_responder_streams_max, both + // left at their own zero default -- neither excluded field leaks in. + REQUIRE(image[0x000E] == 0x00); + REQUIRE(image[0x000F] == 0x00); +} + +TEST_CASE("encode_read_response then decode_read_response round-trips GeneralMap's Table 20 fields", + "[regmap][REQ-RMAP-024]") { + GeneralMap map; + map.magic = 0xDEADBEEF; + map.svr_version = 0x00020000; + map.vendor_id = 0x1234; + map.device_id = 0x5678; + map.svr_ep_count = 3; + map.svr_req_stream_max = 4; + map.svr_implemented_options = kOptTrigger; + map.svr_device_specific_cfg_capacity = 0x0010; + + const auto encoded = encode_read_response(map, static_cast(kGeneralMapLen), /*transaction_num=*/7); + + GeneralMap decoded; + auto ec = decode_read_response(encoded.data(), encoded.size(), decoded); + REQUIRE_FALSE(ec); + REQUIRE(decoded.magic == map.magic); + REQUIRE(decoded.svr_version == map.svr_version); + REQUIRE(decoded.vendor_id == map.vendor_id); + REQUIRE(decoded.device_id == map.device_id); + REQUIRE(decoded.svr_ep_count == map.svr_ep_count); + REQUIRE(decoded.svr_req_stream_max == map.svr_req_stream_max); + REQUIRE(decoded.svr_implemented_options == map.svr_implemented_options); + REQUIRE(decoded.svr_device_specific_cfg_capacity == map.svr_device_specific_cfg_capacity); + // Excluded from the wire image -- stays at decoded's own default, not map's. + REQUIRE(decoded.svr_root_client_index == kNoRootClient); +} + +TEST_CASE("decode_read_response rejects a non-EP0 byte_bus_id, a write op, and a too-short frame", + "[regmap][REQ-RMAP-024]") { + GeneralMap map; + const auto encoded = encode_read_response(map, static_cast(kGeneralMapLen), 1); + + GeneralMap decoded; + REQUIRE_FALSE(decode_read_response(encoded.data(), encoded.size(), decoded)); + + std::vector too_short(encoded.begin(), encoded.begin() + 2); + auto ec_short = decode_read_response(too_short.data(), too_short.size(), decoded); + REQUIRE(ec_short == make_error_code(GeneralMapErrc::short_frame)); +} + +TEST_CASE("decode_write_request always reports LOCKED_MEM_ACCESS -- Table 20 is entirely read-only", + "[regmap][REQ-RMAP-025]") { + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = static_cast(kEp0); + hdr.op = true; // write + hdr.transaction_num = 42; + const auto encoded = acf::encode_acf_abb(hdr, {0x00, 0x01, 0x02}); + + acf::WireErrorCode out_error{}; + uint8_t out_txn = 0; + auto ec = decode_write_request(encoded.data(), encoded.size(), out_error, out_txn); + REQUIRE_FALSE(ec); + REQUIRE(out_error == acf::WireErrorCode::LockedMemAccess); + REQUIRE(out_txn == 42); +} + +TEST_CASE("decode_write_request rejects a read op (wrong_op) and a non-EP0 bus (wrong_bus)", + "[regmap][REQ-RMAP-025]") { + acf::AcfMessageInfo read_hdr; + read_hdr.byte_bus_id = static_cast(kEp0); + read_hdr.op = false; // read, not write + const auto read_encoded = acf::encode_acf_abb(read_hdr, {}); + + acf::WireErrorCode out_error{}; + uint8_t out_txn = 0; + REQUIRE(decode_write_request(read_encoded.data(), read_encoded.size(), out_error, out_txn) == + make_error_code(GeneralMapErrc::wrong_op)); + + acf::AcfMessageInfo wrong_bus_hdr; + wrong_bus_hdr.byte_bus_id = static_cast(kEp0) + 1; + wrong_bus_hdr.op = true; + const auto wrong_bus_encoded = acf::encode_acf_abb(wrong_bus_hdr, {}); + REQUIRE(decode_write_request(wrong_bus_encoded.data(), wrong_bus_encoded.size(), out_error, out_txn) == + make_error_code(GeneralMapErrc::wrong_bus)); +} + +// ── Root-client / per-EP-restricted-client model: writer_ctx() ─────────────────── + +TEST_CASE("writer_ctx grants via_root_client_ep0 only via EP0 on the exact root-client stream", + "[regmap][REQ-RMAP-009]") { + GeneralMap map; + map.svr_root_client_index = 7; + + auto ctx = writer_ctx(map, nullptr, 7, /*via_ep0=*/true, /*via_unicast=*/true, false, 0, nullptr, 0); + REQUIRE(ctx.via_root_client_ep0); + REQUIRE_FALSE(ctx.via_owning_stream); + + REQUIRE_FALSE(writer_ctx(map, nullptr, 8, true, true, false, 0, nullptr, 0).via_root_client_ep0); + REQUIRE_FALSE(writer_ctx(map, nullptr, 7, false, true, false, 0, nullptr, 0).via_root_client_ep0); +} + +TEST_CASE("writer_ctx denies via_root_client_ep0 when no root client is configured", + "[regmap][REQ-RMAP-009]") { + GeneralMap map; // svr_root_client_index == kNoRootClient + auto ctx = writer_ctx(map, nullptr, kNoRootClient, true, true, false, 0, nullptr, 0); + REQUIRE_FALSE(ctx.via_root_client_ep0); +} + +TEST_CASE("writer_ctx grants via_owning_stream only for the matching stream index", + "[regmap][REQ-RMAP-010]") { + GeneralMap map; + EpClient owner; + owner.has_owning_stream = true; + owner.owning_stream_index = 3; + + auto ctx = writer_ctx(map, &owner, 3, false, true, false, 0, nullptr, 0); + REQUIRE(ctx.via_owning_stream); + REQUIRE_FALSE(ctx.via_root_client_ep0); + + REQUIRE_FALSE(writer_ctx(map, nullptr, 3, false, true, false, 0, nullptr, 0).via_owning_stream); + + EpClient no_owner; + no_owner.has_owning_stream = false; + no_owner.owning_stream_index = 3; + REQUIRE_FALSE(writer_ctx(map, &no_owner, 3, false, true, false, 0, nullptr, 0).via_owning_stream); + + REQUIRE_FALSE(writer_ctx(map, &owner, 4, false, true, false, 0, nullptr, 0).via_owning_stream); +} + +TEST_CASE("writer_ctx plumbs via_unicast to via_non_unicast_frame independent of root-client status", + "[regmap][REQ-LIFECYCLE-027]") { + GeneralMap map; + map.svr_root_client_index = 7; + + auto ctx_unicast = writer_ctx(map, nullptr, 7, true, true, false, 0, nullptr, 0); + REQUIRE_FALSE(ctx_unicast.via_non_unicast_frame); + REQUIRE(ctx_unicast.via_root_client_ep0); + + auto ctx_non_unicast = writer_ctx(map, nullptr, 7, true, false, false, 0, nullptr, 0); + REQUIRE(ctx_non_unicast.via_non_unicast_frame); + REQUIRE(ctx_non_unicast.via_root_client_ep0); // independent axis +} + +TEST_CASE("writer_ctx plumbs via_discovery_stream straight through", "[regmap][REQ-RMAP-070]") { + GeneralMap map; // no root client, no owning stream + + auto ctx = writer_ctx(map, nullptr, 3, false, true, true, 0, nullptr, 0); + REQUIRE(ctx.via_discovery_stream); + REQUIRE_FALSE(ctx.via_root_client_ep0); + REQUIRE_FALSE(ctx.via_owning_stream); + + REQUIRE_FALSE(writer_ctx(map, nullptr, 3, false, true, false, 0, nullptr, 0).via_discovery_stream); +} + +// TODO(phase4-batch-b): c-RCP's own writer_ctx() grants +// via_valid_stream_association for a real, matching EP-ID/byte_bus_id +// association when no root client is configured (REQ-LIFECYCLE-025/031). +// This port's own writer_ctx() cannot yet evaluate that (see its own doc +// comment) and always returns false -- pinned here as the current, +// documented, fail-closed behavior rather than left unverified. +TEST_CASE("writer_ctx's via_valid_stream_association is fail-closed (always false) pending batch B", + "[regmap][REQ-LIFECYCLE-025]") { + GeneralMap map; // no root client configured + EpIdMappingEntry entries[1] = {{1, 0x100}}; + + auto ctx = writer_ctx(map, nullptr, 2, false, true, false, 0x100, entries, 1); + REQUIRE_FALSE(ctx.via_valid_stream_association); +} + +TEST_CASE("writer_ctx explicitly assigns every member of the returned WriterCtx", "[regmap][REQ-RMAP-086]") { + GeneralMap map; + map.svr_root_client_index = 7; + EpClient owner; + owner.has_owning_stream = true; + owner.owning_stream_index = 7; + + auto ctx = writer_ctx(map, &owner, 7, true, false, true, 0x100, nullptr, 0); + REQUIRE(ctx.via_root_client_ep0); + REQUIRE(ctx.via_owning_stream); + REQUIRE(ctx.via_non_unicast_frame); + REQUIRE(ctx.via_discovery_stream); + REQUIRE_FALSE(ctx.via_valid_stream_association); + + GeneralMap no_root; // svr_root_client_index == kNoRootClient + auto ctx2 = writer_ctx(no_root, nullptr, 2, false, true, false, 0x100, nullptr, 0); + REQUIRE_FALSE(ctx2.via_root_client_ep0); + REQUIRE_FALSE(ctx2.via_owning_stream); + REQUIRE_FALSE(ctx2.via_non_unicast_frame); + REQUIRE_FALSE(ctx2.via_discovery_stream); + REQUIRE_FALSE(ctx2.via_valid_stream_association); +} + +// ── RC Server's own functional-configuration content (SvrEpCfg) ────────────────── + +TEST_CASE("SvrEpCfg default-constructs with TC18's own stated discovery-timeout default", + "[regmap][REQ-RMAP-066]") { + SvrEpCfg cfg; + REQUIRE(cfg.svr_discovery_timeout == 20000); + REQUIRE(cfg.svr_ep_status == 0); + + cfg.svr_discovery_timeout = 5000; + cfg.svr_ep_status = 0x0001; + REQUIRE(cfg.svr_discovery_timeout == 5000); + REQUIRE(cfg.svr_ep_status == 0x0001); +} + +// ── EndpointGenericConfig: per-endpoint E2E CRC safe-mode toggles (pre-existing) ─ TEST_CASE("EndpointGenericConfig's ep_*_crc_enable toggles default false and are independently settable", "[regmap][REQ-REGMAP-015]") { @@ -343,7 +629,243 @@ TEST_CASE("EndpointGenericConfig's ep_*_crc_enable toggles default false and are REQUIRE_FALSE(cfg.ep_response_crc_enable); } -// ── EP-ID / byte_bus_id mapping table ──────────────────────────────────────────── +// ── ep_generic_cfg boundary conversions (REQ-RMAP-076/077) ─────────────────────── + +TEST_CASE("ep_delay_time_us_to_reg accepts exactly TC18's own four allowed values", "[regmap][REQ-RMAP-076]") { + uint8_t reg = 0xFF; + REQUIRE(ep_generic_cfg::ep_delay_time_us_to_reg(1, reg)); + REQUIRE(reg == 0); + REQUIRE(ep_generic_cfg::ep_delay_time_us_to_reg(10, reg)); + REQUIRE(reg == 1); + REQUIRE(ep_generic_cfg::ep_delay_time_us_to_reg(20, reg)); + REQUIRE(reg == 2); + REQUIRE(ep_generic_cfg::ep_delay_time_us_to_reg(50, reg)); + REQUIRE(reg == 3); +} + +TEST_CASE("ep_delay_time_us_to_reg rejects (not rounds) every other microsecond value", + "[regmap][REQ-RMAP-076]") { + uint8_t reg = 0xFF; + REQUIRE_FALSE(ep_generic_cfg::ep_delay_time_us_to_reg(0, reg)); + REQUIRE_FALSE(ep_generic_cfg::ep_delay_time_us_to_reg(15, reg)); + REQUIRE_FALSE(ep_generic_cfg::ep_delay_time_us_to_reg(999999, reg)); + REQUIRE(reg == 0xFF); // untouched on rejection +} + +TEST_CASE("ep_delay_time_reg_to_us covers all 4 register values and masks out-of-range input", + "[regmap][REQ-RMAP-076]") { + REQUIRE(ep_generic_cfg::ep_delay_time_reg_to_us(0) == 1); + REQUIRE(ep_generic_cfg::ep_delay_time_reg_to_us(1) == 10); + REQUIRE(ep_generic_cfg::ep_delay_time_reg_to_us(2) == 20); + REQUIRE(ep_generic_cfg::ep_delay_time_reg_to_us(3) == 50); + // Masked to 2 bits: 0b100 == 4 -> masked to 0. + REQUIRE(ep_generic_cfg::ep_delay_time_reg_to_us(4) == 1); +} + +TEST_CASE("ep_req_storage_size_words_to_octets is always exact", "[regmap][REQ-RMAP-077]") { + REQUIRE(ep_generic_cfg::ep_req_storage_size_words_to_octets(0) == 0); + REQUIRE(ep_generic_cfg::ep_req_storage_size_words_to_octets(2) == 8); + REQUIRE(ep_generic_cfg::ep_req_storage_size_words_to_octets(0xFFFF) == 262140); +} + +TEST_CASE("ep_req_storage_size_octets_to_words round-trips and rejects non-multiples-of-4/overflow", + "[regmap][REQ-RMAP-077]") { + uint16_t words = 0xFFFF; + REQUIRE(ep_generic_cfg::ep_req_storage_size_octets_to_words(8, words)); + REQUIRE(words == 2); + + REQUIRE_FALSE(ep_generic_cfg::ep_req_storage_size_octets_to_words(9, words)); // not a multiple of 4 + REQUIRE(words == 2); // untouched on rejection + + REQUIRE_FALSE(ep_generic_cfg::ep_req_storage_size_octets_to_words(262144, words)); // one word past max +} + +// ── ep_generic_cfg wire codec, READ side (REQ-RMAP-078/081) ────────────────────── + +TEST_CASE("ep_generic_cfg::render places each field at its own TC18-cited byte offset", + "[regmap][REQ-RMAP-078]") { + EndpointGenericConfig row; + row.ep_type = 0x03; // SPI + row.ep_used = true; + row.ep_delay_time = 20; // register value 2 (10b) + row.ep_req_storage_size = 8; // 2 words + row.ep_description = 0x11223344; + row.ep_tx_buffer_size = 0x5566; + row.ep_rx_buffer_size = 0x7788; + + uint8_t out[12]; + ep_generic_cfg::render(&row, 1, out); + + REQUIRE(out[0] == 0x03); + REQUIRE(out[1] == 0x21); // ep_used=1 (bit0) | delay_reg=2 (bits4:5) + REQUIRE(out[2] == 0x00); + REQUIRE(out[3] == 0x02); + REQUIRE(out[4] == 0x11); + REQUIRE(out[5] == 0x22); + REQUIRE(out[6] == 0x33); + REQUIRE(out[7] == 0x44); + REQUIRE(out[8] == 0x55); + REQUIRE(out[9] == 0x66); + REQUIRE(out[10] == 0x77); + REQUIRE(out[11] == 0x88); +} + +TEST_CASE("ep_generic_cfg::render uses a 12-octet stride across entries", "[regmap][REQ-RMAP-078]") { + EndpointGenericConfig rows[2]; + rows[0].ep_type = 0xAA; + rows[1].ep_type = 0xBB; + + uint8_t out[24]; + ep_generic_cfg::render(rows, 2, out); + + REQUIRE(out[0] == 0xAA); + REQUIRE(out[12] == 0xBB); +} + +TEST_CASE("ep_generic_cfg::render falls back to 1us for an unconfigured or disallowed delay time", + "[regmap][REQ-RMAP-078]") { + EndpointGenericConfig row; // ep_delay_time defaults to 0us -- not one of the 4 allowed values + uint8_t out[12]; + ep_generic_cfg::render(&row, 1, out); + REQUIRE((out[1] & 0x30u) == 0x00); // falls back to register 0 (1us) + + row.ep_delay_time = 999999; + ep_generic_cfg::render(&row, 1, out); + REQUIRE((out[1] & 0x30u) == 0x00); +} + +TEST_CASE("ep_generic_cfg::render clamps an oversized or non-multiple-of-4 req_storage_size", + "[regmap][REQ-RMAP-078]") { + EndpointGenericConfig row; + row.ep_req_storage_size = 262144; // one word past the register's own max + uint8_t out[12]; + ep_generic_cfg::render(&row, 1, out); + REQUIRE(out[2] == 0xFF); + REQUIRE(out[3] == 0xFF); // clamped to max, not wrapped + + row.ep_req_storage_size = 9; // not an exact multiple of 4 -- floors to 2 words + ep_generic_cfg::render(&row, 1, out); + REQUIRE(out[2] == 0x00); + REQUIRE(out[3] == 0x02); +} + +// REQ-RMAP-081 (issue #467): TC18's own prose names a configuration +// parameter (EP_RESP_ON_ERROR) that Table 31 itself never actually defines +// -- see c-RCP's own investigation, ported into this header's own +// EndpointGenericConfig doc comment. This test pins that render() invents +// no bit for it: both reserved spans of octet 1 (bits [3:1] and [7:6], mask +// 0xCE) stay zero for every input, including inputs deliberately chosen to +// be non-zero/extreme everywhere else. +TEST_CASE("ep_generic_cfg::render never sets a bit for the dangling EP_RESP_ON_ERROR reference", + "[regmap][REQ-RMAP-081]") { + EndpointGenericConfig row; + row.ep_type = 0xFF; + row.ep_used = true; + row.ep_delay_time = 50; + row.ep_req_storage_size = 0xFFFFFFFF; + row.ep_description = 0xFFFFFFFF; + row.ep_tx_buffer_size = 0xFFFF; + row.ep_rx_buffer_size = 0xFFFF; + + uint8_t out[12]; + ep_generic_cfg::render(&row, 1, out); + REQUIRE((out[1] & 0xCEu) == 0x00); +} + +// ── ep_generic_cfg wire codec, WRITE side (REQ-RMAP-079/087) ───────────────────── + +TEST_CASE("ep_generic_cfg::apply_reconfig patches only the addressed octets", "[regmap][REQ-RMAP-079]") { + EndpointGenericConfig rows[2]; + rows[0].ep_type = 0xAA; // must survive -- read-only + + const uint8_t patch[12] = { + 0xFF, // ep_type -- must have no effect + 0x21, // ep_used=1, delay_reg=2 (20us) + 0x00, 0x02, // ep_req_storage_size: 2 words = 8 octets + 0x11, 0x22, 0x33, 0x44, // ep_description + 0x55, 0x66, // ep_tx_buffer_size + 0x77, 0x88, // ep_rx_buffer_size + }; + + auto ec = ep_generic_cfg::apply_reconfig(rows, 2, 0, patch, sizeof(patch)); + REQUIRE_FALSE(ec); + REQUIRE(rows[0].ep_type == 0xAA); // unchanged -- read-only + REQUIRE(rows[0].ep_used); + REQUIRE(rows[0].ep_delay_time == 20); + REQUIRE(rows[0].ep_req_storage_size == 8); + REQUIRE(rows[0].ep_description == 0x11223344); + REQUIRE(rows[0].ep_tx_buffer_size == 0x5566); + REQUIRE(rows[0].ep_rx_buffer_size == 0x7788); + // row 1 (relative 12-23) entirely untouched by this write. + REQUIRE(rows[1].ep_type == 0); + REQUIRE_FALSE(rows[1].ep_used); +} + +TEST_CASE("ep_generic_cfg::apply_reconfig touching only ep_type is a no-op, confirmed normally (OK)", + "[regmap][REQ-RMAP-079]") { + EndpointGenericConfig row; + row.ep_type = 0x03; + const uint8_t patch[1] = {0xFF}; + + // TC18 §13.7.1.2: "Writing data to read only registers has no effect + // and request is confirmed normally" -- OK, not an error. + REQUIRE_FALSE(ep_generic_cfg::apply_reconfig(&row, 1, 0, patch, sizeof(patch))); + REQUIRE(row.ep_type == 0x03); +} + +TEST_CASE("ep_generic_cfg::apply_reconfig forces row 0's ep_used true, honors it normally elsewhere", + "[regmap][REQ-RMAP-087]") { + EndpointGenericConfig row; + row.ep_used = true; // EP0's own required-always-on state + const uint8_t patch[1] = {0x00}; // ep_used=0, delay_reg=0 -- targets only octet 0x0001 + + REQUIRE_FALSE(ep_generic_cfg::apply_reconfig(&row, 1, 1, patch, sizeof(patch))); + REQUIRE(row.ep_used); // forced -- incoming 0 bit has no effect on row 0 + REQUIRE(row.ep_delay_time == 1); // ep_delay_time is NOT part of the override +} + +TEST_CASE("ep_generic_cfg::apply_reconfig's row-0-only ep_used override does not leak to EP1", + "[regmap][REQ-RMAP-087]") { + EndpointGenericConfig rows[2]; + rows[0].ep_used = true; + rows[1].ep_used = true; + + const uint8_t clear_row0[1] = {0x00}; + REQUIRE_FALSE(ep_generic_cfg::apply_reconfig(rows, 2, 1, clear_row0, 1)); + REQUIRE(rows[0].ep_used); // row 0: forced, write ignored + + const uint8_t clear_row1[1] = {0x00}; + REQUIRE_FALSE(ep_generic_cfg::apply_reconfig(rows, 2, 13, clear_row1, 1)); + REQUIRE_FALSE(rows[1].ep_used); // row 1: honored normally + + rows[1].ep_used = false; + const uint8_t set_row1[1] = {0x01}; + REQUIRE_FALSE(ep_generic_cfg::apply_reconfig(rows, 2, 13, set_row1, 1)); + REQUIRE(rows[1].ep_used); // setting works normally too, not just clearing +} + +TEST_CASE("ep_generic_cfg::apply_reconfig leaves a partially-covered multi-octet field unchanged", + "[regmap][REQ-RMAP-079]") { + EndpointGenericConfig row; + row.ep_req_storage_size = 40; // pre-existing value + const uint8_t patch[1] = {0x99}; // only byte 0 of the 2-byte field at relative 0x0002-0x0003 + + REQUIRE_FALSE(ep_generic_cfg::apply_reconfig(&row, 1, 2, patch, sizeof(patch))); + REQUIRE(row.ep_req_storage_size == 40); // unchanged -- not corrupted by a half-write +} + +TEST_CASE("ep_generic_cfg::apply_reconfig rejects a zero-length write and an out-of-range write", + "[regmap][REQ-RMAP-079]") { + EndpointGenericConfig row; + const uint8_t patch[1] = {0x00}; + + REQUIRE(ep_generic_cfg::apply_reconfig(&row, 1, 0, patch, 0) == + make_error_code(EpGenericCfgReconfigErrc::short_write)); + REQUIRE(ep_generic_cfg::apply_reconfig(&row, 1, 12, patch, 1) == + make_error_code(EpGenericCfgReconfigErrc::out_of_range)); +} + +// ── EP-ID / byte_bus_id mapping table (batch B, pre-existing) ──────────────────── TEST_CASE("EP-ID mapping table preserves client insertion order without re-sorting it", "[regmap][REQ-REGMAP-010]") { @@ -359,7 +881,7 @@ TEST_CASE("EP-ID mapping table preserves client insertion order without re-sorti REQUIRE(m.ep_id_mapping[2].ep_id == 2); } -// ── Response / ack queue config ────────────────────────────────────────────────── +// ── Response / ack queue config (batch B, pre-existing) ────────────────────────── TEST_CASE("ResponseQueueConfig fields exist and are settable", "[regmap][REQ-REGMAP-011]") { ResponseQueueConfig rqc; @@ -369,7 +891,7 @@ TEST_CASE("ResponseQueueConfig fields exist and are settable", "[regmap][REQ-REG REQUIRE(rqc.ack_queue_size == 6); } -// ── Sequencer-state persistence ────────────────────────────────────────────────── +// ── Sequencer-state persistence (batch B, pre-existing) ────────────────────────── TEST_CASE("Sequencer-state registers persist independent 8-bit values", "[regmap][REQ-REGMAP-012]") { RegisterMap m; @@ -406,16 +928,16 @@ TEST_CASE("Assigning an owner to EP0 itself is rejected as INVALID_PARAMETER", " // ── Size-invariant enforcement (cpp-RCP-N2-01 / cpp-RCP-N2-02, issues #64/#65) ── // Ep0's access checks (check_read_access/check_write_access/set_endpoint_owner) // all bound a target endpoint against endpoint_owner_, which is sized from -// endpoint_count. The actual writes land in regs_.generic_configs / +// general.svr_ep_count. The actual writes land in regs_.generic_configs / // regs_.functional_configs instead. These tests confirm a RegisterMap whose -// endpoint_count disagrees with those vectors' lengths is rejected/fails -// closed rather than letting an in-range access check be followed by an -// out-of-bounds write. +// general.svr_ep_count disagrees with those vectors' lengths is rejected/ +// fails closed rather than letting an in-range access check be followed by +// an out-of-bounds write. -TEST_CASE("A RegisterMap whose config vectors disagree with endpoint_count fails closed instead of allowing OOB access", +TEST_CASE("A RegisterMap whose config vectors disagree with svr_ep_count fails closed instead of allowing OOB access", "[regmap][REQ-REGMAP-013]") { RegisterMap m; - m.endpoint_count = 4; + m.general.svr_ep_count = 4; // generic_configs / functional_configs deliberately left empty/default // -- exactly the mismatch issue #64 describes. ServerLifecycle lc; @@ -426,7 +948,7 @@ TEST_CASE("A RegisterMap whose config vectors disagree with endpoint_count fails REQUIRE_FALSE(ep0.claim_root_client(1)); // Every non-EP0 access check must fail closed (INVALID_PARAMETER), not - // report a target in [1, endpoint_count] as valid. + // report a target in [1, svr_ep_count] as valid. auto ec_owner = ep0.set_endpoint_owner(1, 1); REQUIRE(ec_owner); REQUIRE(ec_owner == make_error_code(RegMapErrc::invalid_parameter)); @@ -453,7 +975,7 @@ TEST_CASE("A RegisterMap whose config vectors disagree with endpoint_count fails TEST_CASE("A RegisterMap with a smaller mismatch (nonzero but undersized config vectors) also fails closed", "[regmap][REQ-REGMAP-013]") { RegisterMap m; - m.endpoint_count = 4; + m.general.svr_ep_count = 4; m.generic_configs.resize(1); m.functional_configs.resize(1); ServerLifecycle lc; @@ -463,7 +985,7 @@ TEST_CASE("A RegisterMap with a smaller mismatch (nonzero but undersized config REQUIRE_FALSE(ep0.claim_root_client(1)); EndpointGenericConfig cfg; - // target=4 would be in-range against endpoint_count (and against the + // target=4 would be in-range against svr_ep_count (and against the // pre-fix endpoint_owner_.size()==4), but generic_configs only has 1 // slot -- this must not be allowed through. auto ec = ep0.write_generic_config(1, /*target=*/4, cfg); @@ -471,7 +993,7 @@ TEST_CASE("A RegisterMap with a smaller mismatch (nonzero but undersized config REQUIRE(ec == make_error_code(RegMapErrc::invalid_parameter)); } -TEST_CASE("write_whole_map rejects a replacement map whose config vectors disagree with its own endpoint_count", +TEST_CASE("write_whole_map rejects a replacement map whose config vectors disagree with its own svr_ep_count", "[regmap][REQ-REGMAP-013]") { auto map = make_map(2); ServerLifecycle lc; @@ -479,7 +1001,7 @@ TEST_CASE("write_whole_map rejects a replacement map whose config vectors disagr REQUIRE_FALSE(ep0.claim_root_client(1)); RegisterMap bad; - bad.endpoint_count = 3; + bad.general.svr_ep_count = 3; bad.generic_configs.resize(1); // mismatched bad.functional_configs.resize(3); @@ -488,11 +1010,11 @@ TEST_CASE("write_whole_map rejects a replacement map whose config vectors disagr REQUIRE(ec == make_error_code(RegMapErrc::invalid_parameter)); // Rejected write must leave the live map untouched. - REQUIRE(ep0.read_whole_map().endpoint_count == 2); + REQUIRE(ep0.read_whole_map().general.svr_ep_count == 2); REQUIRE(ep0.is_endpoint_table_consistent()); } -TEST_CASE("write_whole_map with a valid replacement map resizes endpoint_owner_ to match the new endpoint_count", +TEST_CASE("write_whole_map with a valid replacement map resizes endpoint_owner_ to match the new svr_ep_count", "[regmap][REQ-REGMAP-005]") { auto map = make_map(2); ServerLifecycle lc; @@ -519,8 +1041,9 @@ TEST_CASE("write_whole_map with a valid replacement map resizes endpoint_owner_ // The root client can write any endpoint in the new, larger range, // including endpoint 4, which did not exist under the old map. + cfg.ep_tx_buffer_size = 99; REQUIRE_FALSE(ep0.write_generic_config(1, /*target=*/4, cfg)); - REQUIRE(ep0.read_whole_map().generic_configs[3].request_queue_size == cfg.request_queue_size); + REQUIRE(ep0.read_whole_map().generic_configs[3].ep_tx_buffer_size == cfg.ep_tx_buffer_size); // Shrink: install a 1-endpoint replacement. endpoint 4 must no longer // be accepted as a write target -- this is exactly the stale-bounds @@ -535,7 +1058,7 @@ TEST_CASE("write_whole_map with a valid replacement map resizes endpoint_owner_ REQUIRE(ec_oob == make_error_code(RegMapErrc::invalid_parameter)); } -// ── The four mandatory error codes are distinct ───────────────────────────────── +// ── Error taxonomies are distinct ──────────────────────────────────────────────── TEST_CASE("The four mandatory register-map error codes are distinct values in their own category", "[regmap][REQ-REGMAP-014]") { @@ -556,3 +1079,27 @@ TEST_CASE("The four mandatory register-map error codes are distinct values in th REQUIRE_FALSE(rejected.message().empty()); REQUIRE_FALSE(invalid.message().empty()); } + +TEST_CASE("GeneralMapErrc values are distinct and carry non-empty messages", "[regmap][REQ-RMAP-024]") { + auto short_frame = make_error_code(GeneralMapErrc::short_frame); + auto bad_msg_type = make_error_code(GeneralMapErrc::bad_msg_type); + auto wrong_bus = make_error_code(GeneralMapErrc::wrong_bus); + auto wrong_op = make_error_code(GeneralMapErrc::wrong_op); + + REQUIRE(short_frame.category() == general_map_category()); + REQUIRE(short_frame != bad_msg_type); + REQUIRE(bad_msg_type != wrong_bus); + REQUIRE(wrong_bus != wrong_op); + REQUIRE_FALSE(short_frame.message().empty()); + REQUIRE_FALSE(wrong_op.message().empty()); +} + +TEST_CASE("EpGenericCfgReconfigErrc values are distinct and carry non-empty messages", "[regmap][REQ-RMAP-079]") { + auto short_write = make_error_code(EpGenericCfgReconfigErrc::short_write); + auto out_of_range = make_error_code(EpGenericCfgReconfigErrc::out_of_range); + + REQUIRE(short_write.category() == ep_generic_cfg_reconfig_category()); + REQUIRE(short_write != out_of_range); + REQUIRE_FALSE(short_write.message().empty()); + REQUIRE_FALSE(out_of_range.message().empty()); +}