diff --git a/include/rcp/regmap.hpp b/include/rcp/regmap.hpp index a84336f..292b571 100644 --- a/include/rcp/regmap.hpp +++ b/include/rcp/regmap.hpp @@ -53,6 +53,44 @@ // fusa:req REQ-RMAP-081 // fusa:req REQ-RMAP-086 // fusa:req REQ-RMAP-087 +// +// c-RCP-derived content ported in Phase 4 batch B (this same issue/roadmap +// entry — see this file's own "Phase 4 batch B" banner below): +// fusa:req REQ-RMAP-017 +// fusa:req REQ-RMAP-040 +// fusa:req REQ-RMAP-041 +// fusa:req REQ-RMAP-042 +// fusa:req REQ-RMAP-043 +// fusa:req REQ-RMAP-044 +// fusa:req REQ-RMAP-045 +// fusa:req REQ-RMAP-047 +// fusa:req REQ-RMAP-048 +// fusa:req REQ-RMAP-049 +// fusa:req REQ-RMAP-050 +// fusa:req REQ-RMAP-051 +// fusa:req REQ-RMAP-052 +// fusa:req REQ-RMAP-053 +// fusa:req REQ-RMAP-054 +// fusa:req REQ-RMAP-056 +// fusa:req REQ-RMAP-057 +// fusa:req REQ-RMAP-058 +// fusa:req REQ-RMAP-059 +// fusa:req REQ-RMAP-060 +// fusa:req REQ-RMAP-061 +// fusa:req REQ-RMAP-063 +// fusa:req REQ-RMAP-064 +// fusa:req REQ-RMAP-065 +// fusa:req REQ-RMAP-071 +// fusa:req REQ-RMAP-083 +// fusa:req REQ-RMAP-084 +// fusa:req REQ-WAKEUP-020 +// fusa:req REQ-E2E-029 +// fusa:req REQ-E2E-030 +// fusa:req REQ-E2E-045 +// fusa:req REQ-E2E-046 +// fusa:req REQ-LIFECYCLE-025 +// fusa:req REQ-LIFECYCLE-031 +// fusa:req REQ-SEQ-013 // RC Server register-map data model and EP0 pseudo-endpoint — the // whole-device configuration surface an OPEN Alliance TC18 Remote Control @@ -84,34 +122,70 @@ // (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. +// ── Phase 4 batch B rewrite (cpp-RCP issue #129, ROADMAP.md "Phase 17") ───── +// Ports the rest of c-RCP's own regmap.h/regmap.c batch A left out: HW pin +// mapping (HwPinMapEntry, replaced to match c-RCP's real +// rcp_regmap_hw_pin_map_entry_t row shape — its old pin_id/function +// placeholder had zero consumers anywhere in this codebase, confirmed +// before replacing it), the named-signal index (NamedSignal enum, +// named_signal_string()/named_signal_ep_signal_nr()), request-stream-cfg +// (RequestStreamConfig, EXTENDED — not replaced — with the fields c-RCP +// additionally carries: rx_secure_channel_index/rx_ack_stream_index/ +// rx_resp_stream_index/rx_stream_max_request_size; its own pre-existing +// fields, read by e2e.hpp/watchdog.hpp/sim.hpp and their tests, are kept +// verbatim by name), its watchdog-timeout ms/ticks boundary conversions +// and TC18 0.5.1_RC5-reconciled wire codec (issue #458), response-queue- +// cfg (ResponseQueueConfig, replaced to match c-RCP's real per-queue row +// shape — its old response_queue_size/ack_queue_size/flush_time shape had +// zero real consumers outside this file; the only outside references, +// in respqueue.hpp's/deadline.hpp's own comments, already named fields — +// queue_size/max_avtpdu_size — that never existed on the old struct +// either, i.e. those comments were already stale, not a real dependency), +// the EP-ID/byte_bus_id map (EpIdMappingEntry, EXTENDED with +// request_stream_index/crc_required, appended as its LAST two fields so +// every existing 2-element positional call site — this file's own tests, +// rcp/mock.hpp's power-on EP-ID table — keeps compiling unchanged) plus +// its full diagnostic/query surface (is_ascending, effective_count, +// row_init_default, has_single_client_per_ep, shared_bus_homogeneous, +// ep_type_has_fixed_ep_id, byte_bus_ids_for_stream, +// is_valid_association), and the four optional-subsystem config sections +// (OptionalSubsystemCfg, REQ-RMAP-039). +// +// NOT ported in this batch: c-RCP's own EP0 address-routed dispatcher +// (rcp_regmap_ep0_decode_write_request()/_decode_read_request()/ +// _encode_read_response(), regmap.h:3072-3549) — a single ~500-line +// cross-cutting orchestrator combining every sub-table's own wire codec +// with lifecycle-state/writer authorization and SEQUENCER_config's own +// ownership-aware access control, none of which this batch's own scope +// (ROADMAP.md Phase 4 batch B, this file's own header comment above) +// names — see optional_subsystem_cfg's own section comment below for the +// full rationale. rcp_regmap_sequencer_table_render()/_apply_reconfig() +// (REQ-SEQ-014) are likewise not ported for the same reason: they exist +// in c-RCP purely to serve that same dispatcher. +// +// Batch A's own two loose ends are closed out here: +// +// 1. writer_ctx()'s via_valid_stream_association was pinned fail-closed +// (always false) pending a real EP-ID/byte_bus_id association check +// — ep_id_map::is_valid_association() below is that check, now wired +// in for real (see writer_ctx()'s own updated doc comment). // -// 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. +// 2. The cross-cutting collision batch A found and deliberately left +// unresolved — c-RCP's own rcp_regmap_ep_generic_cfg_t has NO +// per-role E2E CRC-enable fields (that content, in c-RCP, belongs to +// the *functional* config's single ep_req_crc_enable field, not +// three per-role ones), while this codebase's own pre-rewrite +// EndpointGenericConfig already carries THREE — is now resolved, not +// merely re-deferred: c-RCP's rcp_regmap_ep_functional_cfg_t is +// ported for real below (EpFunctionalCfg, content-modeling only — +// every concrete endpoint type in this project's own Phase 3 already +// independently ported its 5 fields inline, per type, rather than +// composing a shared base struct). EndpointGenericConfig's own three +// CRC fields stay exactly where they are: they are not c-RCP's +// ep_req_crc_enable under a different roof, they are a genuinely +// different, broader mechanism this codebase built independently — +// see EndpointGenericConfig's own updated doc comment for the full +// reconciliation. // // Field names and behavior below implement TC18's *behavior* as described // in an internal structured extraction of the specification named above (or, @@ -297,20 +371,85 @@ struct EndpointGenericConfig { 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. + // ── Pre-existing (pre-Phase-4), NOT c-RCP content — RECONCILED batch B ── + // rcp/e2e.hpp's crc_required() and tests/test_e2e.cpp depend on these + // three independently-settable per-role E2E CRC toggles (ROADMAP.md + // milestone 50, v2.6.0). Batch A deferred reconciling them against + // c-RCP's own rcp_regmap_ep_functional_cfg_t (which has a single + // ep_req_crc_enable field, not three per-role ones) as out of its own + // scope; batch B now closes that out, having ported + // rcp_regmap_ep_functional_cfg_t for real (see EpFunctionalCfg below). + // + // RESOLUTION: these three fields stay here, unmoved — this is not a + // deferral, it is the actual answer. They are NOT c-RCP's + // ep_req_crc_enable under a different roof: c-RCP's field is + // request-only and lives on the FUNCTIONAL config (one bool, TC18 + // §13.7 common entries); these three are a genuinely different, + // broader, GENERIC-config mechanism this codebase built on top — + // independently gating CRC verification for the request, the + // acknowledge, AND the response, uniformly, for every endpoint type + // regardless of whether that type's own functional config even has an + // equivalent field. Nothing in this codebase conflates the two: + // e2e.hpp's crc_required() reads only these three; every concrete + // endpoint type added in this project's own Phase 3 (rcp/gpio.hpp, + // rcp/adc.hpp, ...) already independently ported c-RCP's real + // functional-config ep_req_crc_enable field into its OWN config + // struct (e.g. gpio.hpp's own ep_req_crc_enable field, matching + // EpFunctionalCfg::ep_req_crc_enable exactly) and reads only that + // copy for its own wire encode/decode — see EpFunctionalCfg's own doc + // comment below for why that per-type duplication, not composition + // against a shared base, is this codebase's own chosen substitute for + // c-RCP's "compose as first member" C idiom. The two mechanisms + // operate at different layers and neither shadows the other. bool ep_req_crc_enable = false; bool ep_ack_crc_enable = false; bool ep_response_crc_enable = false; }; +// EpFunctionalCfg — REQ-RMAP-017, ported from c-RCP's +// rcp_regmap_ep_functional_cfg_t (include/rcp/regmap.h:1614-1627, +// src/regmap.c). c-RCP's own file header describes this as the +// functional-config prefix "common to every endpoint type. Every +// concrete endpoint type built in Phase 16/19 composes this struct as its +// own first member rather than re-declaring these five fields itself". +// +// NOT composed by anything in this file, and deliberately so: this +// project's own Phase 3 (this codebase's equivalent of c-RCP's Phase +// 16/19 — the point at which concrete endpoint types were added) already +// independently ported these exact 5 fields, inline, into each endpoint +// type's own functional-config struct (e.g. rcp/gpio.hpp's own +// ep_enable/ep_clear_req_storage/ep_req_crc_enable/ep_response_ts_enable/ +// ep_suppress_response fields, rcp/adc.hpp's identical set) rather than +// composing a shared base type — a valid, if duplicative, substitute for +// c-RCP's "anonymous struct as first member" C idiom, which C++ has no +// equally zero-overhead equivalent of at this project's chosen level of +// genericity. Refactoring those already-shipped, already-tested Phase 3 +// headers to compose EpFunctionalCfg instead is out of this batch's own +// scope (it would touch gpio.hpp/adc.hpp/can.hpp/i2c.hpp/iseled.hpp/ +// lin.hpp/mdio.hpp/pwm.hpp/spi.hpp/uart.hpp and their own tests, none of +// which this batch otherwise touches). +// +// EpFunctionalCfg itself is therefore content-modeling only here — the +// same disposition SvrEpCfg above already has (also never wired into +// RegisterMap or Ep0) — not dead code, but not itself consumed by +// anything in this file either. RegisterMap::functional_configs stays +// EndpointFunctionalConfig (Ep0's own opaque-blob wire-level +// representation of "whatever bytes a functional config write carries"), +// entirely unaffected by this struct's addition. +// +// Zero-initialization (every flag false) is this port's equivalent of +// c-RCP's rcp_regmap_ep_functional_cfg_init(). +struct EpFunctionalCfg { + bool ep_enable = false; + bool ep_clear_req_storage = false; + bool ep_req_crc_enable = false; + bool ep_response_ts_enable = false; + bool ep_suppress_response = false; // TC18's own register name renders this with + // a single "s"; spelled correctly here as + // this project's own identifier, not a wire + // encoding (matches c-RCP's own field comment) +}; + struct EndpointFunctionalConfig { std::vector data; // endpoint-type-specific; interpreted starting at v2.3.0 }; @@ -516,35 +655,357 @@ inline std::error_code apply_reconfig(EndpointGenericConfig* entries, size_t cou } // 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. +// ── HW pin-mapping config (extraction §3.7; TC18 §12.7.6 Tables 21/22) ─────── +// HwPinMapEntry — REQ-RMAP-042/043, ported from c-RCP's +// rcp_regmap_hw_pin_map_entry_t (include/rcp/regmap.h:1685-1697, +// src/regmap.c). Replaces this codebase's own earlier, pre-rewrite +// pin_id/function placeholder shape: nothing outside this file or its own +// tests referenced either field (confirmed by search across this worktree +// before replacing them), so this is a clean replacement, not an +// additive extension the way EpIdMappingEntry/RequestStreamConfig below +// need to be for their own already-depended-upon fields. + +// hw_pin_type bit layout (TC18 §12.7.6 Table 22, REQ-RMAP-042/043), +// primary-source-verified directly against the TC18 v0.5.1_RC PDF: four +// packed sub-fields, not a set of independent one-hot flags. Pull (bits +// 1:0): float/pull-down/pull-up — 0b11 is undefined by the table, left +// unnamed here rather than guessed. Output stage (bits 3:2): +// input/open-drain/open-source/push-pull — deliberately NOT a separate +// exclusive INPUT/OUTPUT flag pair: TC18's own text states "All outputs +// are always also an input" (REQ-RMAP-043), so a single 2-bit field +// selecting one of three OUTPUT drive modes (or plain input) is the only +// representation an output-is-simultaneously-readable-as-input pin can +// even have. Drive strength (bits 5:4): input/low/medium/high. Bit 6 is +// reserved, reads 0. Schmitt-Trigger (bit 7): a plain single bit. +namespace hw_pin { +constexpr uint8_t kPullMask = 0x3u; +constexpr uint8_t kPullFloat = 0x0u; +constexpr uint8_t kPullDown = 0x1u; +constexpr uint8_t kPullUp = 0x2u; + +constexpr uint8_t kStageMask = 0xCu; // bits 3:2 +constexpr uint8_t kStageInput = 0x0u; +constexpr uint8_t kStageOpenDrain = 0x4u; +constexpr uint8_t kStageOpenSource = 0x8u; +constexpr uint8_t kStagePushPull = 0xCu; + +constexpr uint8_t kDriveMask = 0x30u; // bits 5:4 +constexpr uint8_t kDriveInput = 0x00u; +constexpr uint8_t kDriveLow = 0x10u; +constexpr uint8_t kDriveMedium = 0x20u; +constexpr uint8_t kDriveHigh = 0x30u; + +// bit 6 reserved, reads 0 -- no constant; never set it. +constexpr uint8_t kSchmittTrigger = 1u << 7; +} // namespace hw_pin 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 + uint8_t hw_ep_nr = 0; // hardware endpoint number this pin belongs to + uint8_t hw_ep_pin_nr = 0; // pin number within that endpoint + uint8_t hw_pin_type = 0; // hw_pin::k* bitmask above (TC18's own register + // name, Table 21) — REQ-RMAP-042 +}; + +// HwPinMapReconfigErrc — REQ-RMAP-040/041, ported from c-RCP's +// rcp_regmap_hw_pin_map_reconfig_errc_t. Errors applying an incoming +// write to a HwPinMapEntry table (see hw_pin_map::apply_reconfig() below). +enum class HwPinMapReconfigErrc : 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& hw_pin_map_reconfig_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.regmap.hw_pin_map_reconfig"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case HwPinMapReconfigErrc::short_write: + return "rcp/regmap: HW_config write has no data"; + case HwPinMapReconfigErrc::out_of_range: + return "rcp/regmap: HW_config write extends past the table's own current extent"; + default: + return "rcp/regmap: HW_config unknown configuration-write error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(HwPinMapReconfigErrc e) noexcept { + return {static_cast(e), hw_pin_map_reconfig_category()}; +} + +// ── HW_config server-side storage + wire codec (REQ-RMAP-040/041) ──────────── +// HW_config is a separate table pointed to by GeneralMap::svr_hw_cfg_ptr +// (an absolute address in the same EP0-scoped space Table 20 itself +// lives in, TC18 §12.7.6 issue #301 finding — see c-RCP's own regmap.h +// file-header investigation, ported into this port's own GeneralMap doc +// comment above). +namespace hw_pin_map { + +// kRowLen — each row's own 3-octet TC18-cited stride (IO_Pin N at +// relative address 3*N/3*N+1/3*N+2). +constexpr size_t kRowLen = 3; + +// kMaxEntries is not itself TC18-derived (svr_io_pin_count is a 16-bit +// register with no fixed upper bound) — matches every sibling table's +// own identical, not-spec-derived bound. +constexpr size_t kMaxEntries = 64; + +// render — REQ-RMAP-040, ported from c-RCP's +// rcp_regmap_hw_pin_map_render(). Serializes entries[0..count) into out +// at each row's own 3-octet stride — out must have room for at least +// kRowLen*count octets. +inline void render(const HwPinMapEntry* entries, size_t count, uint8_t* out) noexcept { + for (size_t i = 0; i < count; ++i) { + out[kRowLen * i + 0] = entries[i].hw_ep_nr; + out[kRowLen * i + 1] = entries[i].hw_ep_pin_nr; + out[kRowLen * i + 2] = entries[i].hw_pin_type; + } +} + +// apply_reconfig — REQ-RMAP-041, ported from c-RCP's +// rcp_regmap_hw_pin_map_apply_reconfig(). Same "render current image, +// patch the addressed octets, re-parse the whole image back" idiom every +// pointed-to table in this file uses — safe here because render() above +// is a lossless 1:1 round-trip (unlike ep_generic_cfg's own render(), +// this table has no defensive fallback/clamp of its own). Every octet of +// every row is R/W* (TC18 Table 21 has no read-only sub-fields within a +// row), so no octet is ever silently skipped. +// +// Returns HwPinMapReconfigErrc::short_write if data_len is 0, +// ::out_of_range if count exceeds kMaxEntries (this port's own fixed +// scratch-buffer bound) or if the write's own span exceeds count*kRowLen +// — entries is left entirely unchanged in either error case. +inline std::error_code apply_reconfig(HwPinMapEntry* 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(HwPinMapReconfigErrc::short_write); + if (count > kMaxEntries) return make_error_code(HwPinMapReconfigErrc::out_of_range); + + const size_t block_len = count * kRowLen; + if (static_cast(relative_start_address) + data_len > block_len) + return make_error_code(HwPinMapReconfigErrc::out_of_range); + + std::array block{}; + render(entries, count, block.data()); + for (size_t i = 0; i < data_len; ++i) block[relative_start_address + i] = data[i]; + for (size_t i = 0; i < count; ++i) { + entries[i].hw_ep_nr = block[kRowLen * i + 0]; + entries[i].hw_ep_pin_nr = block[kRowLen * i + 1]; + entries[i].hw_pin_type = block[kRowLen * i + 2]; + } + + return {}; +} + +} // namespace hw_pin_map + +// ── Per-endpoint-type named-signal index (extraction §3.7; TC18 §12.7.6 +// Table 23) ───────────────────────────────────────────────────────────── +// NamedSignal — REQ-RMAP-044, ported from c-RCP's rcp_regmap_named_signal_t +// (include/rcp/regmap.h:1798-1906, src/regmap.c). The full named-signal +// index shared by every endpoint type, written once here and reused +// unmodified by every endpoint type. This is one flat enumeration for +// human-readable naming (named_signal_string()) — TC18 Table 23's own +// EP_Signal_Nr wire value is NOT this enum's own ordinal; it restarts at +// 0 for every endpoint type (REQ-RMAP-045). named_signal_ep_signal_nr() +// below is the converter between the two: this enum's flat ordinal (for +// naming/identity) and TC18's own per-type-relative wire value (for +// hw_ep_pin_nr, TC18 Table 21). Values are grouped by endpoint type, in +// the same order and per-type numbering Table 23 itself uses. +enum class NamedSignal : uint8_t { + Gpio0 = 0, Gpio1, Gpio2, Gpio3, Gpio4, Gpio5, Gpio6, Gpio7, + Gpio8, Gpio9, Gpio10, Gpio11, Gpio12, Gpio13, Gpio14, Gpio15, + Gpio16, Gpio17, Gpio18, Gpio19, Gpio20, Gpio21, Gpio22, Gpio23, + Gpio24, Gpio25, Gpio26, Gpio27, Gpio28, Gpio29, Gpio30, Gpio31, + SpiClk, SpiPico, SpiPoci, SpiCs0, SpiCs1, SpiCs2, SpiCs3, SpiCs4, SpiCs5, + I2cScl, I2cSda, + // REQ-RMAP-044: TC18 Table 23 enumerates EP_Signal_Nr for every + // endpoint type this codebase implements, not just GPIO/SPI/I2C. + UartTx, UartRx, UartRts, UartCts, + LinTxd, LinRxd, LinNslp, + PwmOut, // positive phase -- Table 23's own name + PwmOutn, // inverted phase -- Table 23's own name + PwmIn, + AdcIn, + DacOut, + CanRxd, + CanTxd, // TC18's own counter-intuitive order: RXD=0, TXD=1 (Table 23) + IseledIspP, IseledIspN, + MdioMdc, + MdioData, // Table 23 names this signal "MDIO" itself, identical to the + // endpoint type name -- disambiguated here as _DATA to avoid + // a name collision with the type name; not a departure from + // the wire meaning + Count, // not itself a valid signal; the number of named signals defined above }; -// ── Sequencer-state registers (extraction §3.11, §3.16) — batch B ──────────── +// named_signal_string — REQ-RMAP-014/015/082, ported from c-RCP's +// rcp_regmap_named_signal_string(). Human-readable, unique name for sig. +// Returns "unknown" (never a null/empty distinguishing failure) for a +// value outside 0..Count-1. +inline const char* named_signal_string(NamedSignal sig) noexcept { + switch (sig) { + case NamedSignal::Gpio0: return "GPIO0"; + case NamedSignal::Gpio1: return "GPIO1"; + case NamedSignal::Gpio2: return "GPIO2"; + case NamedSignal::Gpio3: return "GPIO3"; + case NamedSignal::Gpio4: return "GPIO4"; + case NamedSignal::Gpio5: return "GPIO5"; + case NamedSignal::Gpio6: return "GPIO6"; + case NamedSignal::Gpio7: return "GPIO7"; + case NamedSignal::Gpio8: return "GPIO8"; + case NamedSignal::Gpio9: return "GPIO9"; + case NamedSignal::Gpio10: return "GPIO10"; + case NamedSignal::Gpio11: return "GPIO11"; + case NamedSignal::Gpio12: return "GPIO12"; + case NamedSignal::Gpio13: return "GPIO13"; + case NamedSignal::Gpio14: return "GPIO14"; + case NamedSignal::Gpio15: return "GPIO15"; + case NamedSignal::Gpio16: return "GPIO16"; + case NamedSignal::Gpio17: return "GPIO17"; + case NamedSignal::Gpio18: return "GPIO18"; + case NamedSignal::Gpio19: return "GPIO19"; + case NamedSignal::Gpio20: return "GPIO20"; + case NamedSignal::Gpio21: return "GPIO21"; + case NamedSignal::Gpio22: return "GPIO22"; + case NamedSignal::Gpio23: return "GPIO23"; + case NamedSignal::Gpio24: return "GPIO24"; + case NamedSignal::Gpio25: return "GPIO25"; + case NamedSignal::Gpio26: return "GPIO26"; + case NamedSignal::Gpio27: return "GPIO27"; + case NamedSignal::Gpio28: return "GPIO28"; + case NamedSignal::Gpio29: return "GPIO29"; + case NamedSignal::Gpio30: return "GPIO30"; + case NamedSignal::Gpio31: return "GPIO31"; + case NamedSignal::SpiClk: return "SPI_CLK"; + case NamedSignal::SpiPico: return "SPI_PICO"; + case NamedSignal::SpiPoci: return "SPI_POCI"; + case NamedSignal::SpiCs0: return "SPI_CS0"; + case NamedSignal::SpiCs1: return "SPI_CS1"; + case NamedSignal::SpiCs2: return "SPI_CS2"; + case NamedSignal::SpiCs3: return "SPI_CS3"; + case NamedSignal::SpiCs4: return "SPI_CS4"; + case NamedSignal::SpiCs5: return "SPI_CS5"; + case NamedSignal::I2cScl: return "I2C_SCL"; + case NamedSignal::I2cSda: return "I2C_SDA"; + case NamedSignal::UartTx: return "UART_TX"; + case NamedSignal::UartRx: return "UART_RX"; + case NamedSignal::UartRts: return "UART_RTS"; + case NamedSignal::UartCts: return "UART_CTS"; + case NamedSignal::LinTxd: return "LIN_TXD"; + case NamedSignal::LinRxd: return "LIN_RXD"; + case NamedSignal::LinNslp: return "LIN_NSLP"; + case NamedSignal::PwmOut: return "PWM_OUT"; + case NamedSignal::PwmOutn: return "PWM_OUTN"; + case NamedSignal::PwmIn: return "PWM_IN"; + case NamedSignal::AdcIn: return "ADC_IN"; + case NamedSignal::DacOut: return "DAC_OUT"; + case NamedSignal::CanRxd: return "CAN_RXD"; + case NamedSignal::CanTxd: return "CAN_TXD"; + case NamedSignal::IseledIspP: return "ISELED_ISP_P"; + case NamedSignal::IseledIspN: return "ISELED_ISP_N"; + case NamedSignal::MdioMdc: return "MDIO_MDC"; + case NamedSignal::MdioData: return "MDIO_DATA"; + default: return "unknown"; + } +} + +// named_signal_ep_signal_nr — REQ-RMAP-045, ported from c-RCP's +// rcp_regmap_named_signal_ep_signal_nr(). Converts sig's own flat enum +// ordinal into TC18 Table 23's per-endpoint-type EP_Signal_Nr wire value +// — the value hw_ep_pin_nr (Table 21) actually carries, which restarts +// at 0 for every endpoint type rather than continuing this enum's own +// flat numbering. Returns 0 for NamedSignal::Count or any other value +// outside 0..Count-1 (there is no meaningful EP_Signal_Nr for a signal +// that doesn't exist; 0 is chosen over an out-of-band sentinel to keep +// the return type a plain uint8_t, matching the wire field's own width — +// callers that need to distinguish "not a real signal" from "really is +// EP_Signal_Nr 0" should validate sig against NamedSignal::Count +// themselves before calling). +inline uint8_t named_signal_ep_signal_nr(NamedSignal sig) noexcept { + const auto raw = static_cast(sig); + // NamedSignal is uint8_t-backed and Gpio0 == 0, so raw can never be + // "negative" -- no explicit lower-bound check is needed the way + // c-RCP's own signed-parameter version needs one. + if (raw <= static_cast(NamedSignal::Gpio31)) return raw; // GPIOn's own per-type number is n + if (raw <= static_cast(NamedSignal::SpiCs5)) + return static_cast(raw - static_cast(NamedSignal::SpiClk)); + if (raw <= static_cast(NamedSignal::I2cSda)) + return static_cast(raw - static_cast(NamedSignal::I2cScl)); + if (raw <= static_cast(NamedSignal::UartCts)) + return static_cast(raw - static_cast(NamedSignal::UartTx)); + if (raw <= static_cast(NamedSignal::LinNslp)) + return static_cast(raw - static_cast(NamedSignal::LinTxd)); + if (raw <= static_cast(NamedSignal::PwmOutn)) + return static_cast(raw - static_cast(NamedSignal::PwmOut)); + if (sig == NamedSignal::PwmIn) return 0u; + if (sig == NamedSignal::AdcIn) return 0u; + if (sig == NamedSignal::DacOut) return 0u; + if (raw <= static_cast(NamedSignal::CanTxd)) + return static_cast(raw - static_cast(NamedSignal::CanRxd)); + if (raw <= static_cast(NamedSignal::IseledIspN)) + return static_cast(raw - static_cast(NamedSignal::IseledIspP)); + if (raw <= static_cast(NamedSignal::MdioData)) + return static_cast(raw - static_cast(NamedSignal::MdioMdc)); + return 0u; // NamedSignal::Count or any other invalid value +} + +// ── Sequencer-state registers (extraction §3.11, §3.16) ────────────────────── // Persistent 8-bit values; behavior lives in rcp/request.hpp (v2.5.0). +// Reviewed against c-RCP's own Seq_state register (TC18 §12.7.10 Table 28) +// as part of batch B and left unchanged: this already matches its meaning +// exactly (a plain persistent 8-bit value). c-RCP's own SEQUENCER_config +// WIRE codec (rcp_regmap_sequencer_table_render()/_apply_reconfig(), +// REQ-SEQ-014) is not ported — it exists in c-RCP purely to serve the EP0 +// address-routed dispatcher, itself out of this batch's scope (see this +// file's own "Phase 4 batch B" banner above). using SequencerState = uint8_t; -// ── rx_safety_measure selector (extraction §3.8) — batch B ─────────────────── +// ── rx_safety_measure selector (extraction §3.8) ────────────────────────────── +// Reviewed against c-RCP's own rx_safety_measure field as part of batch B +// and left unchanged: already matches c-RCP's RCP_E2E_MEASURE_FORCE_HIGH_ +// IMPEDANCE/RCP_E2E_MEASURE_SEQUENCER pair exactly, content-modeling only +// on both sides (neither this enum's value nor c-RCP's own has a wire +// register position of its own — TC18 0.5.1_RC5 has no 1:1 replacement +// for it, see request_stream_cfg's own file-header note below). 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) — 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. - +// ── Request-stream config (extraction §3.8; TC18 §12.7.7 Table 24) ────────── +// RequestStreamConfig — REQ-RMAP-018, extended from this codebase's own +// pre-rewrite v2.x design (rcp/e2e.hpp, rcp/watchdog.hpp, rcp/sim.hpp, and +// their own tests already depend on the fields batch A/pre-rewrite already +// had — stream_id, queue_size, rx_wd_timeout_interval, rx_wd_enable, +// rx_wd_safestate_enable, rx_wd_info_enable, rx_enforce_e2e, rx_enforce_seq, +// rx_seq_safestate_enable, rx_ovrflw_safestate_enable, rx_safety_measure, +// rx_safestate_sequencer, rx_safe_sequencer_state — every one of those kept +// verbatim, by name, below). This batch APPENDS the fields c-RCP's own +// rcp_regmap_request_stream_cfg_t (include/rcp/regmap.h:1920-2187, +// src/regmap.c) additionally carries that nothing in this codebase yet +// modeled: rx_secure_channel_index/rx_ack_stream_index/rx_resp_stream_index +// (REQ-RMAP-047/048/049) and rx_stream_max_request_size (fragmentation, +// c-RCP's own Phase 20 addition). rx_wd_timeout_interval is this codebase's +// own pre-existing name for what c-RCP calls rx_wd_timeout_ms — same +// milliseconds unit, not renamed here since e2e.hpp/watchdog.hpp/sim.hpp +// and their tests all already depend on the existing name. +// +// stream_id (avtp::StreamId, not a raw uint64_t rx_stream_id) and +// queue_size (present in this codebase's pre-rewrite design, but with no +// counterpart anywhere in c-RCP's own Table 24 — confirmed by direct +// primary-source read before leaving it alone) are both kept exactly as +// they already were: stream_id because it's this codebase's own +// established stream-identity representation (used the same way +// GeneralMap and every other wire codec in this file use avtp::StreamId/ +// ByteBusId rather than a bare integer), queue_size because nothing +// outside this file/its own tests reads or writes it and removing an +// unused, harmless, pre-existing field is not this batch's job. struct RequestStreamConfig { avtp::StreamId stream_id{}; uint16_t queue_size = 0; @@ -564,28 +1025,865 @@ struct RequestStreamConfig { RxSafetyMeasure rx_safety_measure = RxSafetyMeasure::ForceHighImpedance; uint16_t rx_safestate_sequencer = 0; SequencerState rx_safe_sequencer_state = 0; + + // ── Secure channel / acknowledge & response routing (new this batch) ── + uint8_t rx_secure_channel_index = 0; // REQ-RMAP-047 (Table 24, relative 0x000C, 8 bit, + // R/W*): which secure channel this request stream + // is carried on. 0 is TC18's own defined "no cyber + // security, MACsec uncontrolled port" encoding -- + // content modeling only, this codebase has no + // MACsec layer of its own to select one in. + uint8_t rx_ack_stream_index = 0; // REQ-RMAP-048 (Table 24, relative 0x0010, 8 bit, + // R/W*): index of the response/ack stream (see + // ResponseQueueConfig below) endpoints bound to + // this request stream send their acknowledges on. + // 0 is TC18's own "no acknowledge is to be sent" + // encoding -- content modeling only. + uint8_t rx_resp_stream_index = 1; // REQ-RMAP-049 (Table 24, relative 0x0011, 8 bit, + // R/W*): index of the response stream. 0 is TC18's + // own "no response is to be sent" encoding; the + // power-on default is 1, not 0, so a freshly reset + // server can answer a discovery request before any + // configuration has been written (matches c-RCP's + // own rcp_regmap_request_stream_cfg_init()). + + // ── Fragmentation (new this batch) ───────────────────────────────────── + // rx_stream_max_request_size -- the largest single-AVTPDU ACF payload + // (header-and-payload, excluding any e2e.hpp CRC trailer) this stream + // will assemble or accept in one fragment. 0 means fragmentation is + // unsupported for this stream. size_t internally (matching a byte-count + // convention), saturated -- never wrapped -- to the wire's 16-bit width + // by request_stream_cfg::render() below. + size_t rx_stream_max_request_size = 0; +}; + +// ── request-stream-cfg boundary conversions, wire codec (issue #306, +// REQ-RMAP-047/048/049/050/071; TC18 0.5.1_RC5 reconciliation, issue +// #458) ─────────────────────────────────────────────────────────────── +// +// TC18 0.5.1_RC5 terminology drift (ported from c-RCP's own investigation, +// regmap.h file header, "issue #458"): row-relative octet 0x000D's real +// RC5 layout is 4 meaningful bits (bit0 rx_enforce_crc, bit1 +// rx_enforce_sequence, bit2 rx_enforce_watchdog, bit3 +// rx_enforce_request_filing; bits [6:4] Reserved, R only; bit7 +// rx_stream_status), not 8 independently-configurable bits. This +// codebase's own richer model (rx_enforce_e2e; rx_enforce_seq + +// rx_seq_safestate_enable; rx_wd_enable + rx_wd_safestate_enable; +// rx_ovrflw_safestate_enable) deliberately keeps the "block" and "also +// enter safe state" dimensions of the sequence/watchdog pairs +// independently expressible internally (e2e.hpp's own design) -- a strict +// superset of what an RC5 wire peer can express -- but render() below +// renders each of those two combined wire bits true ONLY when BOTH +// internal dimensions agree (AND, never OR), since OR would let a stream +// that only blocks (without entering safe state) falsely claim, to a real +// RC5 peer reading this register, that it also enters safe state -- an +// overstated safety guarantee this port must never produce. The reverse +// direction is exact and lossless: a real RC5 write can only ever express +// the coupled state to begin with, so apply_reconfig() sets BOTH internal +// dimensions of a pair together from that one arriving bit. +// rx_safety_measure and rx_wd_info_enable both have NO 1:1 replacement in +// RC5's real 4-bit scheme (c-RCP's own still-open finding) -- both are +// content-modeling only, with no wire register position of their own, +// same disposition rx_wd_action has in c-RCP (this port has no +// counterpart field for rx_wd_action at all: c-RCP's own primary-source +// verification found no corresponding register for it anywhere in TC18). +namespace request_stream_cfg { + +// wd_timeout_ms_to_ticks — REQ-RMAP-050/083, ported from c-RCP's +// rcp_regmap_wd_timeout_ms_to_ticks(). TC18 names no fixed clock-tick +// rate for rx_wd_timeout_interval's own register anywhere near its own +// definition, so (matching this codebase's own caller-supplies-already- +// classified-units convention) the caller supplies ms_per_tick. Rounds +// DOWN (never grants more slack than requested -- a safety-integrity +// register should never silently widen a configured watchdog period). +// Returns false (leaving out_ticks unchanged) if ms_per_tick == 0 (no +// register value for a zero-length tick) or if the converted tick count +// would not fit the register's own 16-bit width. +inline bool wd_timeout_ms_to_ticks(uint32_t timeout_ms, uint32_t ms_per_tick, + uint16_t& out_ticks) noexcept { + if (ms_per_tick == 0u) return false; + const uint32_t ticks = timeout_ms / ms_per_tick; + if (ticks > static_cast(UINT16_MAX)) return false; + out_ticks = static_cast(ticks); + return true; +} + +// wd_timeout_ticks_to_ms — REQ-RMAP-050, the inverse conversion. Returns +// false (leaving out_timeout_ms unchanged) if ms_per_tick == 0 or if the +// product would overflow uint32_t. +inline bool wd_timeout_ticks_to_ms(uint16_t ticks, uint32_t ms_per_tick, + uint32_t& out_timeout_ms) noexcept { + if (ms_per_tick == 0u) return false; + const uint64_t product = static_cast(ticks) * static_cast(ms_per_tick); + if (product > static_cast(UINT32_MAX)) return false; + out_timeout_ms = static_cast(product); + return true; +} + +// kRowLen — REQ-RMAP-047: TC18's own 24-octet-per-request-stream wire +// stride (confirmed via direct primary-source read, both spec revisions). +constexpr size_t kRowLen = 24; + +// kMaxEntries is not itself TC18-derived -- matches every sibling table's +// own identical, not-spec-derived bound. +constexpr size_t kMaxEntries = 64; + +// render — REQ-RMAP-047/048/049/050/051/071, ported from c-RCP's +// rcp_regmap_request_stream_cfg_render(). Serializes entries[0..count) +// into out at each row's own 24-octet stride. watchdog_ms_per_tick is +// rx_wd_timeout_interval's own caller-supplied tick duration (see +// wd_timeout_ms_to_ticks() above) -- a value that cannot be represented +// at that rate (including watchdog_ms_per_tick == 0, "not configured") +// falls back to encoding 0x0000, the same "reserved / cannot be +// represented, use 0" treatment this file already uses elsewhere. +// rx_stream_max_request_size and rx_safestate_sequencer are each +// saturated (never wrapped) to their own narrower wire widths (16 bit, +// 8 bit respectively) since wraparound would silently alias onto ANOTHER +// valid, meaningfully-different value. +// +// rx_stream_status_blocked (issue #424, REQ-E2E-046/REQ-RMAP-051) is a +// caller-supplied, index-parallel array (entries[i] <-> +// rx_stream_status_blocked[i]) of already-computed aggregate values for +// TC18's own distinct, live rx_stream_status bit (row-relative 0x000D bit +// 7) -- may be nullptr, meaning "no live status known" (bit 7 then +// renders 0), matching this file's other NULL-means-absent optional +// inputs. +inline void render(const RequestStreamConfig* entries, size_t count, uint8_t* out, + uint32_t watchdog_ms_per_tick, const bool* rx_stream_status_blocked) noexcept { + for (size_t i = 0; i < count; ++i) { + const RequestStreamConfig& e = entries[i]; + uint8_t* row = out + kRowLen * i; + + avtp::detail::put_u64(&row[0x0000], e.stream_id.to_u64()); + + const uint16_t max_request_size_wire = + (e.rx_stream_max_request_size > 0xFFFFu) ? uint16_t{0xFFFFu} + : static_cast(e.rx_stream_max_request_size); + avtp::detail::put_u16(&row[0x0008], max_request_size_wire); + + uint16_t wd_timeout_ticks = 0; + if (wd_timeout_ms_to_ticks(e.rx_wd_timeout_interval, watchdog_ms_per_tick, wd_timeout_ticks)) { + avtp::detail::put_u16(&row[0x000A], wd_timeout_ticks); + } else { + row[0x000A] = 0x00; + row[0x000B] = 0x00; + } + + row[0x000C] = e.rx_secure_channel_index; + + uint8_t bits_0x000d = static_cast(e.rx_enforce_e2e ? 0x01u : 0x00u); + bits_0x000d = static_cast( + bits_0x000d | ((e.rx_enforce_seq && e.rx_seq_safestate_enable) ? 0x02u : 0x00u)); + bits_0x000d = static_cast( + bits_0x000d | ((e.rx_wd_enable && e.rx_wd_safestate_enable) ? 0x04u : 0x00u)); + bits_0x000d = static_cast(bits_0x000d | (e.rx_ovrflw_safestate_enable ? 0x08u : 0x00u)); + bits_0x000d = static_cast( + bits_0x000d | + ((rx_stream_status_blocked != nullptr && rx_stream_status_blocked[i]) ? 0x80u : 0x00u)); + row[0x000D] = bits_0x000d; + + const uint8_t safestate_sequencer_wire = (e.rx_safestate_sequencer > 0xFFu) + ? uint8_t{0xFFu} + : static_cast(e.rx_safestate_sequencer); + row[0x000E] = safestate_sequencer_wire; + row[0x000F] = e.rx_safe_sequencer_state; + row[0x0010] = e.rx_ack_stream_index; + row[0x0011] = e.rx_resp_stream_index; + + row[0x0012] = 0x00; row[0x0013] = 0x00; // reserved (16 bit) + row[0x0014] = 0x00; row[0x0015] = 0x00; // reserved (32 bit) + row[0x0016] = 0x00; row[0x0017] = 0x00; + } +} + +} // namespace request_stream_cfg + +// RequestStreamCfgReconfigErrc — REQ-RMAP-047/048/049/071, ported from +// c-RCP's rcp_regmap_request_stream_cfg_reconfig_errc_t. +enum class RequestStreamCfgReconfigErrc : int { + short_write = 1, + out_of_range = 2, }; -// ── 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. +inline const std::error_category& request_stream_cfg_reconfig_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.regmap.request_stream_cfg_reconfig"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case RequestStreamCfgReconfigErrc::short_write: + return "rcp/regmap: request-stream-cfg write has no data"; + case RequestStreamCfgReconfigErrc::out_of_range: + return "rcp/regmap: request-stream-cfg write extends past the table's own current extent"; + default: + return "rcp/regmap: request-stream-cfg unknown configuration-write error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(RequestStreamCfgReconfigErrc e) noexcept { + return {static_cast(e), request_stream_cfg_reconfig_category()}; +} + +namespace request_stream_cfg { + +// apply_reconfig — REQ-RMAP-047/048/049/050/071, ported from c-RCP's +// rcp_regmap_request_stream_cfg_apply_reconfig(). Same render-patch- +// reparse idiom as every other pointed-to table's own apply_reconfig() +// in this file. A write landing on the 3 reserved trailing octets, on +// bits [6:4] of 0x000D (Reserved, R only), or on bit 7 of 0x000D +// (rx_stream_status, a live server-computed status, not client- +// configurable content) is accepted (every octet here is R/W or R/W*, +// nothing this codec itself must reject) but has no effect on any struct +// field -- it patches the transient scratch image this function builds +// internally, discarded rather than re-parsed back into anything for +// those specific bits. bits [3:0] of 0x000D remain the genuinely R/W* +// content-bearing fields this function round-trips: bit1/bit2 each set +// BOTH of this codebase's own two internal dimensions of their pair +// together, since a real RC5 write can only ever express the coupled +// "block AND enter safe state" state (see this table's own file-header +// note above). rx_safety_measure and rx_wd_info_enable are left +// unchanged (no wire register position of their own). +inline std::error_code apply_reconfig(RequestStreamConfig* entries, size_t count, + uint16_t relative_start_address, const uint8_t* data, + size_t data_len, uint32_t watchdog_ms_per_tick) noexcept { + if (data_len == 0u) return make_error_code(RequestStreamCfgReconfigErrc::short_write); + if (count > kMaxEntries) return make_error_code(RequestStreamCfgReconfigErrc::out_of_range); + + const size_t block_len = count * kRowLen; + if (static_cast(relative_start_address) + data_len > block_len) + return make_error_code(RequestStreamCfgReconfigErrc::out_of_range); + + std::array block{}; + render(entries, count, block.data(), watchdog_ms_per_tick, nullptr); + for (size_t i = 0; i < data_len; ++i) block[relative_start_address + i] = data[i]; + + for (size_t i = 0; i < count; ++i) { + RequestStreamConfig& e = entries[i]; + const uint8_t* row = &block[kRowLen * i]; + + e.stream_id = avtp::StreamId::from_u64(avtp::detail::get_u64(&row[0x0000])); + e.rx_stream_max_request_size = static_cast(avtp::detail::get_u16(&row[0x0008])); + + uint32_t wd_timeout_ms = 0; + if (wd_timeout_ticks_to_ms(avtp::detail::get_u16(&row[0x000A]), watchdog_ms_per_tick, + wd_timeout_ms)) { + e.rx_wd_timeout_interval = wd_timeout_ms; + } + e.rx_secure_channel_index = row[0x000C]; + + const uint8_t bits_0x000d = row[0x000D]; + e.rx_enforce_e2e = (bits_0x000d & 0x01u) != 0u; + { + const bool seq_bit = (bits_0x000d & 0x02u) != 0u; + e.rx_enforce_seq = seq_bit; + e.rx_seq_safestate_enable = seq_bit; + } + { + const bool wd_bit = (bits_0x000d & 0x04u) != 0u; + e.rx_wd_enable = wd_bit; + e.rx_wd_safestate_enable = wd_bit; + } + e.rx_ovrflw_safestate_enable = (bits_0x000d & 0x08u) != 0u; + // bit 7 (rx_stream_status) intentionally not unpacked into any + // field -- see this function's own doc comment above. + + e.rx_safestate_sequencer = static_cast(row[0x000E]); + e.rx_safe_sequencer_state = row[0x000F]; + e.rx_ack_stream_index = row[0x0010]; + e.rx_resp_stream_index = row[0x0011]; + } + return {}; +} + +// resolve_index — REQ-SEQ-013 (issue #335), ported from c-RCP's +// rcp_regmap_request_stream_cfg_resolve_index(). Resolves stream_id to +// its own 1-based position in entries[0..count) (0 reserved as a "no +// match" sentinel rather than a real index, matching +// EpIdMappingEntry.request_stream_index's own convention below). Returns +// 0 if no entry's own stream_id equals stream_id, or entries is nullptr. +inline uint8_t resolve_index(const RequestStreamConfig* entries, size_t count, + uint64_t stream_id) noexcept { + if (entries == nullptr) return 0u; + for (size_t i = 0; i < count; ++i) { + if (entries[i].stream_id.to_u64() == stream_id) return static_cast(i + 1u); + } + return 0u; +} + +} // namespace request_stream_cfg + +// ── EP-ID / byte_bus_id mapping table (extraction §3.9; TC18 §12.7.8 +// Table 25/26) ───────────────────────────────────────────────────────── +// EpIdMappingEntry — REQ-RMAP-052/053, extended from this codebase's own +// pre-rewrite v2.x design (ep_id, byte_bus_id kept verbatim; nothing +// outside this file/its own tests reads or writes those two fields under +// their pre-existing 2-field shape, confirmed before extending). c-RCP's +// own rcp_regmap_ep_id_map_entry_t (include/rcp/regmap.h:2577-2661, +// src/regmap.c) additionally carries request_stream_index and +// crc_required -- appended here, in that order, as this struct's LAST two +// fields (matching c-RCP's own reason for that placement: existing +// 2-element brace-init call sites, e.g. this file's own tests and +// rcp/mock.hpp's power-on EP-ID table, keep compiling unchanged, with +// both new fields correctly defaulting). struct EpIdMappingEntry { EndpointId ep_id = 0; avtp::ByteBusId byte_bus_id = 0; + + uint8_t request_stream_index = 0; // REQ-RMAP-052 (Table 25, row offset 0x0000, 8 bit, + // R/W+): which request stream this row's mapping + // applies to. 0 is TC18's own defined end-of-table + // sentinel (REQ-RMAP-054) -- see + // ep_id_map::effective_count() below. This is what + // is_valid_association() below needs to answer + // TC18 §12.3.1.2's "any valid stream_id/byte_bus_id + // combination" writer_ctx() question for real. + bool crc_required = false; // REQ-RMAP-053 (issue #421, Table 25 row offset + // 0x0002 bit 4 "Ctrl.CRC_required"). Channel_selection + // (Table 26 bits [3:0]) is deliberately NOT modeled -- + // always renders 0, silently dropped on write, per + // c-RCP's own dedicated investigation (still-draft + // BBID-based proposal; evt-bits remain authoritative). }; -// ── Response / ack queue config (extraction §3.10) — batch B ───────────────── +// EpIdMapReconfigErrc — REQ-RMAP-052/053/054, ported from c-RCP's +// rcp_regmap_ep_id_map_reconfig_errc_t. +enum class EpIdMapReconfigErrc : int { + short_write = 1, + out_of_range = 2, +}; + +inline const std::error_category& ep_id_map_reconfig_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.regmap.ep_id_map_reconfig"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case EpIdMapReconfigErrc::short_write: + return "rcp/regmap: EP_ID_config write has no data"; + case EpIdMapReconfigErrc::out_of_range: + return "rcp/regmap: EP_ID_config write extends past the table's own current extent"; + default: + return "rcp/regmap: EP_ID_config unknown configuration-write error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(EpIdMapReconfigErrc e) noexcept { + return {static_cast(e), ep_id_map_reconfig_category()}; +} + +namespace ep_id_map { + +// kRowLen — REQ-RMAP-052: TC18's own 4-octet-per-row stride +// (request_stream_index @0x0000, ep_id/EP_Nr @0x0001, byte_bus_id/Ctrl +// @0x0002, confirmed directly against the primary source). +constexpr size_t kRowLen = 4; +// kMaxEntries is not itself TC18-derived -- matches every sibling table's +// own identical, not-spec-derived bound. +constexpr size_t kMaxEntries = 64; + +// is_ascending — REQ-RMAP-056, ported from c-RCP's +// rcp_regmap_ep_id_map_is_ascending(). Read-only diagnostic: true iff +// entries[0..count) is strictly ascending in the COMPOSITE key +// (request_stream_index, byte_bus_id) -- request_stream_index must never +// decrease, and within one unchanged request_stream_index run, +// byte_bus_id must strictly increase. A higher request_stream_index +// always counts as ascending regardless of that row's own byte_bus_id +// value. Vacuously true for count < 2. +// +// NOTE (ported from c-RCP's own file-header, "Known spec ambiguity"): +// this ordering has NO server-side enforcement in the specification +// itself (as of TC18 0.5.1_RC4 the requirement was deleted from the spec +// entirely) -- this function is a read-only diagnostic for tooling, never +// invoked by, and must not be mistaken for, server-side enforcement; +// there is deliberately no such enforcement to call it from. +inline bool is_ascending(const EpIdMappingEntry* entries, size_t count) noexcept { + if (count < 2) return true; + for (size_t i = 1; i < count; ++i) { + const EpIdMappingEntry& prev = entries[i - 1]; + const EpIdMappingEntry& cur = entries[i]; + if (prev.request_stream_index != cur.request_stream_index) { + if (!(prev.request_stream_index < cur.request_stream_index)) return false; + continue; + } + if (!(prev.byte_bus_id < cur.byte_bus_id)) return false; + } + return true; +} + +// effective_count — REQ-RMAP-054, ported from c-RCP's +// rcp_regmap_ep_id_map_effective_count(). Returns the number of leading +// rows in entries[0..capacity) that precede the first row whose +// request_stream_index == 0 (TC18's own defined end-of-table sentinel); +// if no row in that range is a sentinel, returns capacity unchanged. +// Read-only -- does not modify entries. +inline size_t effective_count(const EpIdMappingEntry* entries, size_t capacity) noexcept { + for (size_t i = 0; i < capacity; ++i) { + if (entries[i].request_stream_index == 0u) return i; + } + return capacity; +} + +// row_init_default — REQ-RMAP-084, ported from c-RCP's +// rcp_regmap_ep_id_map_row_init_default(). TC18 §12.7.8 requires the +// table's power-on default contents to permit access to EP0 before any +// configuration is written: request_stream_index = 1 (the smallest value +// that is a valid stream index rather than the end-of-table sentinel), +// ep_id = kEp0, byte_bus_id = 0. Callers that own a fixed-capacity table +// are expected to place the result at row 0 at startup, before any +// client write. +inline void row_init_default(EpIdMappingEntry& row) noexcept { + row.request_stream_index = 1u; + row.ep_id = kEp0; + row.byte_bus_id = 0u; +} + +// render — REQ-RMAP-052/053, ported from c-RCP's +// rcp_regmap_ep_id_map_render(). Serializes entries[0..count) into out at +// each row's own 4-octet stride. ep_id is truncated to the wire's real +// 8-bit EP_Nr width. byte_bus_id/Ctrl at row-relative offset 0x0002 is +// packed per Table 25/26: byte_bus_id masked to its own real 11-bit wire +// width and shifted into bits[15:5], crc_required at bit 4, +// Channel_selection (bits[3:0]) always 0 -- deliberately unimplemented. +inline void render(const EpIdMappingEntry* entries, size_t count, uint8_t* out) noexcept { + for (size_t i = 0; i < count; ++i) { + uint8_t* row = out + kRowLen * i; + row[0] = entries[i].request_stream_index; + row[1] = static_cast(entries[i].ep_id); + + uint16_t bbid_ctrl = static_cast((entries[i].byte_bus_id & 0x07FFu) << 5); + bbid_ctrl = static_cast(bbid_ctrl | (entries[i].crc_required ? 0x10u : 0x00u)); + avtp::detail::put_u16(&row[2], bbid_ctrl); + } +} + +// apply_reconfig — REQ-RMAP-052/053/054, ported from c-RCP's +// rcp_regmap_ep_id_map_apply_reconfig(). Same render-patch-reparse idiom +// every pointed-to table in this file uses. Every octet of every row is +// R/W+ (no read-only sub-fields within a row), so no octet is ever +// silently skipped. count itself (the table's own current row count) is +// never changed by this call. Channel_selection (bits[3:0] of the +// byte_bus_id/Ctrl word) is read but intentionally discarded, not stored +// to any field. +inline std::error_code apply_reconfig(EpIdMappingEntry* 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(EpIdMapReconfigErrc::short_write); + if (count > kMaxEntries) return make_error_code(EpIdMapReconfigErrc::out_of_range); + + const size_t block_len = count * kRowLen; + if (static_cast(relative_start_address) + data_len > block_len) + return make_error_code(EpIdMapReconfigErrc::out_of_range); + + std::array block{}; + render(entries, count, block.data()); + for (size_t i = 0; i < data_len; ++i) block[relative_start_address + i] = data[i]; + + for (size_t i = 0; i < count; ++i) { + const uint16_t bbid_ctrl = avtp::detail::get_u16(&block[kRowLen * i + 2]); + entries[i].request_stream_index = block[kRowLen * i + 0]; + entries[i].ep_id = block[kRowLen * i + 1]; // zero-extends + entries[i].byte_bus_id = static_cast((bbid_ctrl >> 5) & 0x07FFu); + entries[i].crc_required = (bbid_ctrl & 0x10u) != 0u; + } + + return {}; +} + +// has_single_client_per_ep — REQ-RMAP-057, ported from c-RCP's +// rcp_regmap_ep_id_map_has_single_client_per_ep(). Read-only diagnostic: +// TC18 §12.7.8 recommends, for safety reasons, that an endpoint be mapped +// to at most one RC Client (one request_stream_index) at a time. Returns +// true iff no ep_id in entries[0..count) is associated with two +// different request_stream_index values. O(count^2); count is expected +// to stay small (one server's own endpoint set). +inline bool has_single_client_per_ep(const EpIdMappingEntry* entries, size_t count) noexcept { + for (size_t i = 0; i < count; ++i) { + for (size_t j = i + 1; j < count; ++j) { + if (entries[i].ep_id == entries[j].ep_id && + entries[i].request_stream_index != entries[j].request_stream_index) { + return false; + } + } + } + return true; +} + +// shared_bus_homogeneous — REQ-RMAP-058, ported from c-RCP's +// rcp_regmap_ep_id_map_shared_bus_homogeneous(). Read-only diagnostic: +// TC18 §12.7.8 recommends that endpoints sharing a byte_bus_id within one +// request stream share the same ep_type. ep_types[] is a caller-supplied, +// index-parallel array (ep_types[i] is entries[i]'s own endpoint's +// ep_type, looked up elsewhere -- this row carries no ep_type of its +// own). Returns true iff, for every group of rows sharing one +// (request_stream_index, byte_bus_id) pair, every ep_types[] value in +// that group is identical. O(count^2). +inline bool shared_bus_homogeneous(const EpIdMappingEntry* entries, const uint8_t* ep_types, + size_t count) noexcept { + for (size_t i = 0; i < count; ++i) { + for (size_t j = i + 1; j < count; ++j) { + if (entries[i].request_stream_index == entries[j].request_stream_index && + entries[i].byte_bus_id == entries[j].byte_bus_id && ep_types[i] != ep_types[j]) { + return false; + } + } + } + return true; +} + +// ep_type_has_fixed_ep_id — REQ-WAKEUP-020, ported from c-RCP's +// rcp_regmap_ep_id_map_ep_type_has_fixed_ep_id(). Read-only diagnostic: +// checks that every row whose own ep_types[i] == target_ep_type has +// ep_id == required_ep_id (e.g. TC18 §13.7.2.1 fixes the WakeUp +// endpoint's own EP_Nr to 1). Vacuously true if no such row exists. +// O(count). This function has no dependency on any concrete endpoint-type +// header -- target_ep_type/required_ep_id are caller-supplied. +inline bool ep_type_has_fixed_ep_id(const EpIdMappingEntry* entries, const uint8_t* ep_types, + size_t count, uint8_t target_ep_type, + uint16_t required_ep_id) noexcept { + for (size_t i = 0; i < count; ++i) { + if (ep_types[i] == target_ep_type && entries[i].ep_id != required_ep_id) return false; + } + return true; +} + +// byte_bus_ids_for_stream — REQ-E2E-029/030/045 (issue #335), ported from +// c-RCP's rcp_regmap_ep_id_map_byte_bus_ids_for_stream(). The "cross- +// endpoint orchestrator query": given entries[0..count), writes every +// DISTINCT byte_bus_id whose own row names request_stream_index into +// out_byte_bus_ids[0..out_capacity), skipping a byte_bus_id already +// written. byte_bus_id, not ep_id, is this function's own return unit -- +// a caller's own dispatch mechanism resolves a live endpoint by +// byte_bus_id, never by raw ep_id alone (TC18 §12.9.1: "the EPs are +// mapped by their byte_bus_ids"). Returns the TOTAL number of distinct +// byte_bus_id values found, which may exceed out_capacity -- the same +// "ask first, then size a buffer" idiom this codebase's own scheduler +// uses elsewhere. out_byte_bus_ids may be nullptr iff out_capacity == 0. +// O(count^2); count is expected to stay small. +inline size_t byte_bus_ids_for_stream(const EpIdMappingEntry* entries, size_t count, + uint8_t request_stream_index, + avtp::ByteBusId* out_byte_bus_ids, + size_t out_capacity) noexcept { + size_t found = 0; + for (size_t i = 0; i < count; ++i) { + if (entries[i].request_stream_index != request_stream_index) continue; + + bool already_written = false; + for (size_t j = 0; j < i; ++j) { + if (entries[j].request_stream_index == request_stream_index && + entries[j].byte_bus_id == entries[i].byte_bus_id) { + already_written = true; + break; + } + } + if (already_written) continue; + + if (found < out_capacity) out_byte_bus_ids[found] = entries[i].byte_bus_id; + ++found; + } + return found; +} + +// is_valid_association — REQ-LIFECYCLE-025/031 (issue #341 lineage), +// ported from c-RCP's rcp_regmap_ep_id_map_is_valid_association(). The +// membership test TC18 §12.3.1.2's "the request needs to come either via +// the discovery stream or via a valid stream_id/byte_bus_id combination" +// case needs: given a caller-resolved (request_stream_index, byte_bus_id) +// pair (resolve the raw stream_id first via +// request_stream_cfg::resolve_index() above -- this function does not +// re-derive it), true iff some row in entries[0..count) names EXACTLY +// that pair, regardless of which ep_id that row's own owning endpoint is +// -- "regardless of which endpoint owns it" is the operative distinction +// from EpClient's own single-endpoint-scoped via_owning_stream: TC18's +// "any valid stream_id/byte_bus_id combination" text does not require the +// combination to belong to the endpoint whose field is being written, +// only that the combination itself is a real, currently-configured one. +// entries may be nullptr iff count == 0 (returns false). O(count), a +// single linear scan. +// +// THIS is what unblocks batch A's writer_ctx() (see below): before this +// function existed in this port, via_valid_stream_association had no way +// to evaluate TC18 §12.3.1.2's rule and was pinned fail-closed (always +// false). +inline bool is_valid_association(const EpIdMappingEntry* entries, size_t count, + uint8_t request_stream_index, avtp::ByteBusId byte_bus_id) noexcept { + for (size_t i = 0; i < count; ++i) { + if (entries[i].request_stream_index == request_stream_index && + entries[i].byte_bus_id == byte_bus_id) { + return true; + } + } + return false; +} + +} // namespace ep_id_map + +// ── Response / ack queue config (extraction §3.10; TC18 §12.7.9 Table 27) ─── +// ResponseQueueConfig — REQ-RMAP-059..065, replaces this codebase's own +// pre-rewrite v2.x placeholder shape (response_queue_size/ack_queue_size/ +// flush_time -- confirmed, before replacing them, that nothing outside +// this file/its own tests reads or writes any of those three fields; the +// only other references anywhere in this codebase, rcp/respqueue.hpp's +// and rcp/deadline.hpp's own comments, name fields -- +// ResponseQueueConfig::queue_size/max_avtpdu_size -- that never actually +// existed on the old struct either, i.e. those comments were already +// stale/aspirational, not a real dependency to preserve) with c-RCP's own +// rcp_regmap_response_queue_cfg_t (include/rcp/regmap.h:2457-2529, +// src/regmap.c): one row per response/ack queue, matching TC18's own +// per-queue register layout exactly. struct ResponseQueueConfig { - uint16_t response_queue_size = 0; - uint16_t ack_queue_size = 0; - uint32_t flush_time = 0; + uint16_t stream_uid = 0; // REQ-RMAP-060 (Table 27, relative 0x0000, 16 bit, R/W+): + // bits [63:48] (unique_id half) of the stream_id this + // queue transmits on -- see response_queue_stream_id() + uint16_t max_avtpdu_size = 0; // REQ-RMAP-061 (Table 27, relative 0x0002, 16 bit, R/W*): + // max AVTPDU length, in quadlets, this queue generates + uint16_t queue_size = 0; // REQ-RMAP-059 (Table 27, relative 0x0004, 16 bit, R/W*): + // this queue's configured transmit-memory reservation, + // in 32-bit words + uint16_t flush_on_count = 0; // REQ-RMAP-063 (Table 27, relative 0x0006, 16 bit, R/W+): + // queued-octet threshold that triggers a flush + uint32_t flush_time_us = 0; // REQ-RMAP-064/065 (Table 27, relative 0x0008, 16 bit, + // R/W+, microseconds): elapsed-since-last-transmission + // threshold that forces a flush even of an empty queue. + // Deliberately wider than the 16-bit wire register -- + // rcp/respqueue.hpp's own should_flush_by_time() already + // takes an even wider elapsed/threshold pair -- a genuine + // content/wire width mismatch: render() below saturates + // (never wraps) a value exceeding 0xFFFF. +}; + +// response_queue_stream_id — REQ-RMAP-060, ported from c-RCP's +// rcp_regmap_response_queue_stream_id(). Builds the full stream_id cfg's +// queue transmits on, given the interface's own mac -- cfg.stream_uid +// supplies the suffix half. +inline avtp::StreamId response_queue_stream_id(const ResponseQueueConfig& cfg, + const std::array& mac) noexcept { + avtp::StreamId id; + id.mac = mac; + id.suffix = cfg.stream_uid; + return id; +} + +// ResponseQueueCfgReconfigErrc — REQ-RMAP-061/065, ported from c-RCP's +// rcp_regmap_response_queue_cfg_reconfig_errc_t. +enum class ResponseQueueCfgReconfigErrc : int { + short_write = 1, + out_of_range = 2, +}; + +inline const std::error_category& response_queue_cfg_reconfig_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.regmap.response_queue_cfg_reconfig"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case ResponseQueueCfgReconfigErrc::short_write: + return "rcp/regmap: response-queue-config write has no data"; + case ResponseQueueCfgReconfigErrc::out_of_range: + return "rcp/regmap: response-queue-config write extends past the table's own current extent"; + default: + return "rcp/regmap: response-queue-config unknown configuration-write error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(ResponseQueueCfgReconfigErrc e) noexcept { + return {static_cast(e), response_queue_cfg_reconfig_category()}; +} + +namespace response_queue_cfg { + +// kRowLen — REQ-RMAP-061: TC18's own 10-octet-per-queue wire stride +// (STREAM_UID@0x0000, Max_AVTPDUsize@0x0002, queue_size@0x0004, +// flush_on_count@0x0006, Flush_time@0x0008). +constexpr size_t kRowLen = 10; + +// kMaxEntries is not itself TC18-derived -- matches every sibling table's +// own identical, not-spec-derived bound. +constexpr size_t kMaxEntries = 64; + +// render — REQ-RMAP-061, ported from c-RCP's +// rcp_regmap_response_queue_cfg_render(). Serializes entries[0..count) +// into out at each row's own 10-octet stride. flush_time_us is saturated +// (never wrapped) to 0xFFFF if it exceeds the wire register's own 16-bit +// range. +inline void render(const ResponseQueueConfig* entries, size_t count, uint8_t* out) noexcept { + for (size_t i = 0; i < count; ++i) { + const uint16_t flush_time_wire = (entries[i].flush_time_us > 0xFFFFu) + ? uint16_t{0xFFFFu} + : static_cast(entries[i].flush_time_us); + uint8_t* row = out + kRowLen * i; + avtp::detail::put_u16(&row[0], entries[i].stream_uid); + avtp::detail::put_u16(&row[2], entries[i].max_avtpdu_size); + avtp::detail::put_u16(&row[4], entries[i].queue_size); + avtp::detail::put_u16(&row[6], entries[i].flush_on_count); + avtp::detail::put_u16(&row[8], flush_time_wire); + } +} + +// apply_reconfig — REQ-RMAP-061/065, ported from c-RCP's +// rcp_regmap_response_queue_cfg_apply_reconfig(). Same render-patch- +// reparse idiom every pointed-to table in this file uses. A value parsed +// back for flush_time_us can never itself exceed 0xFFFF (the wire +// register's own full range), so no saturation is needed on this +// direction, unlike render()'s own. +inline std::error_code apply_reconfig(ResponseQueueConfig* 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(ResponseQueueCfgReconfigErrc::short_write); + if (count > kMaxEntries) return make_error_code(ResponseQueueCfgReconfigErrc::out_of_range); + + const size_t block_len = count * kRowLen; + if (static_cast(relative_start_address) + data_len > block_len) + return make_error_code(ResponseQueueCfgReconfigErrc::out_of_range); + + std::array block{}; + render(entries, count, block.data()); + for (size_t i = 0; i < data_len; ++i) block[relative_start_address + i] = data[i]; + + for (size_t i = 0; i < count; ++i) { + const uint8_t* row = &block[kRowLen * i]; + entries[i].stream_uid = avtp::detail::get_u16(&row[0]); + entries[i].max_avtpdu_size = avtp::detail::get_u16(&row[2]); + entries[i].queue_size = avtp::detail::get_u16(&row[4]); + entries[i].flush_on_count = avtp::detail::get_u16(&row[6]); + entries[i].flush_time_us = static_cast(avtp::detail::get_u16(&row[8])); + } + + return {}; +} + +} // namespace response_queue_cfg + +// ── Optional-subsystem config sections: Network/PHY/time-synch/security +// (REQ-RMAP-039, TC18 §12.7.11-.14) ──────────────────────────────────── +// Ported from c-RCP's rcp_regmap_optional_subsystem_cfg_t (include/rcp/ +// regmap.h:2965-3070, src/regmap.c). Unlike every other Table 20 +// pointed-to table this file models (HW_config, EP_ID_config, response- +// queue-config, request-stream-cfg, ep_generic_cfg), TC18 defines NO +// field-level layout for any of these four sections -- each one's own +// section text states verbatim "The content is product specific" +// (§12.7.11: "In case of an Ethernet interface this section comprised the +// entire MAC configuration"; §12.7.12: "might be empty" for +// MDIO-managed PHYs; §12.7.13: "Typically, gPTP (IEEE802.1AS)"; §12.7.14: +// "Typically, MacSec... and specifics of the key agreement"). There is +// therefore no row-typed struct here the way HW_config/EP_ID_config/etc. +// have one: each section is a flat, capacity-bounded, opaque byte buffer +// -- a conformant implementation's whole job is making that buffer +// reachable at its own advertised [ptr, ptr+capacity) extent, not +// interpreting what is inside it. GeneralMap already carries the four +// pointer/capacity pairs these sections are addressed through +// (svr_network_interface_cfg_ptr/_capacity, svr_physical_layer_cfg_ptr/ +// _capacity, svr_time_synch_cfg_ptr/_capacity, svr_security_cfg_ptr/ +// _capacity — REQ-RMAP-039, batch A). +// +// The EP0 address-routed dispatcher that would ROUTE a live ACF_ABB +// read/write to one of these sections by absolute address (c-RCP's +// rcp_regmap_ep0_decode_write_request()/_decode_read_request()/ +// _encode_read_response(), include/rcp/regmap.h:3072-3549) is +// DELIBERATELY NOT ported in this batch: it is a single ~500-line +// cross-cutting orchestrator tying together every sub-table this whole +// file models PLUS lifecycle-state/writer authorization (rcp_lifecycle_ +// field_writable()/_w_plus(), including three separate table-specific +// authorization carve-outs) PLUS SEQUENCER_config's own ownership-aware +// per-octet access control (REQ-SEQ-013) -- none of which ROADMAP.md +// Phase 4 batch B's own scope list (this file's own header comment, +// items 1-6) names. This section's own storage type and its +// apply_reconfig() wire codec ARE ported (REQ-RMAP-039 explicitly is in +// scope) since a caller can already exercise them directly; only the +// unified address-routing dispatcher is deferred, exactly the same +// "content/mechanism ported, dispatcher not" split batch A already +// established for the sequencer/HW-pin/request-stream/response-stream/ +// functional-config table pointers themselves. +namespace optional_subsystem_cfg { + +// kMaxOctets — this implementation's own storage bound per +// optional-subsystem section, NOT a TC18-mandated limit (TC18 leaves +// each section's own real capacity entirely up to the product, via its +// own svr_*_cfg_capacity register) — matches every sibling table's own +// MAX_ENTRIES-style bound. +constexpr size_t kMaxOctets = 256; + +} // namespace optional_subsystem_cfg + +// OptionalSubsystemCfg — one flat byte buffer + its own currently- +// configured length, for one of the four optional-subsystem sections +// above. len mirrors the corresponding svr_*_cfg_capacity register in +// GeneralMap (kept in sync by whichever call installs this buffer — this +// port's own future caller's job, mirroring REQ-RMAP-032/034/036/037's +// already-established capacity-sync convention for other tables). A +// default-constructed instance (len == 0) correctly means "nothing +// installed", matching this table's own zero pointer/capacity register +// default (GeneralMap's own default member initializers) — TC18's own +// defined "not supported" encoding for three of the four (physical-layer/ +// time-synch/security; network-interface has no such explicit note, see +// svr_network_interface_cfg_ptr's own field comment above). +struct OptionalSubsystemCfg { + std::array data{}; + size_t len = 0; +}; + +// OptionalSubsystemCfgReconfigErrc — REQ-RMAP-039, ported from c-RCP's +// rcp_regmap_optional_subsystem_cfg_reconfig_errc_t. +enum class OptionalSubsystemCfgReconfigErrc : int { + short_write = 1, + out_of_range = 2, }; +inline const std::error_category& optional_subsystem_cfg_reconfig_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.regmap.optional_subsystem_cfg_reconfig"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case OptionalSubsystemCfgReconfigErrc::short_write: + return "rcp/regmap: optional-subsystem config write has no data"; + case OptionalSubsystemCfgReconfigErrc::out_of_range: + return "rcp/regmap: optional-subsystem config write extends past the section's own current extent"; + default: + return "rcp/regmap: optional-subsystem config unknown configuration-write error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(OptionalSubsystemCfgReconfigErrc e) noexcept { + return {static_cast(e), optional_subsystem_cfg_reconfig_category()}; +} + +namespace optional_subsystem_cfg { + +// apply_reconfig — REQ-RMAP-039, ported from c-RCP's +// rcp_regmap_optional_subsystem_cfg_apply_reconfig(). Applies a write of +// data[0..data_len) at relative_start_address within cfg's own current +// extent ([0, cfg.len)) — a direct bounded copy, not the render-patch- +// reparse idiom every row-typed table's own apply_reconfig() needs, since +// an opaque byte buffer has no rows to reparse. Fails ::out_of_range if +// the write would extend past cfg.len (this section's own currently- +// configured capacity, not kMaxOctets), ::short_write if data_len == 0 -- +// matching every sibling apply_reconfig()'s own two failure modes exactly. +inline std::error_code apply_reconfig(OptionalSubsystemCfg& cfg, uint16_t relative_start_address, + const uint8_t* data, size_t data_len) noexcept { + if (data_len == 0u) return make_error_code(OptionalSubsystemCfgReconfigErrc::short_write); + if (static_cast(relative_start_address) + data_len > cfg.len) + return make_error_code(OptionalSubsystemCfgReconfigErrc::out_of_range); + + std::copy(data, data + data_len, cfg.data.begin() + relative_start_address); + return {}; +} + +} // namespace optional_subsystem_cfg + // ── svr_implemented_options bitmask ─────────────────────────────────────────── // // Legacy, pre-Phase-4 bitmask constants (uint32_t) — retained ONLY because @@ -726,7 +2024,7 @@ struct GeneralMap { // 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) + 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) -- content storage (RegisterMap::sequencer_states) exists; the wire codec for this specific table is not ported (REQ-SEQ-014, out of batch B's own scope -- see this file's own "Phase 4 batch B" banner) // REQ-RMAP-039 (Table 20 continued -- every address in this group is // INFERRED, not directly read: TC18's own "Absolute address" column is @@ -992,26 +2290,25 @@ struct EpClient { // "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. +// REQ-LIFECYCLE-025/031 (issue #341 lineage, 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 is evaluated for real +// via ep_id_map::is_valid_association() (above) -- batch A's own +// EpIdMappingEntry did not yet carry request_stream_index, so this member +// was pinned fail-closed (always false) until that landed; it now +// evaluates a genuine, matching (request_stream_index, byte_bus_id) row. +// The "no root client configured at all" condition is baked directly into +// this member (not left to a caller to re-check separately), the same +// pattern via_root_client_ep0 above already establishes for its own +// svr_root_client_index check -- this member can therefore never wrongly +// widen access when a root client IS configured. requesting_stream_index +// is narrowed to uint8_t to match EpIdMappingEntry::request_stream_index's +// own field width (TC18 §12.7.8 Table 25: an 8-bit register). 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 && @@ -1026,8 +2323,11 @@ inline lifecycle::WriterCtx writer_ctx(const GeneralMap& map, const EpClient* ep // 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; + ctx.via_valid_stream_association = + map.svr_root_client_index == kNoRootClient && + ep_id_map::is_valid_association(ep_id_map, ep_id_map_count, + static_cast(requesting_stream_index), + requesting_byte_bus_id); return ctx; } @@ -1061,9 +2361,15 @@ struct SvrEpCfg { // ── 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. +// describe. general/svr_ep_cfg are batch A's own GeneralMap/SvrEpCfg +// content; the pointer/capacity TablePointer fields below them remain this +// codebase's own pre-rewrite v2.x shape, unchanged by either batch (nothing +// in c-RCP's own Table 20 bundles pointer+capacity as one struct the way +// TablePointer does — see TablePointer's own doc comment above); the +// table-content vectors are this batch's own extended/replaced types +// (HwPinMapEntry, RequestStreamConfig, ResponseQueueConfig, +// EpIdMappingEntry — see each type's own doc comment above for what +// changed) plus the four new OptionalSubsystemCfg fields. struct RegisterMap { GeneralMap general{}; // TC18 §12.7.5 Table 20 -- rcp_regmap_general_t @@ -1089,6 +2395,17 @@ struct RegisterMap { // Persistent sequencer-state storage (extraction §3.11, §3.16). std::vector sequencer_states; + + // The four optional-subsystem sections (REQ-RMAP-039) — content + // storage only; GeneralMap's own svr_network_interface_cfg_ptr/ + // _capacity (etc.) fields are this map's pointer/capacity registers + // for these, kept in sync by whichever caller installs a buffer here. + // A default-constructed (len == 0) instance means "not supported", + // matching GeneralMap's own zero pointer/capacity default. + OptionalSubsystemCfg network_interface_cfg{}; + OptionalSubsystemCfg physical_layer_cfg{}; + OptionalSubsystemCfg time_synch_cfg{}; + OptionalSubsystemCfg security_cfg{}; }; // ── EP0 -- RC Server as a pseudo-endpoint ───────────────────────────────────── @@ -1316,8 +2633,7 @@ class Ep0 { } // namespace regmap } // namespace rcp -// Enable std::error_code construction from rcp::regmap::RegMapErrc/ -// GeneralMapErrc/EpGenericCfgReconfigErrc. +// Enable std::error_code construction from every rcp::regmap error enum. namespace std { template <> struct is_error_code_enum : true_type {}; @@ -1325,4 +2641,14 @@ template <> struct is_error_code_enum : true_type {}; template <> struct is_error_code_enum : true_type {}; +template <> +struct is_error_code_enum : true_type {}; +template <> +struct is_error_code_enum : true_type {}; +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_regmap.cpp b/tests/test_regmap.cpp index 15d926c..a1e8d67 100644 --- a/tests/test_regmap.cpp +++ b/tests/test_regmap.cpp @@ -562,18 +562,39 @@ TEST_CASE("writer_ctx plumbs via_discovery_stream straight through", "[regmap][R 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", +// REQ-LIFECYCLE-025/031 (issue #341 lineage, batch B): writer_ctx() now +// evaluates a real, matching EP-ID/byte_bus_id association via +// ep_id_map::is_valid_association() (batch A's own fail-closed stub is +// gone) -- see writer_ctx()'s own doc comment. +TEST_CASE("writer_ctx grants via_valid_stream_association for a real, matching association when no root client", "[regmap][REQ-LIFECYCLE-025]") { GeneralMap map; // no root client configured - EpIdMappingEntry entries[1] = {{1, 0x100}}; + EpIdMappingEntry entries[1] = {{1, 0x100, 2}}; // ep 1, bbid 0x100, stream 2 auto ctx = writer_ctx(map, nullptr, 2, false, true, false, 0x100, entries, 1); + REQUIRE(ctx.via_valid_stream_association); + REQUIRE_FALSE(ctx.via_root_client_ep0); // independent axis, not conflated +} + +TEST_CASE("writer_ctx denies via_valid_stream_association once a root client is configured", + "[regmap][REQ-LIFECYCLE-025]") { + GeneralMap map; + map.svr_root_client_index = 7; // a root client IS configured + EpIdMappingEntry entries[1] = {{1, 0x100, 2}}; + + // Same otherwise-valid (stream 2, bbid 0x100) association as the test + // above -- denied purely because a root client now exists. + auto ctx = writer_ctx(map, nullptr, 2, false, true, false, 0x100, entries, 1); + REQUIRE_FALSE(ctx.via_valid_stream_association); +} + +TEST_CASE("writer_ctx denies via_valid_stream_association for an unrecognized pair", + "[regmap][REQ-LIFECYCLE-025]") { + GeneralMap map; // no root client + EpIdMappingEntry entries[1] = {{1, 0x100, 2}}; + + // Right stream, wrong byte_bus_id -- not a real association. + auto ctx = writer_ctx(map, nullptr, 2, false, true, false, 0x200, entries, 1); REQUIRE_FALSE(ctx.via_valid_stream_association); } @@ -583,21 +604,22 @@ TEST_CASE("writer_ctx explicitly assigns every member of the returned WriterCtx" EpClient owner; owner.has_owning_stream = true; owner.owning_stream_index = 7; + EpIdMappingEntry entries[1] = {{1, 0x100, 2}}; - auto ctx = writer_ctx(map, &owner, 7, true, false, true, 0x100, nullptr, 0); + auto ctx = writer_ctx(map, &owner, 7, true, false, true, 0x100, entries, 1); 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); + REQUIRE_FALSE(ctx.via_valid_stream_association); // deterministically false: a root client IS configured GeneralMap no_root; // svr_root_client_index == kNoRootClient - auto ctx2 = writer_ctx(no_root, nullptr, 2, false, true, false, 0x100, nullptr, 0); + auto ctx2 = writer_ctx(no_root, nullptr, 2, false, true, false, 0x100, entries, 1); 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); + REQUIRE(ctx2.via_valid_stream_association); // the one member Call A couldn't exercise as true } // ── RC Server's own functional-configuration content (SvrEpCfg) ────────────────── @@ -865,7 +887,7 @@ TEST_CASE("ep_generic_cfg::apply_reconfig rejects a zero-length write and an out make_error_code(EpGenericCfgReconfigErrc::out_of_range)); } -// ── EP-ID / byte_bus_id mapping table (batch B, pre-existing) ──────────────────── +// ── EP-ID / byte_bus_id mapping table ──────────────────────────────────────────── TEST_CASE("EP-ID mapping table preserves client insertion order without re-sorting it", "[regmap][REQ-REGMAP-010]") { @@ -879,16 +901,22 @@ TEST_CASE("EP-ID mapping table preserves client insertion order without re-sorti REQUIRE(m.ep_id_mapping[0].ep_id == 3); REQUIRE(m.ep_id_mapping[1].ep_id == 1); REQUIRE(m.ep_id_mapping[2].ep_id == 2); + + // The two-element positional init above still compiles and zero-defaults + // the two fields batch B appended (REQ-RMAP-052/053) — this is the exact + // compatibility batch B's own EpIdMappingEntry doc comment relies on. + REQUIRE(m.ep_id_mapping[0].request_stream_index == 0); + REQUIRE_FALSE(m.ep_id_mapping[0].crc_required); } -// ── Response / ack queue config (batch B, pre-existing) ────────────────────────── +// ── Response / ack queue config (TC18 §12.7.9 Table 27) ────────────────────────── TEST_CASE("ResponseQueueConfig fields exist and are settable", "[regmap][REQ-REGMAP-011]") { ResponseQueueConfig rqc; - rqc.response_queue_size = 6; - rqc.ack_queue_size = 6; - REQUIRE(rqc.response_queue_size == 6); - REQUIRE(rqc.ack_queue_size == 6); + rqc.queue_size = 6; + rqc.flush_on_count = 6; + REQUIRE(rqc.queue_size == 6); + REQUIRE(rqc.flush_on_count == 6); } // ── Sequencer-state persistence (batch B, pre-existing) ────────────────────────── @@ -1103,3 +1131,774 @@ TEST_CASE("EpGenericCfgReconfigErrc values are distinct and carry non-empty mess REQUIRE_FALSE(short_write.message().empty()); REQUIRE_FALSE(out_of_range.message().empty()); } + +// ═══════════════════════════════════════════════════════════════════════════ +// Phase 4 batch B (cpp-RCP issue #129, ROADMAP.md "Phase 17") — ported from +// c-RCP's tests/test_regmap.c and the relevant slice of +// tests/test_tc18_gaps_regmap.c: +// fusa:test REQ-RMAP-017 +// fusa:test REQ-RMAP-040 +// fusa:test REQ-RMAP-041 +// fusa:test REQ-RMAP-042 +// fusa:test REQ-RMAP-044 +// fusa:test REQ-RMAP-045 +// fusa:test REQ-RMAP-047 +// fusa:test REQ-RMAP-048 +// fusa:test REQ-RMAP-049 +// fusa:test REQ-RMAP-050 +// fusa:test REQ-RMAP-051 +// fusa:test REQ-RMAP-052 +// fusa:test REQ-RMAP-053 +// fusa:test REQ-RMAP-054 +// fusa:test REQ-RMAP-056 +// fusa:test REQ-RMAP-057 +// fusa:test REQ-RMAP-058 +// fusa:test REQ-RMAP-060 +// fusa:test REQ-RMAP-061 +// fusa:test REQ-RMAP-063 +// fusa:test REQ-RMAP-065 +// fusa:test REQ-RMAP-071 +// fusa:test REQ-RMAP-083 +// fusa:test REQ-RMAP-084 +// fusa:test REQ-WAKEUP-020 +// fusa:test REQ-E2E-029 +// fusa:test REQ-E2E-030 +// fusa:test REQ-E2E-045 +// fusa:test REQ-E2E-046 +// fusa:test REQ-LIFECYCLE-025 +// fusa:test REQ-LIFECYCLE-031 +// ═══════════════════════════════════════════════════════════════════════════ + +// ── EpFunctionalCfg (content-modeling only) ────────────────────────────────── + +TEST_CASE("EpFunctionalCfg default-constructs zeroed and is independently settable", + "[regmap][REQ-RMAP-017]") { + EpFunctionalCfg cfg; + REQUIRE_FALSE(cfg.ep_enable); + REQUIRE_FALSE(cfg.ep_clear_req_storage); + REQUIRE_FALSE(cfg.ep_req_crc_enable); + REQUIRE_FALSE(cfg.ep_response_ts_enable); + REQUIRE_FALSE(cfg.ep_suppress_response); + + cfg.ep_enable = true; + cfg.ep_req_crc_enable = true; + REQUIRE(cfg.ep_enable); + REQUIRE(cfg.ep_req_crc_enable); + REQUIRE_FALSE(cfg.ep_clear_req_storage); +} + +// ── HW pin mapping (TC18 §12.7.6 Tables 21/22) ─────────────────────────────── + +TEST_CASE("HwPinMapEntry defaults to all-zero and matches c-RCP's row shape", "[regmap][REQ-RMAP-042]") { + HwPinMapEntry e; + REQUIRE(e.hw_ep_nr == 0); + REQUIRE(e.hw_ep_pin_nr == 0); + REQUIRE(e.hw_pin_type == 0); +} + +TEST_CASE("hw_pin bit-layout constants are non-overlapping within their own sub-field", + "[regmap][REQ-RMAP-042]") { + REQUIRE((hw_pin::kPullMask & hw_pin::kStageMask) == 0); + REQUIRE((hw_pin::kStageMask & hw_pin::kDriveMask) == 0); + REQUIRE((hw_pin::kDriveMask & hw_pin::kSchmittTrigger) == 0); + REQUIRE(hw_pin::kStageInput == 0); + REQUIRE(hw_pin::kStagePushPull == hw_pin::kStageMask); +} + +TEST_CASE("hw_pin_map::render places each row at its own 3-octet stride", "[regmap][REQ-RMAP-040]") { + HwPinMapEntry rows[2]; + rows[0].hw_ep_nr = 1; + rows[0].hw_ep_pin_nr = 2; + rows[0].hw_pin_type = hw_pin::kStagePushPull | hw_pin::kPullUp; + rows[1].hw_ep_nr = 3; + rows[1].hw_ep_pin_nr = 4; + rows[1].hw_pin_type = hw_pin::kSchmittTrigger; + + uint8_t out[6]; + hw_pin_map::render(rows, 2, out); + REQUIRE(out[0] == 1); + REQUIRE(out[1] == 2); + REQUIRE(out[2] == (hw_pin::kStagePushPull | hw_pin::kPullUp)); + REQUIRE(out[3] == 3); + REQUIRE(out[4] == 4); + REQUIRE(out[5] == hw_pin::kSchmittTrigger); +} + +TEST_CASE("hw_pin_map::apply_reconfig patches only the addressed octets", "[regmap][REQ-RMAP-041]") { + HwPinMapEntry rows[2]; + rows[0].hw_ep_nr = 9; // must survive if not addressed + + const uint8_t patch[3] = {1, 2, hw_pin::kPullDown}; + auto ec = hw_pin_map::apply_reconfig(rows, 2, 3, patch, sizeof(patch)); + REQUIRE_FALSE(ec); + REQUIRE(rows[0].hw_ep_nr == 9); // row 0 untouched + REQUIRE(rows[1].hw_ep_nr == 1); + REQUIRE(rows[1].hw_ep_pin_nr == 2); + REQUIRE(rows[1].hw_pin_type == hw_pin::kPullDown); +} + +TEST_CASE("hw_pin_map::apply_reconfig rejects a zero-length write and an out-of-range write", + "[regmap][REQ-RMAP-041]") { + HwPinMapEntry row; + const uint8_t patch[1] = {0}; + + REQUIRE(hw_pin_map::apply_reconfig(&row, 1, 0, patch, 0) == + make_error_code(HwPinMapReconfigErrc::short_write)); + REQUIRE(hw_pin_map::apply_reconfig(&row, 1, 3, patch, 1) == + make_error_code(HwPinMapReconfigErrc::out_of_range)); +} + +TEST_CASE("HwPinMapReconfigErrc values are distinct and carry non-empty messages", "[regmap][REQ-RMAP-041]") { + auto short_write = make_error_code(HwPinMapReconfigErrc::short_write); + auto out_of_range = make_error_code(HwPinMapReconfigErrc::out_of_range); + REQUIRE(short_write.category() == hw_pin_map_reconfig_category()); + REQUIRE(short_write != out_of_range); + REQUIRE_FALSE(short_write.message().empty()); + REQUIRE_FALSE(out_of_range.message().empty()); +} + +// ── Per-endpoint-type named-signal index (TC18 §12.7.6 Table 23) ──────────── + +TEST_CASE("named_signal_string never returns an empty string for a valid signal", + "[regmap][REQ-RMAP-044]") { + for (uint8_t i = 0; i < static_cast(NamedSignal::Count); ++i) { + auto sig = static_cast(i); + REQUIRE_FALSE(std::string(named_signal_string(sig)).empty()); + } +} + +TEST_CASE("named_signal_string returns \"unknown\" for an out-of-range value", "[regmap][REQ-RMAP-044]") { + REQUIRE(std::string(named_signal_string(NamedSignal::Count)) == "unknown"); + REQUIRE(std::string(named_signal_string(static_cast(0xFF))) == "unknown"); +} + +TEST_CASE("named_signal_string names are unique across the whole index", "[regmap][REQ-RMAP-044]") { + std::vector names; + for (uint8_t i = 0; i < static_cast(NamedSignal::Count); ++i) { + names.emplace_back(named_signal_string(static_cast(i))); + } + for (size_t i = 0; i < names.size(); ++i) { + for (size_t j = i + 1; j < names.size(); ++j) { + REQUIRE(names[i] != names[j]); + } + } +} + +TEST_CASE("named_signal_ep_signal_nr restarts at 0 for each endpoint type", "[regmap][REQ-RMAP-045]") { + REQUIRE(named_signal_ep_signal_nr(NamedSignal::Gpio0) == 0); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::Gpio31) == 31); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::SpiClk) == 0); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::SpiCs5) == 8); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::I2cScl) == 0); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::I2cSda) == 1); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::UartTx) == 0); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::UartCts) == 3); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::LinTxd) == 0); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::PwmOut) == 0); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::PwmOutn) == 1); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::PwmIn) == 0); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::AdcIn) == 0); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::DacOut) == 0); + // TC18's own counter-intuitive order: RXD=0, TXD=1. + REQUIRE(named_signal_ep_signal_nr(NamedSignal::CanRxd) == 0); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::CanTxd) == 1); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::IseledIspP) == 0); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::IseledIspN) == 1); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::MdioMdc) == 0); + REQUIRE(named_signal_ep_signal_nr(NamedSignal::MdioData) == 1); +} + +TEST_CASE("named_signal_ep_signal_nr returns 0 for Count or any other invalid value", + "[regmap][REQ-RMAP-045]") { + REQUIRE(named_signal_ep_signal_nr(NamedSignal::Count) == 0); + REQUIRE(named_signal_ep_signal_nr(static_cast(0xFF)) == 0); +} + +// ── Request-stream config: appended fields, boundary conversions ──────────── + +TEST_CASE("RequestStreamConfig's batch-B-appended fields default per TC18's own power-on rule", + "[regmap][REQ-RMAP-047]") { + RequestStreamConfig cfg; + REQUIRE(cfg.rx_secure_channel_index == 0); + REQUIRE(cfg.rx_ack_stream_index == 0); + REQUIRE(cfg.rx_resp_stream_index == 1); // REQ-RMAP-049: power-on default is 1, not 0 + REQUIRE(cfg.rx_stream_max_request_size == 0); + + // Pre-existing fields batch A already established are untouched. + REQUIRE_FALSE(cfg.rx_wd_enable); + REQUIRE(cfg.rx_safety_measure == RxSafetyMeasure::ForceHighImpedance); +} + +TEST_CASE("request_stream_cfg::wd_timeout_ms_to_ticks rounds down and bounds-checks", + "[regmap][REQ-RMAP-050]") { + uint16_t ticks = 0xFFFF; + REQUIRE(request_stream_cfg::wd_timeout_ms_to_ticks(1000, 10, ticks)); + REQUIRE(ticks == 100); + + // Rounds down: 105ms / 10ms-per-tick = 10.5 -> 10 ticks, never 11. + REQUIRE(request_stream_cfg::wd_timeout_ms_to_ticks(105, 10, ticks)); + REQUIRE(ticks == 10); + + REQUIRE_FALSE(request_stream_cfg::wd_timeout_ms_to_ticks(1000, 0, ticks)); // no zero-length tick + REQUIRE(ticks == 10); // untouched on rejection + + REQUIRE_FALSE(request_stream_cfg::wd_timeout_ms_to_ticks(0xFFFFFFFFu, 1, ticks)); // overflows 16 bit +} + +TEST_CASE("request_stream_cfg::wd_timeout_ticks_to_ms round-trips and rejects a zero rate", + "[regmap][REQ-RMAP-050]") { + uint32_t ms = 0xFFFFFFFFu; + REQUIRE(request_stream_cfg::wd_timeout_ticks_to_ms(100, 10, ms)); + REQUIRE(ms == 1000); + + REQUIRE_FALSE(request_stream_cfg::wd_timeout_ticks_to_ms(100, 0, ms)); + REQUIRE(ms == 1000); // untouched on rejection +} + +// ── request-stream-cfg wire codec (issue #306/#458) ────────────────────────── + +TEST_CASE("request_stream_cfg::render places stream_id, secure channel, and ack/resp indices", + "[regmap][REQ-RMAP-047]") { + RequestStreamConfig row; + row.stream_id.mac = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; + row.stream_id.suffix = 0x0102; + row.rx_secure_channel_index = 5; + row.rx_ack_stream_index = 2; + row.rx_resp_stream_index = 3; + + uint8_t out[24]; + request_stream_cfg::render(&row, 1, out, /*watchdog_ms_per_tick=*/0, nullptr); + + REQUIRE(avtp::detail::get_u64(&out[0x0000]) == row.stream_id.to_u64()); + REQUIRE(out[0x000C] == 5); + REQUIRE(out[0x0010] == 2); + REQUIRE(out[0x0011] == 3); +} + +TEST_CASE("request_stream_cfg::render saturates an oversized rx_stream_max_request_size", + "[regmap][REQ-RMAP-071]") { + RequestStreamConfig row; + row.rx_stream_max_request_size = 0x1FFFF; // one past 16-bit max + + uint8_t out[24]; + request_stream_cfg::render(&row, 1, out, 0, nullptr); + REQUIRE(avtp::detail::get_u16(&out[0x0008]) == 0xFFFF); +} + +TEST_CASE("request_stream_cfg::render saturates an oversized rx_safestate_sequencer", + "[regmap][REQ-RMAP-047]") { + RequestStreamConfig row; + row.rx_safestate_sequencer = 0x1FF; // one past 8-bit max + + uint8_t out[24]; + request_stream_cfg::render(&row, 1, out, 0, nullptr); + REQUIRE(out[0x000E] == 0xFF); +} + +TEST_CASE("request_stream_cfg::render couples the sequence/watchdog bits with AND, not OR", + "[regmap][REQ-RMAP-051]") { + uint8_t out[24]; + + // Only the "block" dimension set -- must NOT render as if safe-state + // were also entered (that would overstate a safety guarantee). + RequestStreamConfig only_block; + only_block.rx_enforce_seq = true; + only_block.rx_seq_safestate_enable = false; + request_stream_cfg::render(&only_block, 1, out, 0, nullptr); + REQUIRE((out[0x000D] & 0x02u) == 0x00u); + + RequestStreamConfig both; + both.rx_enforce_seq = true; + both.rx_seq_safestate_enable = true; + request_stream_cfg::render(&both, 1, out, 0, nullptr); + REQUIRE((out[0x000D] & 0x02u) == 0x02u); + + RequestStreamConfig wd_only_block; + wd_only_block.rx_wd_enable = true; + wd_only_block.rx_wd_safestate_enable = false; + request_stream_cfg::render(&wd_only_block, 1, out, 0, nullptr); + REQUIRE((out[0x000D] & 0x04u) == 0x00u); + + RequestStreamConfig wd_both; + wd_both.rx_wd_enable = true; + wd_both.rx_wd_safestate_enable = true; + request_stream_cfg::render(&wd_both, 1, out, 0, nullptr); + REQUIRE((out[0x000D] & 0x04u) == 0x04u); +} + +TEST_CASE("request_stream_cfg::render packs rx_enforce_e2e and rx_ovrflw_safestate_enable directly", + "[regmap][REQ-RMAP-051]") { + RequestStreamConfig row; + row.rx_enforce_e2e = true; + row.rx_ovrflw_safestate_enable = true; + + uint8_t out[24]; + request_stream_cfg::render(&row, 1, out, 0, nullptr); + REQUIRE((out[0x000D] & 0x01u) != 0); + REQUIRE((out[0x000D] & 0x08u) != 0); + REQUIRE((out[0x000D] & 0x70u) == 0); // bits [6:4] reserved, always 0 +} + +TEST_CASE("request_stream_cfg::render wires rx_stream_status from the live-status array", + "[regmap][REQ-E2E-046]") { + RequestStreamConfig rows[2]; + const bool blocked[2] = {false, true}; + + uint8_t out[48]; + request_stream_cfg::render(rows, 2, out, 0, blocked); + REQUIRE((out[0x000D] & 0x80u) == 0x00u); + REQUIRE((out[24 + 0x000D] & 0x80u) == 0x80u); + + // nullptr means "no live status known" -- bit 7 renders 0 for every row. + request_stream_cfg::render(rows, 2, out, 0, nullptr); + REQUIRE((out[0x000D] & 0x80u) == 0x00u); + REQUIRE((out[24 + 0x000D] & 0x80u) == 0x00u); +} + +TEST_CASE("request_stream_cfg::render falls back to 0x0000 when the watchdog tick rate is unconfigured", + "[regmap][REQ-RMAP-050]") { + RequestStreamConfig row; + row.rx_wd_timeout_interval = 1000; + + uint8_t out[24]; + request_stream_cfg::render(&row, 1, out, /*watchdog_ms_per_tick=*/0, nullptr); + REQUIRE(out[0x000A] == 0x00); + REQUIRE(out[0x000B] == 0x00); + + request_stream_cfg::render(&row, 1, out, /*watchdog_ms_per_tick=*/10, nullptr); + REQUIRE(avtp::detail::get_u16(&out[0x000A]) == 100); +} + +TEST_CASE("request_stream_cfg::apply_reconfig round-trips stream_id and the coupled bit pairs", + "[regmap][REQ-RMAP-047]") { + RequestStreamConfig row; + row.stream_id.mac = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + row.stream_id.suffix = 0x1234; + row.rx_wd_timeout_interval = 500; + + uint8_t block[24]; + request_stream_cfg::render(&row, 1, block, /*watchdog_ms_per_tick=*/10, nullptr); + block[0x000D] = 0x0F; // set all 4 real content bits + + RequestStreamConfig out_row; + auto ec = request_stream_cfg::apply_reconfig(&out_row, 1, 0, block, sizeof(block), + /*watchdog_ms_per_tick=*/10); + REQUIRE_FALSE(ec); + REQUIRE(out_row.stream_id == row.stream_id); + REQUIRE(out_row.rx_wd_timeout_interval == 500); + REQUIRE(out_row.rx_enforce_e2e); + REQUIRE(out_row.rx_enforce_seq); + REQUIRE(out_row.rx_seq_safestate_enable); // both dimensions set together from one bit + REQUIRE(out_row.rx_wd_enable); + REQUIRE(out_row.rx_wd_safestate_enable); + REQUIRE(out_row.rx_ovrflw_safestate_enable); +} + +TEST_CASE("request_stream_cfg::apply_reconfig leaves rx_wd_timeout_interval unchanged when the tick rate is unconfigured", + "[regmap][REQ-RMAP-050]") { + RequestStreamConfig row; + row.rx_wd_timeout_interval = 777; // pre-existing value + + uint8_t block[24]; + request_stream_cfg::render(&row, 1, block, 0, nullptr); + + auto ec = request_stream_cfg::apply_reconfig(&row, 1, 0, block, sizeof(block), + /*watchdog_ms_per_tick=*/0); + REQUIRE_FALSE(ec); + REQUIRE(row.rx_wd_timeout_interval == 777); // untouched, not zeroed +} + +TEST_CASE("request_stream_cfg::apply_reconfig rejects a zero-length write and an out-of-range write", + "[regmap][REQ-RMAP-047]") { + RequestStreamConfig row; + const uint8_t patch[1] = {0}; + + REQUIRE(request_stream_cfg::apply_reconfig(&row, 1, 0, patch, 0, 10) == + make_error_code(RequestStreamCfgReconfigErrc::short_write)); + REQUIRE(request_stream_cfg::apply_reconfig(&row, 1, 24, patch, 1, 10) == + make_error_code(RequestStreamCfgReconfigErrc::out_of_range)); +} + +TEST_CASE("RequestStreamCfgReconfigErrc values are distinct and carry non-empty messages", + "[regmap][REQ-RMAP-047]") { + auto short_write = make_error_code(RequestStreamCfgReconfigErrc::short_write); + auto out_of_range = make_error_code(RequestStreamCfgReconfigErrc::out_of_range); + REQUIRE(short_write.category() == request_stream_cfg_reconfig_category()); + REQUIRE(short_write != out_of_range); + REQUIRE_FALSE(short_write.message().empty()); + REQUIRE_FALSE(out_of_range.message().empty()); +} + +TEST_CASE("request_stream_cfg::resolve_index matches by stream_id and returns a 1-based index", + "[regmap][REQ-SEQ-013]") { + RequestStreamConfig rows[2]; + rows[0].stream_id = avtp::StreamId::from_u64(100); + rows[1].stream_id = avtp::StreamId::from_u64(200); + + REQUIRE(request_stream_cfg::resolve_index(rows, 2, 100) == 1); + REQUIRE(request_stream_cfg::resolve_index(rows, 2, 200) == 2); +} + +TEST_CASE("request_stream_cfg::resolve_index returns the 0 sentinel for no match, null, or empty", + "[regmap][REQ-SEQ-013]") { + RequestStreamConfig rows[1]; + rows[0].stream_id = avtp::StreamId::from_u64(100); + + REQUIRE(request_stream_cfg::resolve_index(rows, 1, 999) == 0); + REQUIRE(request_stream_cfg::resolve_index(nullptr, 0, 100) == 0); + REQUIRE(request_stream_cfg::resolve_index(rows, 0, 100) == 0); +} + +// ── Response / ack queue config wire codec (TC18 §12.7.9 Table 27) ────────── + +TEST_CASE("ResponseQueueConfig matches c-RCP's real per-queue row shape", "[regmap][REQ-RMAP-059]") { + ResponseQueueConfig cfg; + REQUIRE(cfg.stream_uid == 0); + REQUIRE(cfg.max_avtpdu_size == 0); + REQUIRE(cfg.queue_size == 0); + REQUIRE(cfg.flush_on_count == 0); + REQUIRE(cfg.flush_time_us == 0); +} + +TEST_CASE("response_queue_stream_id combines stream_uid with the interface's own mac", + "[regmap][REQ-RMAP-060]") { + ResponseQueueConfig cfg; + cfg.stream_uid = 0xABCD; + std::array mac{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; + + auto id = response_queue_stream_id(cfg, mac); + REQUIRE(id.mac == mac); + REQUIRE(id.suffix == 0xABCD); +} + +TEST_CASE("response_queue_cfg::render places each field at its own TC18-cited byte offset", + "[regmap][REQ-RMAP-061]") { + ResponseQueueConfig cfg; + cfg.stream_uid = 0x1122; + cfg.max_avtpdu_size = 0x3344; + cfg.queue_size = 0x5566; + cfg.flush_on_count = 0x7788; + cfg.flush_time_us = 0x99AA; + + uint8_t out[10]; + response_queue_cfg::render(&cfg, 1, out); + REQUIRE(avtp::detail::get_u16(&out[0]) == 0x1122); + REQUIRE(avtp::detail::get_u16(&out[2]) == 0x3344); + REQUIRE(avtp::detail::get_u16(&out[4]) == 0x5566); + REQUIRE(avtp::detail::get_u16(&out[6]) == 0x7788); + REQUIRE(avtp::detail::get_u16(&out[8]) == 0x99AA); +} + +TEST_CASE("response_queue_cfg::render saturates an oversized flush_time_us without wrapping", + "[regmap][REQ-RMAP-065]") { + ResponseQueueConfig cfg; + cfg.flush_time_us = 0x10000; // one past the 16-bit wire register's own max + + uint8_t out[10]; + response_queue_cfg::render(&cfg, 1, out); + REQUIRE(avtp::detail::get_u16(&out[8]) == 0xFFFF); // saturated, not wrapped to 0x0000 +} + +TEST_CASE("response_queue_cfg::apply_reconfig patches only the addressed octets", + "[regmap][REQ-RMAP-061]") { + ResponseQueueConfig rows[2]; + rows[0].stream_uid = 0xAAAA; // must survive if not addressed + + const uint8_t patch[10] = {0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05}; + auto ec = response_queue_cfg::apply_reconfig(rows, 2, 10, patch, sizeof(patch)); + REQUIRE_FALSE(ec); + REQUIRE(rows[0].stream_uid == 0xAAAA); + REQUIRE(rows[1].stream_uid == 0x0001); + REQUIRE(rows[1].max_avtpdu_size == 0x0002); + REQUIRE(rows[1].queue_size == 0x0003); + REQUIRE(rows[1].flush_on_count == 0x0004); + REQUIRE(rows[1].flush_time_us == 0x0005); +} + +TEST_CASE("response_queue_cfg::apply_reconfig rejects a zero-length write and an out-of-range write", + "[regmap][REQ-RMAP-061]") { + ResponseQueueConfig row; + const uint8_t patch[1] = {0}; + + REQUIRE(response_queue_cfg::apply_reconfig(&row, 1, 0, patch, 0) == + make_error_code(ResponseQueueCfgReconfigErrc::short_write)); + REQUIRE(response_queue_cfg::apply_reconfig(&row, 1, 10, patch, 1) == + make_error_code(ResponseQueueCfgReconfigErrc::out_of_range)); +} + +TEST_CASE("ResponseQueueCfgReconfigErrc values are distinct and carry non-empty messages", + "[regmap][REQ-RMAP-061]") { + auto short_write = make_error_code(ResponseQueueCfgReconfigErrc::short_write); + auto out_of_range = make_error_code(ResponseQueueCfgReconfigErrc::out_of_range); + REQUIRE(short_write.category() == response_queue_cfg_reconfig_category()); + REQUIRE(short_write != out_of_range); + REQUIRE_FALSE(short_write.message().empty()); + REQUIRE_FALSE(out_of_range.message().empty()); +} + +// ── EP-ID / byte_bus_id map: appended fields, diagnostics, wire codec ──────── + +TEST_CASE("EpIdMappingEntry's appended fields default false/0", "[regmap][REQ-RMAP-052]") { + EpIdMappingEntry e; + REQUIRE(e.request_stream_index == 0); + REQUIRE_FALSE(e.crc_required); +} + +TEST_CASE("ep_id_map::is_ascending is true for strictly increasing composite keys", + "[regmap][REQ-RMAP-056]") { + EpIdMappingEntry entries[3] = {{1, 10, 1, false}, {2, 20, 1, false}, {3, 5, 2, false}}; + // stream 1: bbid 10 < 20 (ascending); stream 2 > stream 1 (always ascending + // regardless of its own bbid, even though 5 < 20). + REQUIRE(ep_id_map::is_ascending(entries, 3)); +} + +TEST_CASE("ep_id_map::is_ascending is false for an equal or descending byte_bus_id within one stream", + "[regmap][REQ-RMAP-056]") { + EpIdMappingEntry equal_adjacent[2] = {{1, 10, 1, false}, {2, 10, 1, false}}; + REQUIRE_FALSE(ep_id_map::is_ascending(equal_adjacent, 2)); + + EpIdMappingEntry descending[2] = {{1, 20, 1, false}, {2, 10, 1, false}}; + REQUIRE_FALSE(ep_id_map::is_ascending(descending, 2)); +} + +TEST_CASE("ep_id_map::is_ascending is false for a decreasing request_stream_index", + "[regmap][REQ-RMAP-056]") { + EpIdMappingEntry entries[2] = {{1, 10, 2, false}, {2, 20, 1, false}}; + REQUIRE_FALSE(ep_id_map::is_ascending(entries, 2)); +} + +TEST_CASE("ep_id_map::is_ascending is vacuously true for zero or one entries", "[regmap][REQ-RMAP-056]") { + REQUIRE(ep_id_map::is_ascending(nullptr, 0)); + EpIdMappingEntry one[1] = {{1, 10, 1, false}}; + REQUIRE(ep_id_map::is_ascending(one, 1)); +} + +TEST_CASE("ep_id_map::effective_count stops at the first request_stream_index==0 sentinel", + "[regmap][REQ-RMAP-054]") { + EpIdMappingEntry entries[4] = {{1, 1, 1, false}, {2, 2, 1, false}, {0, 0, 0, false}, {4, 4, 1, false}}; + REQUIRE(ep_id_map::effective_count(entries, 4) == 2); +} + +TEST_CASE("ep_id_map::effective_count returns capacity unchanged when no sentinel exists", + "[regmap][REQ-RMAP-054]") { + EpIdMappingEntry entries[2] = {{1, 1, 1, false}, {2, 2, 1, false}}; + REQUIRE(ep_id_map::effective_count(entries, 2) == 2); +} + +TEST_CASE("ep_id_map::row_init_default permits EP0 access before any configuration is written", + "[regmap][REQ-RMAP-084]") { + EpIdMappingEntry row; + row.ep_id = 99; + row.request_stream_index = 0; + ep_id_map::row_init_default(row); + REQUIRE(row.request_stream_index == 1); // smallest valid index, not the end-of-table sentinel + REQUIRE(row.ep_id == kEp0); + REQUIRE(row.byte_bus_id == 0); +} + +TEST_CASE("ep_id_map::render packs BBID into bits[15:5] and crc_required into bit 4", + "[regmap][REQ-RMAP-053]") { + EpIdMappingEntry row; + row.request_stream_index = 3; + row.ep_id = 7; + row.byte_bus_id = 0x0100; + row.crc_required = true; + + uint8_t out[4]; + ep_id_map::render(&row, 1, out); + REQUIRE(out[0] == 3); + REQUIRE(out[1] == 7); + const uint16_t bbid_ctrl = avtp::detail::get_u16(&out[2]); + REQUIRE(((bbid_ctrl >> 5) & 0x07FFu) == 0x0100); + REQUIRE((bbid_ctrl & 0x10u) != 0); + REQUIRE((bbid_ctrl & 0x0Fu) == 0); // Channel_selection always 0 +} + +TEST_CASE("ep_id_map::render and apply_reconfig round-trip BBID/crc_required per Table 25/26", + "[regmap][REQ-RMAP-053]") { + EpIdMappingEntry row; + row.request_stream_index = 2; + row.ep_id = 5; + row.byte_bus_id = 0x0234; + row.crc_required = true; + + uint8_t block[4]; + ep_id_map::render(&row, 1, block); + + EpIdMappingEntry out_row; + auto ec = ep_id_map::apply_reconfig(&out_row, 1, 0, block, sizeof(block)); + REQUIRE_FALSE(ec); + REQUIRE(out_row.request_stream_index == 2); + REQUIRE(out_row.ep_id == 5); + REQUIRE(out_row.byte_bus_id == 0x0234); + REQUIRE(out_row.crc_required); +} + +TEST_CASE("ep_id_map::apply_reconfig patches only the addressed octets", "[regmap][REQ-RMAP-052]") { + EpIdMappingEntry rows[2]; + rows[0].ep_id = 42; // must survive if not addressed + + const uint8_t patch[4] = {1, 9, 0x00, 0x00}; + auto ec = ep_id_map::apply_reconfig(rows, 2, 4, patch, sizeof(patch)); + REQUIRE_FALSE(ec); + REQUIRE(rows[0].ep_id == 42); + REQUIRE(rows[1].request_stream_index == 1); + REQUIRE(rows[1].ep_id == 9); +} + +TEST_CASE("ep_id_map::apply_reconfig rejects a zero-length write and an out-of-range write", + "[regmap][REQ-RMAP-052]") { + EpIdMappingEntry row; + const uint8_t patch[1] = {0}; + + REQUIRE(ep_id_map::apply_reconfig(&row, 1, 0, patch, 0) == + make_error_code(EpIdMapReconfigErrc::short_write)); + REQUIRE(ep_id_map::apply_reconfig(&row, 1, 4, patch, 1) == + make_error_code(EpIdMapReconfigErrc::out_of_range)); +} + +TEST_CASE("EpIdMapReconfigErrc values are distinct and carry non-empty messages", + "[regmap][REQ-RMAP-052]") { + auto short_write = make_error_code(EpIdMapReconfigErrc::short_write); + auto out_of_range = make_error_code(EpIdMapReconfigErrc::out_of_range); + REQUIRE(short_write.category() == ep_id_map_reconfig_category()); + REQUIRE(short_write != out_of_range); + REQUIRE_FALSE(short_write.message().empty()); + REQUIRE_FALSE(out_of_range.message().empty()); +} + +TEST_CASE("ep_id_map::has_single_client_per_ep flags an endpoint mapped under two distinct streams", + "[regmap][REQ-RMAP-057]") { + EpIdMappingEntry ok[2] = {{1, 10, 1, false}, {1, 20, 1, false}}; // same ep_id, same stream -- fine + REQUIRE(ep_id_map::has_single_client_per_ep(ok, 2)); + + EpIdMappingEntry multi_client[2] = {{1, 10, 1, false}, {1, 20, 2, false}}; // same ep_id, different stream + REQUIRE_FALSE(ep_id_map::has_single_client_per_ep(multi_client, 2)); +} + +TEST_CASE("ep_id_map::shared_bus_homogeneous flags a shared bus with differing ep_types", + "[regmap][REQ-RMAP-058]") { + EpIdMappingEntry entries[2] = {{1, 10, 1, false}, {2, 10, 1, false}}; // shared (stream,bbid) + const uint8_t same_type[2] = {3, 3}; + REQUIRE(ep_id_map::shared_bus_homogeneous(entries, same_type, 2)); + + const uint8_t different_type[2] = {3, 4}; + REQUIRE_FALSE(ep_id_map::shared_bus_homogeneous(entries, different_type, 2)); +} + +TEST_CASE("ep_id_map::ep_type_has_fixed_ep_id checks every row of the target type", + "[regmap][REQ-WAKEUP-020]") { + EpIdMappingEntry fixed[1] = {{1, 10, 1, false}}; + const uint8_t fixed_types[1] = {7}; + REQUIRE(ep_id_map::ep_type_has_fixed_ep_id(fixed, fixed_types, 1, 7, 1)); + + EpIdMappingEntry wrong[1] = {{2, 10, 1, false}}; + const uint8_t wrong_types[1] = {7}; + REQUIRE_FALSE(ep_id_map::ep_type_has_fixed_ep_id(wrong, wrong_types, 1, 7, 1)); + + // Vacuously true when no row of that ep_type exists. + const uint8_t none_of_type[1] = {9}; + REQUIRE(ep_id_map::ep_type_has_fixed_ep_id(wrong, none_of_type, 1, 7, 1)); +} + +TEST_CASE("ep_id_map::byte_bus_ids_for_stream reports each distinct byte_bus_id once", + "[regmap][REQ-E2E-029]") { + EpIdMappingEntry entries[3] = {{1, 0x100, 5, false}, {2, 0x100, 5, false}, {3, 0x200, 5, false}}; + avtp::ByteBusId out[4] = {}; + + auto found = ep_id_map::byte_bus_ids_for_stream(entries, 3, /*request_stream_index=*/5, out, 4); + REQUIRE(found == 2); // 0x100 (twice, deduplicated) and 0x200 + REQUIRE(out[0] == 0x100); + REQUIRE(out[1] == 0x200); +} + +TEST_CASE("ep_id_map::byte_bus_ids_for_stream reports the total count even past out_capacity", + "[regmap][REQ-E2E-030]") { + EpIdMappingEntry entries[2] = {{1, 0x100, 5, false}, {2, 0x200, 5, false}}; + avtp::ByteBusId out[1] = {}; + + auto found = ep_id_map::byte_bus_ids_for_stream(entries, 2, 5, out, /*out_capacity=*/1); + REQUIRE(found == 2); // total, not just what fit + REQUIRE(out[0] == 0x100); +} + +TEST_CASE("ep_id_map::byte_bus_ids_for_stream ignores rows on a different request stream", + "[regmap][REQ-E2E-045]") { + EpIdMappingEntry entries[2] = {{1, 0x100, 5, false}, {2, 0x200, 6, false}}; + avtp::ByteBusId out[4] = {}; + + auto found = ep_id_map::byte_bus_ids_for_stream(entries, 2, 5, out, 4); + REQUIRE(found == 1); + REQUIRE(out[0] == 0x100); +} + +TEST_CASE("ep_id_map::is_valid_association matches an exact (stream, byte_bus_id) pair", + "[regmap][REQ-LIFECYCLE-025]") { + EpIdMappingEntry entries[1] = {{1, 0x100, 2, false}}; + REQUIRE(ep_id_map::is_valid_association(entries, 1, 2, 0x100)); + REQUIRE_FALSE(ep_id_map::is_valid_association(entries, 1, 2, 0x200)); // wrong bbid + REQUIRE_FALSE(ep_id_map::is_valid_association(entries, 1, 3, 0x100)); // wrong stream + REQUIRE_FALSE(ep_id_map::is_valid_association(nullptr, 0, 2, 0x100)); // empty table +} + +// ── Optional-subsystem config sections (REQ-RMAP-039) ──────────────────────── + +TEST_CASE("OptionalSubsystemCfg default-constructs with len == 0, meaning \"not supported\"", + "[regmap][REQ-RMAP-039]") { + OptionalSubsystemCfg cfg; + REQUIRE(cfg.len == 0); +} + +TEST_CASE("optional_subsystem_cfg::apply_reconfig writes within the section's own current extent", + "[regmap][REQ-RMAP-039]") { + OptionalSubsystemCfg cfg; + cfg.len = 4; + const uint8_t patch[2] = {0xAA, 0xBB}; + + auto ec = optional_subsystem_cfg::apply_reconfig(cfg, 1, patch, sizeof(patch)); + REQUIRE_FALSE(ec); + REQUIRE(cfg.data[0] == 0x00); + REQUIRE(cfg.data[1] == 0xAA); + REQUIRE(cfg.data[2] == 0xBB); + REQUIRE(cfg.data[3] == 0x00); +} + +TEST_CASE("optional_subsystem_cfg::apply_reconfig rejects a zero-length write and an out-of-range write", + "[regmap][REQ-RMAP-039]") { + OptionalSubsystemCfg cfg; + cfg.len = 2; + const uint8_t patch[1] = {0}; + + REQUIRE(optional_subsystem_cfg::apply_reconfig(cfg, 0, patch, 0) == + make_error_code(OptionalSubsystemCfgReconfigErrc::short_write)); + REQUIRE(optional_subsystem_cfg::apply_reconfig(cfg, 2, patch, 1) == + make_error_code(OptionalSubsystemCfgReconfigErrc::out_of_range)); +} + +TEST_CASE("optional_subsystem_cfg::apply_reconfig rejects a write past the section's own current" + " extent even though kMaxOctets has room", + "[regmap][REQ-RMAP-039]") { + OptionalSubsystemCfg cfg; // cfg.len == 0 -- "not supported" + const uint8_t patch[1] = {0xFF}; + REQUIRE(optional_subsystem_cfg::apply_reconfig(cfg, 0, patch, 1) == + make_error_code(OptionalSubsystemCfgReconfigErrc::out_of_range)); +} + +TEST_CASE("OptionalSubsystemCfgReconfigErrc values are distinct and carry non-empty messages", + "[regmap][REQ-RMAP-039]") { + auto short_write = make_error_code(OptionalSubsystemCfgReconfigErrc::short_write); + auto out_of_range = make_error_code(OptionalSubsystemCfgReconfigErrc::out_of_range); + REQUIRE(short_write.category() == optional_subsystem_cfg_reconfig_category()); + REQUIRE(short_write != out_of_range); + REQUIRE_FALSE(short_write.message().empty()); + REQUIRE_FALSE(out_of_range.message().empty()); +} + +TEST_CASE("RegisterMap carries all four optional-subsystem sections, each defaulting to \"not supported\"", + "[regmap][REQ-RMAP-039]") { + RegisterMap m; + REQUIRE(m.network_interface_cfg.len == 0); + REQUIRE(m.physical_layer_cfg.len == 0); + REQUIRE(m.time_synch_cfg.len == 0); + REQUIRE(m.security_cfg.len == 0); +}