diff --git a/ROADMAP.md b/ROADMAP.md index 696a8d7..e9b2bff 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -153,7 +153,7 @@ work, since every endpoint type's functional config block depends on it. 0. Branch setup, issue cleanup, this ROADMAP section (done). 1. Core wire/protocol: `acf`, `avtp`, `request` (+sequencer+scheduler folded into this codebase's existing unified shape), `fragment` (new — CAN XL multi-segment support this codebase never had), `respqueue` (new — TC18 §12.7.9 TX queue, no prior equivalent), `loan`. 2. Safety layer: `e2e`, `lifecycle`, `watchdog`, a fresh allocation seam (dependency-injected/`std::pmr`-style, not a global hook table) with fault-injection tests from day one. -3. Per-endpoint modules, batched: `can`, `lin`, `iseled`, `adc`, `pwm`, `gpio`, `spi` (incl. the confirmed missing `nr_cs`/`deassert_cs_pause` delta), `uart`, `mdio`, `wakeup`. +3. Per-endpoint modules, batched: `can`, `lin`, `iseled`, `i2c`, `adc`, `pwm`, `gpio`, `spi` (incl. the confirmed missing `nr_cs`/`deassert_cs_pause` delta), `uart`, `mdio`, `wakeup`. 4. Server/dispatch: `regmap` (c-RCP's is ~7x more complete — a real content gap, not style), `mock` (wire in the response classifier, Table 30/33 evt validation, the `RxSequenceGuard` E2E replay guard, and fragmentation dispatch from day one), `server`/`endpoint.hpp`, `discovery`, `adapt`. 5. Transport: `udp`, `l2`, `shmem`, `admin`; re-verify `tls`/`capi` against the new core. 6. Requirement catalog: re-derive `.fusa-reqs.json` from c-RCP's current 1282-entry catalog (already RC5-correct, atomicity-audited, `status`/`scope`/`tc18`/`tc18_master_id` populated), authored fresh with per-function tags and c-RCP's id-prefix conventions (e.g. `REQ-RMAP-*` over this codebase's current `REQ-REGMAP-*`) — no separate atomicity/retagging pass needed since it's new authorship. diff --git a/include/rcp/i2c.hpp b/include/rcp/i2c.hpp index 191020e..95d908c 100644 --- a/include/rcp/i2c.hpp +++ b/include/rcp/i2c.hpp @@ -5,52 +5,95 @@ // fusa:req REQ-I2C-005 // fusa:req REQ-I2C-006 // fusa:req REQ-I2C-007 +// fusa:req REQ-I2C-008 +// fusa:req REQ-I2C-009 +// fusa:req REQ-I2C-010 +// fusa:req REQ-I2C-011 +// fusa:req REQ-I2C-012 +// fusa:req REQ-I2C-013 +// fusa:req REQ-I2C-014 +// fusa:req REQ-I2C-015 +// fusa:req REQ-I2C-016 +// fusa:req REQ-I2C-017 +// fusa:req REQ-I2C-018 +// fusa:req REQ-I2C-020 +// fusa:req REQ-I2C-021 +// fusa:req REQ-I2C-022 +// fusa:req REQ-I2C-023 +// fusa:req REQ-I2C-024 +// fusa:req REQ-I2C-025 +// fusa:req REQ-I2C-026 -// I2C endpoint (ep_type 0x04) — controller-only raw byte-stream transfer -// (including address bytes, per the OPEN Alliance TC18 Remote Control -// Protocol Specification v0.5.1_RC's own framing) and the compound-wait -// arbitrary-bit-sequence match rule (extraction §5.7, §7). +// I2C endpoint (ep_type 0x04) — controller-only, ACF-level raw byte-stream +// transfer (address bytes included, per the OPEN Alliance TC18 Remote +// Control Protocol Specification v0.5.1_RC5's own framing, §13.7.7.1/ +// §13.7.7.3), its Table 49 functional-configuration register block +// (§13.7.7.2, reachable through the generic evt[2:0]=111b configuration- +// write escape hatch, §12.7.1), and the Table 33 Row 2 evt[2:0] plain/ +// reserved/config-write classification every endpoint type in that row +// shares (§13.5). // -// ROADMAP.md milestone 48, "Basic Endpoint Types II — I2C, UART, ADC, -// PWM_OUT, PWM_IN (v2.4.0)": I2C is built on rcp/endpoint.hpp's shared -// TriggerRegistry, following the request-dispatch shape rcp/gpio.hpp and -// rcp/spi.hpp establish (I2cEndpoint mirrors SpiEndpoint's shape: a raw -// byte-exchange transfer() call plus per-endpoint trigger signals). Unlike -// SPI, I2C has no pre-configured channel concept in this milestone's scope -// — a single I2cEndpoint instance models one controller-mode I2C bus, and -// the target device address travels inside the raw byte stream itself -// rather than as a separate selector field. +// Phase 3 rewrite (cpp-RCP issue #129, ROADMAP.md "Phase 17"), ported from +// c-RCP's include/rcp/ep_i2c.h + src/ep_i2c.c — this project's RC5-spec- +// conformant reference for this module. Content correction, not a fresh +// design: this header's own pre-rewrite content (last touched at v2.20.0, +// commits ae5c69f/e488732) modeled `i2c_mode` as a 2-way Standard/HighSpeed +// decode of AcfMessageInfo::hs, and a local `compound_wait_matches_bits` +// helper — both invented, and both wrong against c-RCP's actual design: +// i2c_mode is a persistent 5-way bus-speed preset stored in this endpoint's +// own functional-config register block (Table 49, set via the generic +// evt[2:0]=111b reconfig path, never carried per-transfer on the ACF +// header's `hs` bit at all), and I2C carries no endpoint-specific compound- +// wait logic in c-RCP — rcp/acf.hpp's own compound_wait_evt_valid()/ +// compound_wait_match() (added during this rewrite's Phase 1) are already +// the correct, endpoint-type-independent primitive for that TC18 §13.5.1 +// mechanism, applicable identically to every endpoint type, not something +// I2C should keep its own local duplicate of. Both are removed here in +// favor of the real Table 49 register-block content c-RCP actually +// implements, ported below. // -// Table 30/33 Row 2 evt[2:0] validation pilot (post-v2.4.0): I2cEndpoint's -// handle_request is this repo's first wiring of -// rcp::endpoint::evt_row2_kind_of — the shared 3-way evt[2:0] classifier -// for Table 33's {ADC, PWM_IN, I2C, LIN, CAN, UART, ISELED, MDIO} row — into -// a concrete endpoint type's request decode. The other seven Row 2 endpoint -// types are expected to add their own handle_request-shaped entry point -// calling the same rcp::endpoint::evt_row2_kind_of in their own follow-up -// PRs, matching this shape. +// Retained from the pre-rewrite pilot (post-v2.4.0, first endpoint type to +// wire Table 33 Row 2 evt[2:0] validation): I2cEndpoint's controller-only +// raw byte-stream transfer() call, its TransferComplete/Nack trigger pair, +// and handle_request()'s evt_row2_kind_of dispatch — genuinely good, +// cpp-RCP-only convenience-class content with no c-RCP equivalent (c-RCP is +// a pure free-function C library with no persistent per-transfer "endpoint +// object"), kept and re-verified rather than replaced, per this rewrite's +// "content correction, not API redesign" scope. // -// OPEN ITEM, called out explicitly per the roadmap rather than guessed at: -// the extraction leaves the mapping of I2C's `i2c_mode` field (device -// speed grade — Standard/Fast/Fast-mode-Plus/High-Speed) onto wire values -// ambiguous beyond the coarse `AcfMessageInfo::hs` "high-speed requested" -// flag rcp/acf.hpp already reserves for this purpose. This header -// therefore only decodes that coarse bit (see i2c_mode_of below) and does -// not attempt to further distinguish the standard-speed grades from each -// other; extending i2c_mode_of to do so must wait for a spec errata pass -// that resolves the ambiguity, not a guess made here. +// ── Transfer direction: the ACF op bit vs. the address byte's R/W bit ────── +// An I2C transfer is directional, and that direction appears in two +// entirely independent places, which this module keeps strictly separate +// (§13.7.7.3, TC18.txt L5241-5243, "The byte msg payload is the I2C payload +// including the address. The I2C endpoint does not know whether there is a +// 7- or 10-bit address, since the endpoint is just transparent."): +// - The *I2C-bus-level* R/W bit rides inside the payload's own address +// byte(s); this module never inspects it — it is the caller's to set, +// round-tripped byte for byte. +// - The *RCP-level* direction is the ACF header's op bit: I2cDir::Read +// (wire op=0) asks for a payload-bearing response; I2cDir::Write (wire +// op=1) asks only for a payload-less success confirmation (§12.9.1, +// §11.3.2/§11.3.3). Unlike a LIN command or an SPI transfer — both +// unconditionally response-bearing, so a constant op is correct for +// them — an I2C transfer is genuinely either-directional, so +// encode_transfer_request()/encode_response() take I2cDir explicitly +// rather than hard-coding either sense. // // Field names and behavior below implement TC18's *behavior* as described in // an internal structured extraction of the specification named above; no -// text from that document is reproduced here. The concrete transfer-shape -// and trigger-signal id encoding chosen in this file are this -// implementation's own, same as the equivalent disclaimers in rcp/avtp.hpp, -// rcp/regmap.hpp, rcp/endpoint.hpp, rcp/gpio.hpp, and rcp/spi.hpp. +// text from that document is reproduced here. The concrete struct/enum +// shapes chosen in this file are this implementation's own, same as the +// equivalent disclaimers in rcp/acf.hpp, rcp/avtp.hpp, rcp/endpoint.hpp, +// rcp/lifecycle.hpp, and rcp/fragment.hpp. #pragma once +#include +#include #include +#include #include +#include #include #include #include @@ -60,32 +103,60 @@ namespace rcp { namespace i2c { -// ── i2c_mode (OPEN ITEM — see header comment above) ────────────────────────── -// Only the coarse high-speed-requested bit is unambiguous from the -// extraction available to this implementation; finer speed grades are -// deliberately not modeled here. +// ── i2c_mode: bus-speed presets (§13.7.7.2 Table 49) ───────────────────────── +// The specification extraction available to this implementation carries two +// internally inconsistent numberings for where its highest-speed preset +// sits relative to "Fast mode plus" immediately below it — an apparent +// drafting inconsistency in the source material. c-RCP deliberately +// implements the *lower*-numbered of the two candidate positions +// (HighSpeed = 3, immediately following FastPlus with no reserved value +// skipped) as the more conservative reading, flagged here as pending +// resolution by spec errata rather than guessed at — ported verbatim, +// unchanged, from c-RCP's own identical flag (RCP_EP_I2C_MODE_HIGH_SPEED). enum class I2cMode : uint8_t { - Standard = 0, // hs not set — this implementation's catch-all for every non-high-speed grade - HighSpeed = 1, // hs set — sub-mode (if any) is the unresolved part of this open item + Standard = 0, // ~100 kHz-class preset + Fast = 1, // ~400 kHz-class preset + FastPlus = 2, // ~1 MHz-class preset + HighSpeed = 3, // conservative, lower-numbered reading; pending spec errata + UltraFast = 4, // ~5 MHz-class preset — Table 49's own fifth row, unambiguous }; -// i2c_mode_of decodes AcfMessageInfo::hs into the two-way distinction this -// implementation can make without guessing (see OPEN ITEM above). -constexpr I2cMode i2c_mode_of(bool hs) noexcept { - return hs ? I2cMode::HighSpeed : I2cMode::Standard; +// i2c_mode_valid: true iff v is one of the five defined presets (v <= 4). +constexpr bool i2c_mode_valid(uint8_t v) noexcept { + return v <= static_cast(I2cMode::UltraFast); } // ── Errors ──────────────────────────────────────────────────────────────────── enum class I2cErrc : int { - nack = 1, // the addressed device did not acknowledge (controller-only view) - // evt_row2_kind_of classified the request as ConfigWrite (evt[2:0] == - // 111b, §12.7.1). This milestone's pilot deliberately does not - // implement the configuration-write shape (relative EP_functional- - // config start address + configuration data) — see handle_request's own - // comment. Reported explicitly rather than silently accepted as a plain - // transfer or silently ignored. - config_write_not_supported = 2, + short_frame = 1, + bad_msg_type = 2, + wrong_bus = 3, + // Retained for source stability; no longer produced by any decoder in + // this module — both op senses are valid on an I2C transfer/response + // (see the file header), so there is no longer a "wrong" one to reject. + wrong_op = 4, + // evt[2:0] is not 0b000, TC18 §13.5 Table 33's only legal value for a + // plain (non-configuration) request in I2C's endpoint-type row — + // caller shall respond with error code UNSUPPORTED_CMD. + bad_evt = 5, + // The addressed device did not acknowledge (I2cEndpoint::transfer's own + // controller-only convenience-class concept — c-RCP's ep_i2c.c has no + // equivalent; TC18 does not model bus-level ack/nack for this endpoint + // type at all, so this is additive, not a port). + nack = 6, + // handle_request()'s own convenience-wrapper limitation: a Table 33 + // ConfigWrite (evt[2:0]==111b) request's payload is a relative EP_func + // start-address + configuration-data shape, entirely different from + // handle_request()'s own out_bytes/in_bytes bus-transfer parameters, so + // it cannot be routed through this call — see handle_request's own + // comment. The real configuration-write mechanism this milestone's + // pilot deliberately punted on is fully implemented below + // (apply_reconfig()/render_registers()/encode_reconfig_request(), + // REQ-I2C-021/022/025) — a caller integrating real EP0/regmap dispatch + // calls those directly with the request's actual raw payload, rather + // than through this endpoint's own transfer-shaped convenience call. + config_write_not_supported = 7, }; inline const std::error_category& i2c_category() noexcept { @@ -93,9 +164,15 @@ inline const std::error_category& i2c_category() noexcept { const char* name() const noexcept override { return "rcp.i2c"; } std::string message(int ev) const override { switch (static_cast(ev)) { - case I2cErrc::nack: return "rcp/i2c: addressed device did not acknowledge (NACK)"; + case I2cErrc::short_frame: return "rcp/i2c: frame too short"; + case I2cErrc::bad_msg_type: return "rcp/i2c: unexpected ACF message type"; + case I2cErrc::wrong_bus: return "rcp/i2c: wrong byte_bus_id"; + case I2cErrc::wrong_op: return "rcp/i2c: wrong ACF op"; + case I2cErrc::bad_evt: return "rcp/i2c: evt[2:0] is not 0b000"; + case I2cErrc::nack: return "rcp/i2c: addressed device did not acknowledge (NACK)"; case I2cErrc::config_write_not_supported: - return "rcp/i2c: evt[2:0]=111b configuration-write requests are not yet implemented"; + return "rcp/i2c: evt[2:0]=111b configuration-write requests are not supported by " + "this convenience wrapper — call apply_reconfig() directly"; default: return "rcp/i2c: unknown error"; } } @@ -108,41 +185,378 @@ inline std::error_code make_error_code(I2cErrc e) noexcept { return {static_cast(e), i2c_category()}; } -// ── Compound-wait arbitrary-bit-sequence match ──────────────────────────────── -// I2C's compound-wait condition is matched against an arbitrary-length bit -// sequence rather than SPI's fixed 4-of-20-byte truncation (rcp/spi.hpp's -// compound_wait_matches) — extraction §5.7. `bit_len` is the number of -// leading bits of `received` to compare against `expected`, MSB-first -// within each byte; a `bit_len` that is not a multiple of 8 compares only -// the high `bit_len % 8` bits of the final partial byte, leaving the low -// bits of that byte unconstrained. This function takes no position on the -// rest of compound-wait's semantics (sequencer state, the `cs` field), -// which are v2.5.0 scope, same disclaimer as rcp/spi.hpp's own -// compound_wait_matches. -inline bool compound_wait_matches_bits(const std::vector& received, - const std::vector& expected, - size_t bit_len) noexcept { - if (bit_len == 0) return false; - const size_t full_bytes = bit_len / 8; - const size_t rem_bits = bit_len % 8; - const size_t needed_bytes = full_bytes + (rem_bits != 0 ? 1 : 0); - if (received.size() < needed_bytes || expected.size() < needed_bytes) return false; - - if (!std::equal(received.begin(), received.begin() + static_cast(full_bytes), expected.begin())) - return false; - - if (rem_bits != 0) { - const uint8_t mask = static_cast(0xFFu << (8u - rem_bits)); // top rem_bits of the byte - if ((received[full_bytes] & mask) != (expected[full_bytes] & mask)) return false; - } +// ── Functional config (§13.7.7.2 Table 49) ──────────────────────────────────── +// The common EP_func prefix (ep_enable/ep_clear_req_storage/ +// ep_req_crc_enable/ep_response_ts_enable/ep_suppress_response) every +// endpoint type's own Table shares (c-RCP's rcp_regmap_ep_functional_cfg_t, +// composed as `common` there) is modeled directly as members here rather +// than through rcp/regmap.hpp's own EndpointFunctionalConfig — that struct +// is still an opaque byte blob pending its own structural port (Phase 4, +// ROADMAP.md "Phase 17" phase 4's "regmap: c-RCP's is ~7x more complete"). +struct I2cFunctionalCfg { + 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; + + uint8_t i2c_mode = static_cast(I2cMode::Standard); // 0x0009, R/W + uint16_t ep_status = 0; // 0x0006, R/W + uint8_t clock_divider = 0; // 0x0008, R/W + uint8_t trail = 0; // 0x000A, R/W +}; + +inline void i2c_functional_cfg_init(I2cFunctionalCfg& cfg) noexcept { cfg = I2cFunctionalCfg{}; } + +// i2c_functional_cfg_writable is a thin, named wrapper over +// rcp/lifecycle.hpp's field_writable() (FieldKind::FunctionalW) — reuses, +// never duplicates, that function's authorization logic. +inline bool i2c_functional_cfg_writable(lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + return lifecycle::field_writable(state, lifecycle::FieldKind::FunctionalW, writer); +} + +// set_mode: applies mode to cfg iff mode is i2c_mode_valid() AND +// i2c_functional_cfg_writable() authorizes the write for state/writer; +// returns whether the write was applied. cfg is left entirely unchanged +// when it returns false. +inline bool set_mode(I2cFunctionalCfg& cfg, I2cMode mode, lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + if (!i2c_mode_valid(static_cast(mode))) return false; + if (!i2c_functional_cfg_writable(state, writer)) return false; + cfg.i2c_mode = static_cast(mode); return true; } +// ── The EP_func register block (evt[2:0] == 111b, §13.7.7.2 Table 49) ──────── +// TC18 §13.7.7.2 Table 49's own printed relative-address column collides +// two entries at 0x0002 (i2c_ep_enable&clr, 8 bit, and i2c_base_clk, 16 +// bit) and two more at 0x0004 (i2c_base_clk's own second octet and +// i2c_ep_status, 16 bit) — a genuine editorial defect, confirmed by direct +// visual inspection of the source PDF, the same class of defect PWM_OUT's/ +// GPIO's/SPI's own Tables carry. Every endpoint type's common EP_func +// prefix places EP_LEN/reserved/enable&clr/options/a 16-bit base_clk at the +// identical address sequence 0x0000/0x0001/0x0002/0x0003/0x0004-0x0005 — +// that cross-table pattern is authoritative here too, so i2c_base_clk moves +// to 0x0004-0x0005, pushing i2c_ep_status to 0x0006-0x0007, +// i2c_clock_divider to 0x0008, i2c_mode to 0x0009, and i2c_trail to +// 0x000A (kEpFuncLen = 0x000B, 11 octets total). +constexpr uint16_t kRegEpLen = 0x0000; // 8 bit, R +constexpr uint16_t kRegReserved01 = 0x0001; // 8 bit, R +constexpr uint16_t kRegEpEnableClr = 0x0002; // 8 bit, R/W +constexpr uint16_t kRegEpOptions = 0x0003; // 8 bit, R/W +constexpr uint16_t kRegBaseClk = 0x0004; // 16 bit, R +constexpr uint16_t kRegEpStatus = 0x0006; // 16 bit, R/W +constexpr uint16_t kRegClockDivider = 0x0008; // 8 bit, R/W +constexpr uint16_t kRegMode = 0x0009; // 8 bit, R/W +constexpr uint16_t kRegTrail = 0x000A; // 8 bit, R/W + +// The block's own length in octets — one past the last assigned offset, +// i.e. the value reported at kRegEpLen and the bound the "write beyond +// EP_LEN is ignored" rule (§12.7.1) is applied against. +constexpr size_t kEpFuncLen = 0x000B; + +using EpFuncBlock = std::array; + +namespace detail { +constexpr uint8_t kEnableClrBitEnable = 1u << 0; +constexpr uint8_t kEnableClrBitClear = 1u << 4; +constexpr uint8_t kOptionsBitReqCrc = 1u << 0; +constexpr uint8_t kOptionsBitRespTs = 1u << 3; +constexpr uint8_t kOptionsBitSuppress = 1u << 7; +} // namespace detail + +// render_registers serializes cfg's whole EP_func register block into the +// corrected (not the table's own colliding-printed) offsets above — the +// inverse of apply_reconfig()'s own parse step. i2c_base_clk (read-only) +// always renders 0 — no real clock source is modelled. +inline EpFuncBlock render_registers(const I2cFunctionalCfg& cfg) noexcept { + EpFuncBlock out{}; + uint8_t enable_clr = 0; + uint8_t options = 0; + if (cfg.ep_enable) enable_clr |= detail::kEnableClrBitEnable; + if (cfg.ep_clear_req_storage) enable_clr |= detail::kEnableClrBitClear; + if (cfg.ep_req_crc_enable) options |= detail::kOptionsBitReqCrc; + if (cfg.ep_response_ts_enable) options |= detail::kOptionsBitRespTs; + if (cfg.ep_suppress_response) options |= detail::kOptionsBitSuppress; + + out[kRegEpLen] = static_cast(kEpFuncLen); + out[kRegReserved01] = 0; + out[kRegEpEnableClr] = enable_clr; + out[kRegEpOptions] = options; + out[kRegBaseClk] = 0; // no real clock source modelled + out[kRegBaseClk + 1] = 0; + out[kRegEpStatus] = static_cast(cfg.ep_status >> 8); + out[kRegEpStatus + 1] = static_cast(cfg.ep_status & 0xFF); + out[kRegClockDivider] = cfg.clock_divider; + out[kRegMode] = cfg.i2c_mode; + out[kRegTrail] = cfg.trail; + return out; +} + +namespace detail { +inline void parse_registers(I2cFunctionalCfg& cfg, const EpFuncBlock& in) noexcept { + const uint8_t enable_clr = in[kRegEpEnableClr]; + const uint8_t options = in[kRegEpOptions]; + + cfg.ep_enable = (enable_clr & kEnableClrBitEnable) != 0; + cfg.ep_clear_req_storage = (enable_clr & kEnableClrBitClear) != 0; + cfg.ep_req_crc_enable = (options & kOptionsBitReqCrc) != 0; + cfg.ep_response_ts_enable = (options & kOptionsBitRespTs) != 0; + cfg.ep_suppress_response = (options & kOptionsBitSuppress) != 0; + + cfg.ep_status = static_cast((static_cast(in[kRegEpStatus]) << 8) | in[kRegEpStatus + 1]); + cfg.clock_divider = in[kRegClockDivider]; + cfg.i2c_mode = in[kRegMode]; + cfg.trail = in[kRegTrail]; +} + +// True iff the octet at relative offset addr belongs to a read-only +// register of the block — EP_LEN, the reserved octet, and both octets of +// base_clk. +constexpr bool reg_offset_read_only(uint16_t addr) noexcept { + return addr == kRegEpLen || addr == kRegReserved01 || addr == kRegBaseClk || + addr == static_cast(kRegBaseClk + 1); +} +} // namespace detail + +// The fixed width (octets) of the relative-start-address prefix every +// configuration request's payload begins with — a 16-bit big-endian field, +// followed by the configuration data octets to write from that address +// onward (§12.7.1). +constexpr size_t kReconfigAddrLen = 2; + +// apply_reconfig applies the configuration escape hatch (evt[2:0] == 111b): +// payload is a 16-bit big-endian relative start address followed by the +// configuration data octets to write from that address onward (§12.7.1). +// Patches the block's current image at octet granularity, then adopts it +// wholesale — a write covering only part of a multi-octet register updates +// exactly the octets it addresses and leaves that register's other octets +// alone; octets landing on a read-only register (EP_LEN, the reserved +// octet, base_clk) are silently skipped while the rest of the span is still +// applied. A write whose start_address+length exceeds kEpFuncLen is +// rejected wholesale and cfg left entirely unchanged, per §12.7.1's own +// "such a payload is to be ignored" rule. +enum class I2cReconfigErrc : int { + // payload carries no address prefix, or an address prefix with no data + // octet after it. + short_payload = 1, + // start_address + data length exceeds kEpFuncLen — the whole write is + // ignored. + out_of_range = 2, +}; + +inline const std::error_category& i2c_reconfig_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.i2c.reconfig"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case I2cReconfigErrc::short_payload: + return "rcp/i2c: configuration write has no address and data"; + case I2cReconfigErrc::out_of_range: + return "rcp/i2c: configuration write extends past the EP_func block"; + default: return "rcp/i2c: unknown configuration-write error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(I2cReconfigErrc e) noexcept { + return {static_cast(e), i2c_reconfig_category()}; +} + +inline std::error_code apply_reconfig(I2cFunctionalCfg& cfg, const uint8_t* payload, + size_t payload_len) noexcept { + if (payload_len <= kReconfigAddrLen) return make_error_code(I2cReconfigErrc::short_payload); + + const uint16_t start_address = + static_cast((static_cast(payload[0]) << 8) | payload[1]); + const size_t data_len = payload_len - kReconfigAddrLen; + + if (static_cast(start_address) + data_len > kEpFuncLen) + return make_error_code(I2cReconfigErrc::out_of_range); + + EpFuncBlock block = render_registers(cfg); + for (size_t i = 0; i < data_len; ++i) { + const uint16_t addr = static_cast(start_address + i); + if (detail::reg_offset_read_only(addr)) continue; // write ignored + block[addr] = payload[kReconfigAddrLen + i]; + } + detail::parse_registers(cfg, block); + return {}; +} + +// encode_reconfig_request encodes an ACF_ABB configuration request +// (evt[2:0] == 111b) addressed to byte_bus_id: payload is start_address +// (16-bit big-endian) followed by data. Returns an empty vector if data is +// empty, or if the encoded payload would exceed acf::kAcfAbbMaxPayload. +inline std::vector encode_reconfig_request(avtp::ByteBusId byte_bus_id, uint16_t start_address, + const std::vector& data, + uint8_t transaction_num) { + if (data.empty()) return {}; + if (kReconfigAddrLen + data.size() > acf::kAcfAbbMaxPayload) return {}; + + std::vector payload(kReconfigAddrLen + data.size()); + payload[0] = static_cast(start_address >> 8); + payload[1] = static_cast(start_address & 0xFF); + std::copy(data.begin(), data.end(), payload.begin() + static_cast(kReconfigAddrLen)); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = true; // write — §12.7.1: the write request's payload is written into EP_func + hdr.evt_op = 0x7; // evt[2:0] = 111b, the reconfiguration escape hatch + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, payload); +} + +// ── Transfer direction ──────────────────────────────────────────────────────── +// The RCP-level direction of a transfer/response — NOT the I2C-bus-level R/W +// bit inside the payload's address byte(s), which this module never +// inspects. See the file header. +enum class I2cDir : uint8_t { + Write = 0, // op=1: octets go out, no data comes back + Read = 1, // op=0: read_size octets are clocked back in the response +}; + +constexpr bool i2c_dir_valid(I2cDir d) noexcept { return d == I2cDir::Write || d == I2cDir::Read; } + +namespace detail { +constexpr bool dir_to_write_op(I2cDir d) noexcept { return d == I2cDir::Write; } +constexpr I2cDir op_to_dir(bool op) noexcept { return op ? I2cDir::Write : I2cDir::Read; } +} // namespace detail + +// ── Transfer request ────────────────────────────────────────────────────────── + +// Largest value the ACF header's 12-bit read_size_or_segment_num field can +// carry. +constexpr uint16_t kMaxReadSize = 0x0FFFu; + +// encode_transfer_request encodes an ACF_ABB transfer request addressed to +// byte_bus_id: payload is exactly tx_data, the raw bytes to place on the +// bus — target-device address byte(s) included, never parsed by this +// module (§13.7.7.3, §13.5 Table 33 — no channel selector, evt is always +// 0). read_size applies to I2cDir::Read only; for I2cDir::Write it must be +// 0, since that header slot carries a segment_num, not a read_size, in the +// write sense. Returns an empty vector if direction is not i2c_dir_valid(), +// if read_size exceeds kMaxReadSize, if read_size != 0 with +// I2cDir::Write, or if tx_data exceeds acf::kAcfAbbMaxPayload. +inline std::vector encode_transfer_request(avtp::ByteBusId byte_bus_id, I2cDir direction, + const std::vector& tx_data, + uint16_t read_size, uint8_t transaction_num) { + if (!i2c_dir_valid(direction)) return {}; + if (read_size > kMaxReadSize) return {}; + if (direction == I2cDir::Write && read_size != 0) return {}; + if (tx_data.size() > acf::kAcfAbbMaxPayload) return {}; + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = detail::dir_to_write_op(direction); + hdr.evt_op = 0; // no channel selector — see the file header + hdr.transaction_num = transaction_num; + hdr.read_size_or_segment_num = (direction == I2cDir::Read) ? read_size : static_cast(0); + return acf::encode_acf_abb(hdr, tx_data); +} + +// decode_transfer_request decodes and validates an ACF-level I2C transfer +// request. Both op senses are valid — reported via out_direction, never +// rejected (§13.7.7.1, §13.5 Table 33's own row carries no per-op-sense +// restriction). Rejects evt[2:0] != 0b000 with I2cErrc::bad_evt +// (acf::evt_row2_is_plain(), TC18 §13.5 Table 33 — caller shall respond +// with error code UNSUPPORTED_CMD). out_tx_data (address byte(s) plus data, +// round-tripped verbatim byte for byte, per §13.7.7.3) is set to a copy of +// the request's byte_msg_payload. +inline std::error_code decode_transfer_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + I2cDir& out_direction, std::vector& out_tx_data, + uint16_t& out_read_size, uint8_t& out_transaction_num) { + acf::AcfMessageInfo hdr; + std::vector payload; + const auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(I2cErrc::short_frame); + if (ec) return make_error_code(I2cErrc::bad_msg_type); + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(I2cErrc::wrong_bus); + if (!acf::evt_row2_is_plain(hdr.evt_op)) return make_error_code(I2cErrc::bad_evt); + + const I2cDir direction = detail::op_to_dir(hdr.op); + out_direction = direction; + out_tx_data = std::move(payload); + // Only meaningful in the read sense; in the write sense that slot is a + // segment_num this module does not interpret. + out_read_size = (direction == I2cDir::Read) ? hdr.read_size_or_segment_num : static_cast(0); + out_transaction_num = hdr.transaction_num; + return {}; +} + +// ── Response ─────────────────────────────────────────────────────────────────── + +// encode_response encodes the response to a transfer request of the same +// direction: a read response has a byte_msg_payload (rx_data), a write +// response does not (§11.3.2/§11.3.3). Encoded as ACF_ABB when timed is +// false; ACF_GBB (message_timestamp = timestamp, mtv valid) when timed is +// true. Returns an empty vector if direction is not i2c_dir_valid(), or if +// rx_data is non-empty with direction I2cDir::Write. +inline std::vector encode_response(avtp::ByteBusId byte_bus_id, I2cDir direction, + const std::vector& rx_data, uint8_t transaction_num, + bool timed, uint64_t timestamp) { + if (!i2c_dir_valid(direction)) return {}; + if (direction == I2cDir::Write && !rx_data.empty()) return {}; + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = detail::dir_to_write_op(direction); + hdr.rsp = true; + hdr.evt_op = 0; + hdr.transaction_num = transaction_num; + + if (timed) { + hdr.mtv = true; + return acf::encode_acf_gbb(hdr, timestamp, rx_data); + } + return acf::encode_acf_abb(hdr, rx_data); +} + +// decode_response decodes an I2C response from either an ACF_ABB or +// ACF_GBB message (peeks the message type itself, since a response's +// encoding depends on the responding endpoint's own timed/untimed choice). +inline std::error_code decode_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + I2cDir& out_direction, std::vector& out_rx_data, + bool& out_timed, uint64_t& out_timestamp, + uint8_t& out_transaction_num) { + uint8_t msg_type = 0; + if (acf::peek_msg_type(b, len, msg_type)) return make_error_code(I2cErrc::short_frame); + + acf::AcfMessageInfo hdr; + std::vector payload; + bool timed = false; + uint64_t timestamp = 0; + + if (msg_type == acf::kAcfMsgTypeGbb) { + uint64_t ts = 0; + const auto ec = acf::decode_acf_gbb(b, len, hdr, ts, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(I2cErrc::short_frame); + if (ec) return make_error_code(I2cErrc::bad_msg_type); + timed = hdr.mtv; + timestamp = timed ? ts : 0; + } else { + const auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(I2cErrc::short_frame); + if (ec) return make_error_code(I2cErrc::bad_msg_type); + } + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(I2cErrc::wrong_bus); + + out_direction = detail::op_to_dir(hdr.op); + out_rx_data = std::move(payload); + out_timed = timed; + out_timestamp = timestamp; + out_transaction_num = hdr.transaction_num; + return {}; +} + // ── Trigger signals ─────────────────────────────────────────────────────────── -// One TransferComplete/Nack pair per I2cEndpoint instance (no per-channel -// scoping — see the header-comment note on I2C having no channel concept in -// this milestone's scope), built on rcp/endpoint.hpp's generic -// TriggerRegistry, same primitive rcp/gpio.hpp and rcp/spi.hpp use. +// One TransferComplete/Nack pair per I2cEndpoint instance (I2C has no +// channel concept — a single I2cEndpoint instance models one controller- +// mode I2C bus), built on rcp/endpoint.hpp's generic TriggerRegistry. enum class I2cSignal : uint8_t { TransferComplete = 0, Nack = 1 }; @@ -151,14 +565,13 @@ constexpr endpoint::TriggerRegistry::SignalId i2c_signal_id(I2cSignal sig) noexc } // ── I2cEndpoint ─────────────────────────────────────────────────────────────── -// Mirrors rcp::spi::SpiEndpoint's shape: one request-dispatch entry point -// per incoming I2C transfer. `out_bytes` is the raw stream sent to the bus, -// address byte(s) included per this milestone's "raw byte stream including -// address bytes" scope; `in_bytes` is this implementation's record of the -// bytes received over the same transfer (supplied by the caller — this -// header models the request/response and trigger-signal shape of an I2C -// transfer, not an actual bus controller). Controller-only: this header has -// no target/peripheral-mode behavior at all. +// A per-transfer convenience wrapper (no c-RCP equivalent — c-RCP is a pure +// free-function codec, see the free functions above for the real ACF-level +// wire content this class does not itself encode/decode): records the raw +// out/in byte streams of one transfer and fires trigger signals. Nack +// modeling is this class's own addition (TC18 does not itself model I2C +// bus-level ack/nack); everything else below is unchanged from the +// pre-rewrite pilot. class I2cEndpoint { public: std::error_code transfer(std::vector out_bytes, std::vector in_bytes, @@ -173,33 +586,17 @@ class I2cEndpoint { return {}; } - // handle_request is I2C's request-decode entry point — the piece this - // header previously had none of (issue: cpp-RCP had zero Table 33 Row 2 - // evt[2:0] validation anywhere). It classifies the incoming request's - // evt[2:0] field via rcp::endpoint::evt_row2_kind_of (Table 33's shared - // ADC/PWM_IN/I2C/LIN/CAN/UART/ISELED/MDIO rule) before doing anything - // else, so a Reserved value can never reach transfer() and be misread - // as an ordinary transfer, and a ConfigWrite value can never be - // silently accepted or silently dropped: - // - Plain (evt[2:0] == 000b): delegates straight to transfer() with - // `out_bytes`/`in_bytes`/`acked` unchanged — I2C's existing - // controller-only raw byte-stream transfer model (extraction - // §13.7.7.3) already IS this row's correct "plain request" - // behavior; evt[2:0] carries no combinable value or channel - // selector for this row the way it does for GPIO/PWM_OUT or SPI. - // - Reserved (evt[2:0] in 001b-110b): returns + // handle_request classifies the incoming request's evt[2:0] field via + // rcp::endpoint::evt_row2_kind_of (Table 33's shared ADC/PWM_IN/I2C/LIN/ + // CAN/UART/ISELED/MDIO rule) before doing anything else: + // - Plain (evt[2:0] == 000b): delegates to transfer() unchanged. + // - Reserved (001b-110b): returns // endpoint::EndpointErrc::reserved_evt_row2 without touching any - // endpoint state or recording anything as sent/received — TC18 - // requires this be rejected with error code UNSUPPORTED_CMD. - // - ConfigWrite (evt[2:0] == 111b): §12.7.1's configuration-write - // shape targets the I2C EP's own functional-config block (relative - // start address + configuration data), not a bus transfer at all. - // Full handling is deliberately out of scope for this milestone's - // pilot (nontrivial — it needs EP_functional-config wiring this - // header does not yet have); this returns - // I2cErrc::config_write_not_supported rather than crashing, - // silently accepting the request as a transfer, or silently doing - // nothing. + // endpoint state. + // - ConfigWrite (111b): returns I2cErrc::config_write_not_supported — + // see that enumerator's own comment for why this convenience call's + // out_bytes/in_bytes shape cannot carry a reconfig payload, and + // where the real, now-implemented mechanism lives. std::error_code handle_request(uint8_t evt_op, std::vector out_bytes, std::vector in_bytes, bool acked = true) { switch (endpoint::evt_row2_kind_of(evt_op)) { @@ -226,8 +623,10 @@ class I2cEndpoint { } // namespace i2c } // namespace rcp -// Enable std::error_code construction from rcp::i2c::I2cErrc. +// Enable std::error_code construction from rcp::i2c::I2cErrc / I2cReconfigErrc. namespace std { template <> struct is_error_code_enum : true_type {}; +template <> +struct is_error_code_enum : true_type {}; } // namespace std diff --git a/include/rcp/iseled.hpp b/include/rcp/iseled.hpp index cba30c3..6c92cad 100644 --- a/include/rcp/iseled.hpp +++ b/include/rcp/iseled.hpp @@ -5,78 +5,108 @@ // fusa:req REQ-ISELED-005 // fusa:req REQ-ISELED-006 // fusa:req REQ-ISELED-007 +// fusa:req REQ-ISELED-008 +// fusa:req REQ-ISELED-009 +// fusa:req REQ-ISELED-010 +// fusa:req REQ-ISELED-011 +// fusa:req REQ-ISELED-012 +// fusa:req REQ-ISELED-013 +// fusa:req REQ-ISELED-014 +// fusa:req REQ-ISELED-015 +// fusa:req REQ-ISELED-016 +// fusa:req REQ-ISELED-017 +// fusa:req REQ-ISELED-018 +// fusa:req REQ-ISELED-019 +// fusa:req REQ-ISELED-020 +// fusa:req REQ-ISELED-021 +// fusa:req REQ-ISELED-022 +// fusa:req REQ-ISELED-023 +// fusa:req REQ-ISELED-024 +// fusa:req REQ-ISELED-025 +// fusa:req REQ-ISELED-026 +// fusa:req REQ-ISELED-027 +// fusa:req REQ-ISELED-029 +// fusa:req REQ-ISELED-030 +// fusa:req REQ-ISELED-031 +// fusa:req REQ-ISELED-032 +// fusa:req REQ-ISELED-033 +// fusa:req REQ-ISELED-034 +// fusa:req REQ-ISELED-035 +// fusa:req REQ-ISELED-036 +// fusa:req REQ-ISELED-037 +// fusa:req REQ-ISELED-038 +// fusa:req REQ-ISELED-039 +// fusa:req REQ-ISELED-040 +// fusa:req REQ-ISELED-041 +// fusa:req REQ-ISELED-042 +// REQ-ISELED-028 is retired (stale duplicate of REQ-ISELED-007) — see +// c-RCP's .fusa-reqs.json; not ported here. // ISELED endpoint (ep_type 0x0C) — the OPEN Alliance TC18 Remote Control -// Protocol Specification v0.5.1_RC's native ISELED daisy-chain framing: a -// 4-bit Instruction / 12-bit Address / variable-length Data request shape -// and a 12-bit Address / 12-bit Data response shape (extraction §5.12). +// Protocol Specification v0.5.1_RC5's ACF-level raw byte-stream command/ +// read/response codec for the ISELED daisy-chain (§13.7.12.1/§13.7.12.3), +// its native 4-bit/5-bit even-parity bit framing plus an independent, +// optional CRC-8 integrity layer for actually driving the physical ISP_P/ +// ISP_N pair, its Table 58 functional-configuration register block +// (§13.7.12.2), and read-direction response fragmentation bounded by +// read_size (§13.7.12.1, rcp/fragment.hpp). // -// ROADMAP.md milestone 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN -// XL), ISELED, MDIO, Wakeup Control (v2.7.0)". +// Phase 3 rewrite (cpp-RCP issue #129, ROADMAP.md "Phase 17"), ported from +// c-RCP's include/rcp/ep_iseled.h + src/ep_iseled.c — this project's +// RC5-spec-conformant reference for this module, itself the product of a +// real fix history (issue #270's requires_isp_n polarity correction, issue +// #256 Group I's Table 58 register-block + evt[2:0]=111b reconfig work, +// issue #471's read-direction request, and the REQ-ISELED-025 response- +// fragmentation work) — this port reflects that current, post-fix state. // -// Wire-format fix (issue #71, cpp-RCP-02): this header previously modeled -// IseledResponse::address as a full uint16_t, IseledResponse::data as an -// open-ended std::vector, and carried an invented 8-bit CRC -// (polynomial 0x07) the header's own prior comment admitted was not from -// the ISELED standard. Verified against the OPEN Alliance TC18 Remote -// Control Protocol Specification's "iseled request format" (Figure 40) and -// "iseled response format" (Figure 41) figures, §13.7.12.3: the address -// field is 12 bits wide (both in the request and, per Figure 41, again in -// the response), and a response's data is a single 12-bit value -// ("Data[11:0]"), not an open byte vector. kIseledFieldMask below is that -// shared 12-bit width, applied to IseledRequest::address, -// IseledResponse::address, and IseledResponse::data via validate_request/ -// validate_response. IseledRequest::data is left as std::vector: -// unlike the response's single Data[11:0] value, the request's data is -// realistic-payload-length-dependent (Figure 40 shows Data1/Data2/Data3/... -// plus padding), so a byte vector remains the right shape there — only the -// response's fixed-width fields were wrong. +// This is a wholesale content replacement of this header's own pre-rewrite +// design (last touched at v2.20.0/#120), not an incremental patch: the +// prior IseledRequest/IseledResponse structs modeled a 4-bit Instruction/ +// 12-bit Address/12-bit Data shape as ACF *payload structure* — but c-RCP's +// actual ACF-level codec (encode_command_request/encode_read_request/ +// encode_response below) treats the byte_msg_payload as an opaque raw byte +// stream, exactly like every other raw-byte-stream endpoint type (I2C, +// LIN, CAN) — it does not itself decode Instruction/Address/Data at the ACF +// layer at all. That structured decoding is a *bus-physical-layer* concern +// this module keeps separate: the real Instruction/Address/Data framing +// (and the invented, non-spec-sourced 12-bit field widths and CRC-8 the +// prior design used to represent it) belongs to the ISELED standard itself +// — a separate, independently-documented industry protocol this repository +// has no verified copy of — never to the RCP wire codec. The native 4-bit/ +// 5-bit symbol framing and CRC-8 ported below are c-RCP's own original, +// clearly-labeled design (poly 0x07, init 0x00, no reflection — a standard, +// publicly documented small CRC, not derived from the confidential TC18 +// extraction or from ISELED's own specification) for that separate, +// physical-layer job, matching the "separate jobs, never conflated" split +// c-RCP's own file header draws. // -// This header's invented CRC-8 (polynomial 0x07) is removed outright rather -// than replaced with a differently-sized invented polynomial: the real -// ISELED-standard CRC algorithm and its optional-4-bit-trailer width (per -// Figure 41's "CRC (optional)" field) are not sourced anywhere in this -// codebase or in the TC18 spec extract this header is built from — the -// spec explicitly defers the ISELED CRC's own definition to the separate -// ISELED standard, which this repository does not have a verified copy of -// — so inventing a differently-sized polynomial here would repeat the same -// mistake this fix is for. IseledErrc::field_out_of_range takes over the -// error category in its place: it is a direct consequence of the same -// width fix (rejecting an instruction/address/data value that does not fit -// its documented field width), not a new invented protocol behavior. -// IseledSignal::NativeCrcError is removed along with it, leaving -// IseledSignal::TransferComplete as the endpoint's only trigger signal. -// -// Table 30/33 Row 2 evt[2:0] validation (post-v2.7.0, seventh endpoint type -// after I2C, ADC, PWM_IN, LIN, CAN, and UART): IseledEndpoint::handle_request -// is this header's own wiring of rcp::endpoint::evt_row2_kind_of — the -// shared 3-way evt[2:0] classifier for Table 33's {ADC, PWM_IN, I2C, LIN, -// CAN, UART, ISELED, MDIO} row — into ISELED's request decode, following the -// exact shape rcp/i2c.hpp's I2cEndpoint::handle_request, rcp/adc.hpp's -// AdcEndpoint::handle_request, rcp/pwm.hpp's PwmInEndpoint::handle_request, -// rcp/lin.hpp's LinEndpoint::handle_request, rcp/can.hpp's CanEndpoint:: -// handle_request, and rcp/uart.hpp's UartEndpoint::handle_request -// established. This is a completely separate, TC18-Table-33-derived -// concern from the Figure 40/41 wire-format fix directly above — it does -// not touch, reinterpret, or extend IseledRequest's/IseledResponse's field -// shapes, or the encode_iseled_request/decode_iseled_request/ -// encode_iseled_response/decode_iseled_response codec, in any way. See -// handle_request's own doc comment below for why a Reserved or ConfigWrite -// evt must never reach transact(). +// ── Response fragmentation and rcp/fragment.hpp ───────────────────────────── +// c-RCP's ep_iseled.c genuinely `#include`s "rcp/fragment.h" and calls its +// rcp_fragment_plan_count()/rcp_fragment_plan() directly from +// rcp_ep_iseled_response_fragment_count()/_encode_response_fragmented() — +// unlike an endpoint-specific multi-response dispatch mechanism, ISELED's +// own response fragmentation genuinely is the generic fragment module +// applied to one endpoint type's read-direction response, so this port +// wires rcp/iseled.hpp to rcp/fragment.hpp (fragment::plan_count()/ +// fragment::plan()) the same way, rather than inventing ISELED-specific +// fragmentation logic. // // Field names and behavior below implement TC18's *behavior* as described in // an internal structured extraction of the specification named above; no -// text from that document is reproduced here. The concrete request `data` -// vector representation and trigger-signal id encoding chosen in this file -// are this implementation's own, same as the equivalent disclaimers in -// rcp/avtp.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, rcp/e2e.hpp, rcp/i2c.hpp, -// rcp/adc.hpp, rcp/pwm.hpp, rcp/lin.hpp, rcp/can.hpp, and rcp/uart.hpp. +// text from that document is reproduced here. The concrete struct/enum +// shapes chosen in this file are this implementation's own, same as the +// equivalent disclaimers in rcp/acf.hpp, rcp/avtp.hpp, rcp/endpoint.hpp, +// rcp/lifecycle.hpp, rcp/fragment.hpp, and rcp/i2c.hpp. #pragma once +#include #include #include +#include +#include #include +#include #include #include #include @@ -86,40 +116,93 @@ namespace rcp { namespace iseled { -// ── Field widths ────────────────────────────────────────────────────────────── -// 12-bit address/data field width shared by the request's Address field and -// the response's Address/Data[11:0] fields (Figures 40/41, §13.7.12.3); 4-bit -// instruction field width (Figure 40). - -constexpr uint16_t kIseledFieldMask = 0x0FFFu; // 12 bits: address, and response data -constexpr uint8_t kIseledInstructionMask = 0x0Fu; // 4 bits: request instruction +// ── Native 4-bit/5-bit bit framing (§13.7.12.1) ─────────────────────────────── +// Each 4-bit data nibble is framed onto the ISP_P/ISP_N pair as a 5-bit +// symbol: the low 4 bits carry the nibble unchanged, and bit 4 carries that +// nibble's own even-parity bit. A full content buffer is framed two symbols +// per octet, high nibble first, one symbol value (0-31) stored per output +// octet. + +namespace detail { +constexpr uint8_t popcount4(uint8_t n) noexcept { + uint8_t c = 0; + uint8_t v = static_cast(n & 0x0F); + while (v != 0) { + c = static_cast(c + (v & 0x01)); + v = static_cast(v >> 1); + } + return c; +} +} // namespace detail + +// symbol_encode: encodes nibble's low 4 bits into a 5-bit even-parity +// symbol (bit 4 = even parity of bits [3:0]). Any bits of nibble above bit +// 3 are ignored. Return value is always in 0..31. +constexpr uint8_t symbol_encode(uint8_t nibble) noexcept { + const uint8_t n = static_cast(nibble & 0x0F); + const uint8_t parity = static_cast(detail::popcount4(n) & 0x01); + return static_cast(static_cast(parity << 4) | n); +} -// ── Request / response shapes ──────────────────────────────────────────────── +// symbol_decode: decodes symbol (only bits [4:0] are inspected) back into a +// 4-bit nibble. Returns true and sets out_nibble to bits [3:0] of symbol +// iff bit 4 equals the even parity of bits [3:0]; returns false (leaving +// out_nibble untouched) for any symbol whose parity bit does not match. +inline bool symbol_decode(uint8_t symbol, uint8_t& out_nibble) noexcept { + const uint8_t s = static_cast(symbol & 0x1F); + const uint8_t n = static_cast(s & 0x0F); + const uint8_t parity_bit = static_cast((s >> 4) & 0x01); + const uint8_t want_parity = static_cast(detail::popcount4(n) & 0x01); + if (parity_bit != want_parity) return false; + out_nibble = n; + return true; +} -struct IseledRequest { - uint8_t instruction = 0; // 4-bit instruction (kIseledInstructionMask), per Figure 40 - uint16_t address = 0; // 12-bit address (kIseledFieldMask), per Figure 40 - std::vector data; // variable-length data bytes (Data1/Data2/Data3/... + padding), per Figure 40 -}; +// bitframe_encoded_len: the number of framed octets encode_bitframe() +// produces for data_len octets of plain content, optionally with the +// one-octet CRC-8 trailer appended before framing: 2 * (data_len + +// (append_crc ? 1 : 0)). +constexpr size_t bitframe_encoded_len(size_t data_len, bool append_crc) noexcept { + return (data_len + (append_crc ? size_t{1} : size_t{0})) * 2; +} -struct IseledResponse { - uint16_t address = 0; // 12-bit address (kIseledFieldMask), per Figure 41 - uint16_t data = 0; // 12-bit data value ("Data[11:0]"), per Figure 41 — a single field, not a byte vector -}; +// ── ISELED-level CRC (distinct from rcp/e2e.hpp; see the file header) ──────── +// A standard CRC-8 (poly 0x07, init 0x00, no input/output reflection) over +// data — a second, independent integrity layer from rcp/e2e.hpp's own +// CRC-16, gated by IseledFunctionalCfg::crc_enable. +inline uint8_t crc8(const uint8_t* data, size_t len) noexcept { + constexpr uint8_t poly = 0x07; + uint8_t crc = 0x00; + for (size_t i = 0; i < len; ++i) { + crc = static_cast(crc ^ data[i]); + for (int b = 0; b < 8; ++b) + crc = (crc & 0x80) ? static_cast(static_cast(crc << 1) ^ poly) + : static_cast(crc << 1); + } + return crc; +} +inline uint8_t crc8(const std::vector& data) noexcept { + return crc8(data.empty() ? nullptr : data.data(), data.size()); +} // ── Errors ──────────────────────────────────────────────────────────────────── enum class IseledErrc : int { - field_out_of_range = 1, // instruction/address/data exceeds its documented wire field width - // evt_row2_kind_of classified the request as ConfigWrite (evt[2:0] == - // 111b, §12.7.1). This milestone's follow-up deliberately does not - // implement the configuration-write shape (relative EP_functional- - // config start address + configuration data) — see handle_request's - // own comment. Reported explicitly rather than silently accepted as a - // plain transaction or silently ignored, same as I2C's, ADC's, - // PWM_IN's, LIN's, CAN's, and UART's own config_write_not_supported - // variants. - config_write_not_supported = 2, + short_frame = 1, + bad_msg_type = 2, + wrong_bus = 3, + wrong_op = 4, + bad_symbol = 5, + crc_mismatch = 6, + odd_symbol_count = 7, + // evt[2:0] is not 0b000, TC18 §13.5 Table 33's only legal value for a + // plain (non-configuration) request in ISELED's endpoint-type row — + // caller shall respond with error code UNSUPPORTED_CMD. + bad_evt = 8, + // handle_request()'s own convenience-wrapper limitation — see that + // method's own comment and I2cErrc::config_write_not_supported's + // identical rationale (rcp/i2c.hpp). + config_write_not_supported = 9, }; inline const std::error_category& iseled_category() noexcept { @@ -127,12 +210,19 @@ inline const std::error_category& iseled_category() noexcept { const char* name() const noexcept override { return "rcp.iseled"; } std::string message(int ev) const override { switch (static_cast(ev)) { - case IseledErrc::field_out_of_range: - return "rcp/iseled: instruction/address/data exceeds its wire field width"; + case IseledErrc::short_frame: return "rcp/iseled: frame too short"; + case IseledErrc::bad_msg_type: return "rcp/iseled: unexpected ACF message type"; + case IseledErrc::wrong_bus: return "rcp/iseled: wrong byte_bus_id"; + case IseledErrc::wrong_op: return "rcp/iseled: wrong ACF op"; + case IseledErrc::bad_symbol: return "rcp/iseled: invalid bit-framing symbol"; + case IseledErrc::crc_mismatch: return "rcp/iseled: native ISELED CRC-8 mismatch"; + case IseledErrc::odd_symbol_count: + return "rcp/iseled: odd symbol count — every octet frames to two symbols"; + case IseledErrc::bad_evt: return "rcp/iseled: evt[2:0] is not 0b000"; case IseledErrc::config_write_not_supported: - return "rcp/iseled: evt[2:0]=111b configuration-write requests are not yet implemented"; - default: - return "rcp/iseled: unknown error"; + return "rcp/iseled: evt[2:0]=111b configuration-write requests are not supported " + "by this convenience wrapper — call apply_reconfig() directly"; + default: return "rcp/iseled: unknown error"; } } }; @@ -144,93 +234,556 @@ inline std::error_code make_error_code(IseledErrc e) noexcept { return {static_cast(e), iseled_category()}; } -// validate_request rejects an instruction wider than 4 bits or an address -// wider than 12 bits (Figure 40). -inline std::error_code validate_request(const IseledRequest& req) noexcept { - if (req.instruction > kIseledInstructionMask) return make_error_code(IseledErrc::field_out_of_range); - if (req.address > kIseledFieldMask) return make_error_code(IseledErrc::field_out_of_range); +// encode_bitframe: bit-frames data into a newly built symbol vector of +// bitframe_encoded_len(data.size(), append_crc) octets, each holding one +// symbol_encode() result (0-31) in its low 5 bits. When append_crc is true, +// crc8(data) is framed as one extra trailing content octet before the two- +// symbols-per-octet expansion. High nibble framed before low nibble for +// every content octet, including the trailing CRC octet. Returns an empty +// vector iff bitframe_encoded_len(data.size(), append_crc) would be 0. +inline std::vector encode_bitframe(const std::vector& data, bool append_crc) { + const size_t content_len = data.size() + (append_crc ? size_t{1} : size_t{0}); + const size_t n = content_len * 2; + if (n == 0) return {}; + + const uint8_t trailer = append_crc ? crc8(data) : 0; + std::vector out(n); + for (size_t i = 0; i < content_len; ++i) { + const uint8_t octet = (i < data.size()) ? data[i] : trailer; + out[2 * i] = symbol_encode(static_cast(octet >> 4)); + out[2 * i + 1] = symbol_encode(static_cast(octet & 0x0F)); + } + return out; +} + +// decode_bitframe reverses encode_bitframe(): decodes symbols[0..symbol_count) +// back into plain content. Fails with odd_symbol_count if symbol_count is +// odd; short_frame if expect_crc is true and symbol_count yields fewer than +// one content octet (no room for the CRC trailer itself); bad_symbol if any +// symbol fails symbol_decode(); crc_mismatch if expect_crc is true and the +// trailing octet does not equal crc8() of the preceding content. On +// success, out_data holds the decoded plain content — the trailing CRC +// octet is verified but not included in out_data when expect_crc is true. +inline std::error_code decode_bitframe(const uint8_t* symbols, size_t symbol_count, bool expect_crc, + std::vector& out_data) { + out_data.clear(); + if ((symbol_count & size_t{1}) != 0) return make_error_code(IseledErrc::odd_symbol_count); + + const size_t byte_count = symbol_count / 2; + if (expect_crc && byte_count == 0) return make_error_code(IseledErrc::short_frame); + + std::vector bytes(byte_count); + for (size_t i = 0; i < byte_count; ++i) { + uint8_t hi = 0, lo = 0; + if (!symbol_decode(symbols[2 * i], hi) || !symbol_decode(symbols[2 * i + 1], lo)) + return make_error_code(IseledErrc::bad_symbol); + bytes[i] = static_cast(static_cast(hi << 4) | lo); + } + + if (expect_crc) { + const size_t data_len = byte_count - 1; + const uint8_t want = crc8(bytes.data(), data_len); + if (bytes[byte_count - 1] != want) return make_error_code(IseledErrc::crc_mismatch); + out_data.assign(bytes.begin(), bytes.begin() + static_cast(data_len)); + } else { + out_data = std::move(bytes); + } return {}; } -// validate_response rejects an address or data value wider than 12 bits -// (Figure 41). -inline std::error_code validate_response(const IseledResponse& resp) noexcept { - if (resp.address > kIseledFieldMask) return make_error_code(IseledErrc::field_out_of_range); - if (resp.data > kIseledFieldMask) return make_error_code(IseledErrc::field_out_of_range); +// ── Recovered-clock mode (§13.7.12.2 Table 58) ──────────────────────────────── +// requires_isp_n: true iff the ISP_N pin must be wired/mapped for this +// endpoint to operate — true iff use_rcv_clk is true (device-provided +// clock, which arrives on ISP_N), false iff the Freq_Sync pattern is used +// instead. +constexpr bool requires_isp_n(bool use_rcv_clk) noexcept { return use_rcv_clk; } + +// ── Transmission-complete trigger (§13.7.12.1) ──────────────────────────────── + +enum class IseledTrigger : uint8_t { None = 0, TxComplete = 1 }; + +// trigger_fires: true iff tx_complete_event satisfies trigger — never for +// None; for TxComplete iff tx_complete_event is true. +constexpr bool trigger_fires(IseledTrigger trigger, bool tx_complete_event) noexcept { + return trigger == IseledTrigger::TxComplete && tx_complete_event; +} + +// ── Functional config (§13.7.12.2 Table 58) ─────────────────────────────────── +// The common EP_func prefix is modeled directly as members here, same as +// rcp/i2c.hpp's I2cFunctionalCfg — see that header's own identical note on +// why (rcp/regmap.hpp's own EndpointFunctionalConfig is still an opaque +// byte blob pending Phase 4). +struct IseledFunctionalCfg { + bool ep_enable = false; + bool ep_clear_req_storage = false; + bool ep_req_crc_enable = false; + bool ep_response_ts_enable = false; + bool ep_suppress_response = false; + + uint32_t bit_clk_divider = 0; // this endpoint's own outbound bit-time clock divider + bool use_rcv_clk = false; // 0x0009.4, R/W — recovered-clock mode; see requires_isp_n() + // Gates the native ISELED CRC-8 trailer (encode_bitframe's append_crc) + // — NOT part of the Table 58 register block below (Table 58 defines no + // register for it; never rendered onto or parsed from the wire). + bool crc_enable = false; + uint8_t trigger = static_cast(IseledTrigger::None); // IseledTrigger + + uint16_t base_clk = 0; // 0x0004, R + uint16_t ep_status = 0; // 0x0006, R/W + uint8_t wire_clk_divider = 0; // 0x0008, R/W — the real wire register, distinct from bit_clk_divider + bool collect_resp = false; // 0x0009.3, R/W + uint16_t nr_leds = 0; // 0x000A, R/W + uint16_t rcv_timeout = 0; // 0x000C, R/W +}; + +inline void iseled_functional_cfg_init(IseledFunctionalCfg& cfg) noexcept { cfg = IseledFunctionalCfg{}; } + +// iseled_functional_cfg_writable is a thin, named wrapper over +// rcp/lifecycle.hpp's field_writable() (FieldKind::FunctionalW) — reuses, +// never duplicates, that function's authorization logic. +inline bool iseled_functional_cfg_writable(lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + return lifecycle::field_writable(state, lifecycle::FieldKind::FunctionalW, writer); +} + +inline bool set_bit_clk_divider(IseledFunctionalCfg& cfg, uint32_t divider, lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + if (!iseled_functional_cfg_writable(state, writer)) return false; + cfg.bit_clk_divider = divider; + return true; +} + +inline bool set_use_rcv_clk(IseledFunctionalCfg& cfg, bool use_rcv_clk, lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + if (!iseled_functional_cfg_writable(state, writer)) return false; + cfg.use_rcv_clk = use_rcv_clk; + return true; +} + +inline bool set_crc_enable(IseledFunctionalCfg& cfg, bool enable, lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + if (!iseled_functional_cfg_writable(state, writer)) return false; + cfg.crc_enable = enable; + return true; +} + +inline bool set_trigger(IseledFunctionalCfg& cfg, IseledTrigger trigger, lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + if (!iseled_functional_cfg_writable(state, writer)) return false; + cfg.trigger = static_cast(trigger); + return true; +} + +// ── The EP_func register block (evt[2:0] == 111b, §13.7.12.2 Table 58) ─────── +// Table 58's own printed relative-address column has the same class of +// address-collision editorial defect as I2C's Table 49 (rcp/i2c.hpp): +// iseled_base_clk (16 bit, R) is printed at relative address 0x0001, one +// octet after iseled_ep_len, with no reserved octet at 0x0001 — colliding +// with iseled_ep_enable&clr, separately printed at 0x0002. Resolved via the +// same cross-table structural precedent: iseled_base_clk moves to +// 0x0004-0x0005, pushing iseled_ep_status to 0x0006-0x0007, +// iseled_clk_divider to 0x0008, the flags octet (iseled_collect_resp bit 3, +// iseled_use_rcv_clk bit 4) to 0x0009, iseled_nr_leds to 0x000A-0x000B, and +// iseled_rcv_timeout to 0x000C-0x000D (kEpFuncLen = 0x000E). +constexpr uint16_t kRegEpLen = 0x0000; // 8 bit, R +constexpr uint16_t kRegReserved01 = 0x0001; // 8 bit, R +constexpr uint16_t kRegEpEnableClr = 0x0002; // 8 bit, R/W +constexpr uint16_t kRegEpOptions = 0x0003; // 8 bit, R/W +constexpr uint16_t kRegBaseClk = 0x0004; // 16 bit, R +constexpr uint16_t kRegEpStatus = 0x0006; // 16 bit, R/W +constexpr uint16_t kRegClkDivider = 0x0008; // 8 bit, R/W +constexpr uint16_t kRegFlags = 0x0009; // 8 bit, R/W +constexpr uint16_t kRegNrLeds = 0x000A; // 16 bit, R/W +constexpr uint16_t kRegRcvTimeout = 0x000C; // 16 bit, R/W + +// The block's own length in octets — one past the last assigned offset. +constexpr size_t kEpFuncLen = 0x000E; + +using EpFuncBlock = std::array; + +// Bit masks within the kRegFlags octet — Table 58's own two named +// single-bit parameters, at their corrected relative bit positions +// (0x0009.3/0x0009.4); the remaining bits are reserved and always read 0. +constexpr uint8_t kFlagCollectResp = 1u << 3; +constexpr uint8_t kFlagUseRcvClk = 1u << 4; + +namespace detail { +constexpr uint8_t kEnableClrBitEnable = 1u << 0; +constexpr uint8_t kEnableClrBitClear = 1u << 4; +constexpr uint8_t kOptionsBitReqCrc = 1u << 0; +constexpr uint8_t kOptionsBitRespTs = 1u << 3; +constexpr uint8_t kOptionsBitSuppress = 1u << 7; +} // namespace detail + +// render_registers serializes cfg's whole EP_func register block into the +// corrected offsets above — the inverse of apply_reconfig()'s own parse +// step. iseled_crc_enable is NOT part of this block (see the file header) +// and is never touched here. +inline EpFuncBlock render_registers(const IseledFunctionalCfg& cfg) noexcept { + EpFuncBlock out{}; + uint8_t enable_clr = 0; + uint8_t options = 0; + uint8_t flags = 0; + if (cfg.ep_enable) enable_clr |= detail::kEnableClrBitEnable; + if (cfg.ep_clear_req_storage) enable_clr |= detail::kEnableClrBitClear; + if (cfg.ep_req_crc_enable) options |= detail::kOptionsBitReqCrc; + if (cfg.ep_response_ts_enable) options |= detail::kOptionsBitRespTs; + if (cfg.ep_suppress_response) options |= detail::kOptionsBitSuppress; + if (cfg.collect_resp) flags |= kFlagCollectResp; + if (cfg.use_rcv_clk) flags |= kFlagUseRcvClk; + + out[kRegEpLen] = static_cast(kEpFuncLen); + out[kRegReserved01] = 0; + out[kRegEpEnableClr] = enable_clr; + out[kRegEpOptions] = options; + out[kRegBaseClk] = static_cast(cfg.base_clk >> 8); + out[kRegBaseClk + 1] = static_cast(cfg.base_clk & 0xFF); + out[kRegEpStatus] = static_cast(cfg.ep_status >> 8); + out[kRegEpStatus + 1] = static_cast(cfg.ep_status & 0xFF); + out[kRegClkDivider] = cfg.wire_clk_divider; + out[kRegFlags] = flags; + out[kRegNrLeds] = static_cast(cfg.nr_leds >> 8); + out[kRegNrLeds + 1] = static_cast(cfg.nr_leds & 0xFF); + out[kRegRcvTimeout] = static_cast(cfg.rcv_timeout >> 8); + out[kRegRcvTimeout + 1] = static_cast(cfg.rcv_timeout & 0xFF); + return out; +} + +namespace detail { +inline void parse_registers(IseledFunctionalCfg& cfg, const EpFuncBlock& in) noexcept { + const uint8_t enable_clr = in[kRegEpEnableClr]; + const uint8_t options = in[kRegEpOptions]; + const uint8_t flags = in[kRegFlags]; + + cfg.ep_enable = (enable_clr & kEnableClrBitEnable) != 0; + cfg.ep_clear_req_storage = (enable_clr & kEnableClrBitClear) != 0; + cfg.ep_req_crc_enable = (options & kOptionsBitReqCrc) != 0; + cfg.ep_response_ts_enable = (options & kOptionsBitRespTs) != 0; + cfg.ep_suppress_response = (options & kOptionsBitSuppress) != 0; + cfg.collect_resp = (flags & kFlagCollectResp) != 0; + cfg.use_rcv_clk = (flags & kFlagUseRcvClk) != 0; + + // base_clk (read-only) is deliberately NOT read back here — see + // reg_offset_read_only() below and apply_reconfig()'s own doc comment; + // re-rendering from cfg before patching means a write covering it is a + // no-op, matching c-RCP's identical parse_iseled_registers() design. + cfg.ep_status = static_cast((static_cast(in[kRegEpStatus]) << 8) | in[kRegEpStatus + 1]); + cfg.wire_clk_divider = in[kRegClkDivider]; + cfg.nr_leds = static_cast((static_cast(in[kRegNrLeds]) << 8) | in[kRegNrLeds + 1]); + cfg.rcv_timeout = static_cast((static_cast(in[kRegRcvTimeout]) << 8) | in[kRegRcvTimeout + 1]); +} + +// True iff the octet at relative offset addr belongs to a read-only +// register of the block — EP_LEN, the reserved octet, and both octets of +// base_clk. +constexpr bool reg_offset_read_only(uint16_t addr) noexcept { + return addr == kRegEpLen || addr == kRegReserved01 || addr == kRegBaseClk || + addr == static_cast(kRegBaseClk + 1); +} +} // namespace detail + +// The fixed width (octets) of the relative-start-address prefix every +// configuration request's payload begins with. +constexpr size_t kReconfigAddrLen = 2; + +enum class IseledReconfigErrc : int { + short_payload = 1, // payload carries no address prefix, or no data octet after it + out_of_range = 2, // start_address + data length exceeds kEpFuncLen — the whole write is ignored +}; + +inline const std::error_category& iseled_reconfig_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.iseled.reconfig"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case IseledReconfigErrc::short_payload: + return "rcp/iseled: configuration write has no address and data"; + case IseledReconfigErrc::out_of_range: + return "rcp/iseled: configuration write extends past the EP_func block"; + default: return "rcp/iseled: unknown configuration-write error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(IseledReconfigErrc e) noexcept { + return {static_cast(e), iseled_reconfig_category()}; +} + +// apply_reconfig applies the configuration escape hatch (evt[2:0] == 111b): +// payload is a 16-bit big-endian relative start address followed by the +// configuration data octets to write from that address onward (§12.7.1). +// Same octet-granularity patch, read-only-offset-skip, and +// out-of-range-ignores-the-whole-write rules as rcp/i2c.hpp's own +// apply_reconfig(). +inline std::error_code apply_reconfig(IseledFunctionalCfg& cfg, const uint8_t* payload, + size_t payload_len) noexcept { + if (payload_len <= kReconfigAddrLen) return make_error_code(IseledReconfigErrc::short_payload); + + const uint16_t start_address = + static_cast((static_cast(payload[0]) << 8) | payload[1]); + const size_t data_len = payload_len - kReconfigAddrLen; + + if (static_cast(start_address) + data_len > kEpFuncLen) + return make_error_code(IseledReconfigErrc::out_of_range); + + EpFuncBlock block = render_registers(cfg); + for (size_t i = 0; i < data_len; ++i) { + const uint16_t addr = static_cast(start_address + i); + if (detail::reg_offset_read_only(addr)) continue; + block[addr] = payload[kReconfigAddrLen + i]; + } + detail::parse_registers(cfg, block); + return {}; +} + +// encode_reconfig_request encodes an ACF_ABB configuration request +// (evt[2:0] == 111b) addressed to byte_bus_id: payload is start_address +// (16-bit big-endian) followed by data. Returns an empty vector if data is +// empty, or if the encoded payload would exceed acf::kAcfAbbMaxPayload. +inline std::vector encode_reconfig_request(avtp::ByteBusId byte_bus_id, uint16_t start_address, + const std::vector& data, + uint8_t transaction_num) { + if (data.empty()) return {}; + if (kReconfigAddrLen + data.size() > acf::kAcfAbbMaxPayload) return {}; + + std::vector payload(kReconfigAddrLen + data.size()); + payload[0] = static_cast(start_address >> 8); + payload[1] = static_cast(start_address & 0xFF); + std::copy(data.begin(), data.end(), payload.begin() + static_cast(kReconfigAddrLen)); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = true; // write + hdr.evt_op = 0x7; // evt[2:0] = 111b + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, payload); +} + +// ── Command request (write direction, §13.7.12.3 Figure 41) ────────────────── +// The payload is the raw plain Instruction/Address/Data content — never +// pre-encoded ISELED symbols; this module never inspects, strips, or +// reformats a byte of it at the ACF layer (see the file header). + +// encode_command_request encodes an ACF_ABB command request addressed to +// byte_bus_id: payload is exactly tx_data. +inline std::vector encode_command_request(avtp::ByteBusId byte_bus_id, + const std::vector& tx_data, + uint8_t transaction_num) { + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = true; // write + hdr.evt_op = 0; + hdr.transaction_num = transaction_num; + return acf::encode_acf_abb(hdr, tx_data); +} + +// decode_command_request decodes and validates an ACF-level ISELED command +// request. Rejects an op other than write with wrong_op, and evt[2:0] != +// 0b000 with bad_evt (acf::evt_row2_is_plain(), TC18 §13.5 Table 33). +inline std::error_code decode_command_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + std::vector& out_tx_data, + uint8_t& out_transaction_num) { + acf::AcfMessageInfo hdr; + std::vector payload; + const auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(IseledErrc::short_frame); + if (ec) return make_error_code(IseledErrc::bad_msg_type); + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(IseledErrc::wrong_bus); + if (!hdr.op) return make_error_code(IseledErrc::wrong_op); + if (!acf::evt_row2_is_plain(hdr.evt_op)) return make_error_code(IseledErrc::bad_evt); + + out_tx_data = std::move(payload); + out_transaction_num = hdr.transaction_num; return {}; } -// ── ACF byte_msg_payload codec (Figure 40/41, §13.7.12.3; issue cpp-RCP-A4-iseled) ── -// Before this pass, IseledRequest/IseledResponse and IseledEndpoint::transact -// operated purely on in-memory structs with range validation — nothing in -// this file packed/unpacked the 12-bit address + 12-bit data fields into -// actual ACF wire payload bytes, so no real ISELED request/response could be -// built or parsed for the wire despite ep_type 0x0C being otherwise wired -// up. encode_iseled_request/decode_iseled_request and -// encode_iseled_response/decode_iseled_response below are that missing -// byte-level codec. - -// Figure 40: "Instruction | Address | Data1 | Data2 | Data3 | padding" — the -// 4-bit Instruction and 12-bit Address share the first two payload bytes -// (instruction in byte0's top nibble, address's top 4 bits in byte0's low -// nibble, address's low 8 bits in byte1 — the same nibble-then-byte split -// this specification uses throughout, e.g. rcp/acf.hpp's own -// evt/byte_bus_id fields), followed by `data` verbatim. Any trailing -// padding octets Figure 40 shows are this codec's caller's concern to add, -// same as every other endpoint payload in this codebase (see e.g. -// rcp/acf.hpp's own AcfMessageInfo::pad convention) — encode_iseled_request -// does not itself round `data` up to any particular length. -constexpr size_t kIseledRequestFixedLen = 2; // Instruction(4 bits) + Address(12 bits), before variable-length Data - -inline std::vector encode_iseled_request(const IseledRequest& req) { - std::vector buf(kIseledRequestFixedLen + req.data.size()); - buf[0] = static_cast(((req.instruction & kIseledInstructionMask) << 4) | - ((req.address >> 8) & 0x0F)); - buf[1] = static_cast(req.address & 0xFF); - std::copy(req.data.begin(), req.data.end(), buf.begin() + static_cast(kIseledRequestFixedLen)); - return buf; -} - -inline std::error_code decode_iseled_request(const uint8_t* buf, size_t len, IseledRequest& out) noexcept { - if (len < kIseledRequestFixedLen) return avtp::make_error_code(avtp::AvtpErrc::short_buffer); - out.instruction = static_cast((buf[0] >> 4) & kIseledInstructionMask); - out.address = static_cast(((buf[0] & 0x0F) << 8) | buf[1]); - out.data.assign(buf + kIseledRequestFixedLen, buf + len); +// ── Read request (read direction, issue #471, REQ-ISELED-030/031) ──────────── +// TC18 §13.7.12.1: "Upon read requests the responses are collected 5/4bit +// decoded and aggregated into one or multiple ACF [messages] up to the +// requested read_size." A read request may still carry a payload — the +// plain Instruction/Address content selecting what to read back — exactly +// as an I2C read carries the target register address (rcp/i2c.hpp's own +// analogous decode_transfer_request()); only the Data octets are +// meaningless on a read. The write-direction encode_command_request()/ +// decode_command_request() pair above is completely unchanged by this: +// it continues to model op=write only, and continues to reject a read-op +// frame with wrong_op. + +// Largest value the ACF header's 12-bit read_size_or_segment_num field can +// carry. +constexpr uint16_t kMaxReadSize = 0x0FFFu; + +// encode_read_request encodes an ACF_ABB read request addressed to +// byte_bus_id: payload is exactly tx_data (the plain Instruction/Address +// content selecting what to read back; no Data octets), and the ACF +// header's own read_size_or_segment_num field carries read_size (0-4095). +// Returns an empty vector if read_size exceeds kMaxReadSize or tx_data +// exceeds acf::kAcfAbbMaxPayload. +inline std::vector encode_read_request(avtp::ByteBusId byte_bus_id, + const std::vector& tx_data, uint16_t read_size, + uint8_t transaction_num) { + if (read_size > kMaxReadSize) return {}; + if (tx_data.size() > acf::kAcfAbbMaxPayload) return {}; + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read + hdr.evt_op = 0; + hdr.transaction_num = transaction_num; + hdr.read_size_or_segment_num = read_size; + return acf::encode_acf_abb(hdr, tx_data); +} + +// decode_read_request decodes and validates an ACF-level ISELED read +// request. Rejects an op other than read with wrong_op (the mirror image +// of decode_command_request()'s own check), and evt[2:0] != 0b000 with +// bad_evt. +inline std::error_code decode_read_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + std::vector& out_tx_data, uint16_t& out_read_size, + uint8_t& out_transaction_num) { + acf::AcfMessageInfo hdr; + std::vector payload; + const auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(IseledErrc::short_frame); + if (ec) return make_error_code(IseledErrc::bad_msg_type); + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(IseledErrc::wrong_bus); + if (hdr.op) return make_error_code(IseledErrc::wrong_op); + if (!acf::evt_row2_is_plain(hdr.evt_op)) return make_error_code(IseledErrc::bad_evt); + + out_tx_data = std::move(payload); + out_read_size = hdr.read_size_or_segment_num; + out_transaction_num = hdr.transaction_num; return {}; } -// Figure 41: "Address | Data[11:0] | CRC (optional) | rsvd" — Address (12 -// bits) and Data[11:0] (12 bits) pack into 3 bytes (address's 8 high bits -// in byte0, address's low 4 bits + data's top 4 bits sharing byte1's two -// nibbles, data's low 8 bits in byte2). The optional 4-bit CRC + rsvd bits -// Figure 41 also shows are NOT encoded here: this repository has no -// verified copy of the ISELED standard's own CRC algorithm (see this -// header's own top-of-file comment on the removed invented CRC-8), so — -// same as the rest of this file — this codec only represents the -// Address/Data fields it can source confidently, not the CRC/rsvd trailer. -constexpr size_t kIseledResponseLen = 3; // Address(12 bits) + Data[11:0](12 bits); no CRC/rsvd - -inline std::vector encode_iseled_response(const IseledResponse& resp) { - std::vector buf(kIseledResponseLen); - const uint16_t addr = resp.address & kIseledFieldMask; - const uint16_t data = resp.data & kIseledFieldMask; - buf[0] = static_cast(addr >> 4); - buf[1] = static_cast(((addr & 0x0F) << 4) | ((data >> 8) & 0x0F)); - buf[2] = static_cast(data & 0xFF); - return buf; -} - -inline std::error_code decode_iseled_response(const uint8_t* buf, size_t len, IseledResponse& out) noexcept { - if (len != kIseledResponseLen) return avtp::make_error_code(avtp::AvtpErrc::short_buffer); - out.address = static_cast((static_cast(buf[0]) << 4) | (buf[1] >> 4)); - out.data = static_cast(((buf[1] & 0x0F) << 8) | buf[2]); +// ── Response (§13.7.12.3 Figure 42) ─────────────────────────────────────────── + +// encode_response encodes an ISELED response carrying rx_data as its +// payload, echoing transaction_num. Encoded as ACF_ABB when timed is +// false; ACF_GBB (message_timestamp = timestamp, mtv valid) when timed is +// true. +inline std::vector encode_response(avtp::ByteBusId byte_bus_id, const std::vector& rx_data, + uint8_t transaction_num, bool timed, uint64_t timestamp) { + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read + hdr.rsp = true; + hdr.evt_op = 0; + hdr.transaction_num = transaction_num; + + if (timed) { + hdr.mtv = true; + return acf::encode_acf_gbb(hdr, timestamp, rx_data); + } + return acf::encode_acf_abb(hdr, rx_data); +} + +// decode_response decodes an ISELED response from either an ACF_ABB or +// ACF_GBB message (peeks the message type itself, since a response's +// encoding depends on the responding endpoint's own timed/untimed choice). +inline std::error_code decode_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + std::vector& out_rx_data, bool& out_timed, + uint64_t& out_timestamp, uint8_t& out_transaction_num) { + uint8_t msg_type = 0; + if (acf::peek_msg_type(b, len, msg_type)) return make_error_code(IseledErrc::short_frame); + + acf::AcfMessageInfo hdr; + std::vector payload; + bool timed = false; + uint64_t timestamp = 0; + + if (msg_type == acf::kAcfMsgTypeGbb) { + uint64_t ts = 0; + const auto ec = acf::decode_acf_gbb(b, len, hdr, ts, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(IseledErrc::short_frame); + if (ec) return make_error_code(IseledErrc::bad_msg_type); + timed = hdr.mtv; + timestamp = timed ? ts : 0; + } else { + const auto ec = acf::decode_acf_abb(b, len, hdr, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(IseledErrc::short_frame); + if (ec) return make_error_code(IseledErrc::bad_msg_type); + } + + if (hdr.byte_bus_id != expected_bus_id) return make_error_code(IseledErrc::wrong_bus); + + out_rx_data = std::move(payload); + out_timed = timed; + out_timestamp = timestamp; + out_transaction_num = hdr.transaction_num; return {}; } +// ── Fragmented response (REQ-ISELED-025/040, rcp/fragment.hpp) ─────────────── +// TC18 §13.7.12.1: "Upon read requests the responses are collected 5/4bit +// decoded and aggregated into one or multiple ACF [messages] up to the +// requested read_size." Wired directly to rcp/fragment.hpp's own +// fragment::plan_count()/fragment::plan() — c-RCP's ep_iseled.c genuinely +// `#include`s "rcp/fragment.h" and calls its equivalents directly rather +// than implementing its own ISELED-specific multi-response splitting logic +// (see the file header). + +// response_fragment_count: the number of ACF messages +// encode_response_fragmented() would produce for available_len octets of +// already-decoded ISELED data, first capped to at most read_size octets, +// then split into fragments of at most max_fragment_payload octets each. +inline size_t response_fragment_count(size_t available_len, uint16_t read_size, + size_t max_fragment_payload) noexcept { + const size_t capped_len = std::min(available_len, static_cast(read_size)); + return fragment::plan_count(capped_len, max_fragment_payload); +} + +// encode_response_fragmented encodes an ISELED response as one or more ACF +// messages, first capping rx_data to at most read_size octets (TC18 +// §13.7.12.1's own response-aggregation ceiling), then fragmenting via +// rcp/fragment.hpp's ms/segment_num mechanism whenever the capped data +// exceeds max_fragment_payload octets. Every fragment shares byte_bus_id/ +// op(read)/transaction_num/timed/timestamp with encode_response(); only the +// ms flag, the read_size_or_segment_num field, and each fragment's own +// payload slice differ. Returns an empty vector under the same conditions +// fragment::plan_count() returns 0 for (fragmentation disabled or too many +// segments needed). +inline std::vector> encode_response_fragmented( + avtp::ByteBusId byte_bus_id, const std::vector& rx_data, uint16_t read_size, + uint8_t transaction_num, bool timed, uint64_t timestamp, size_t max_fragment_payload) { + const size_t capped_len = std::min(rx_data.size(), static_cast(read_size)); + const size_t count = fragment::plan_count(capped_len, max_fragment_payload); + if (count == 0) return {}; + + std::vector segs(count); + if (fragment::plan(capped_len, max_fragment_payload, segs.data(), count)) return {}; + + std::vector> out; + out.reserve(count); + for (const auto& seg : segs) { + std::vector slice(rx_data.begin() + static_cast(seg.offset), + rx_data.begin() + static_cast(seg.offset + seg.len)); + + acf::AcfMessageInfo hdr; + hdr.byte_bus_id = byte_bus_id; + hdr.op = false; // read + hdr.rsp = true; + hdr.evt_op = 0; + hdr.transaction_num = transaction_num; + hdr.ms = seg.ms; + hdr.read_size_or_segment_num = seg.ms ? seg.segment_num : static_cast(0); + + if (timed) { + hdr.mtv = true; + out.push_back(acf::encode_acf_gbb(hdr, timestamp, slice)); + } else { + out.push_back(acf::encode_acf_abb(hdr, slice)); + } + } + return out; +} + // ── Trigger signals ─────────────────────────────────────────────────────────── -// One TransferComplete signal per IseledEndpoint instance, built on -// rcp/endpoint.hpp's generic TriggerRegistry, same primitive -// rcp/i2c.hpp's and rcp/spi.hpp's transfer-complete signals use. +// One TransferComplete signal per IseledEndpoint instance — the endpoint's +// one asynchronous-event trigger mode, matching IseledTrigger above. enum class IseledSignal : uint8_t { TransferComplete = 0 }; @@ -239,75 +792,41 @@ constexpr endpoint::TriggerRegistry::SignalId iseled_signal_id(IseledSignal sig) } // ── IseledEndpoint ──────────────────────────────────────────────────────────── -// Mirrors rcp::spi::SpiEndpoint's/rcp::i2c::I2cEndpoint's shape: one -// request-dispatch entry point per daisy-chain transaction, recording the -// exact Instruction/Address/Data request and Address/Data response -// (supplied together by the caller — this header models the request/ -// response and trigger-signal shape of one ISELED transaction, not an -// actual daisy-chain driver or its per-device propagation timing). A -// request or response whose fields do not fit their documented wire widths -// is rejected without being recorded. +// A per-transaction convenience wrapper (no c-RCP equivalent — see +// rcp/i2c.hpp's I2cEndpoint for the identical rationale): records raw +// tx/rx byte streams and fires TransferComplete. class IseledEndpoint { public: - std::error_code transact(IseledRequest request, IseledResponse response) { - if (auto ec = validate_request(request)) return ec; - if (auto ec = validate_response(response)) return ec; - - last_request_ = std::move(request); - last_response_ = std::move(response); + // Records a plain-mode ISELED command's raw tx bytes as sent (mirroring + // encode_command_request/decode_command_request's own raw-byte-stream + // model) and fires TransferComplete. + void send(std::vector tx_data) { + last_sent_ = std::move(tx_data); triggers_.notify(iseled_signal_id(IseledSignal::TransferComplete)); - return {}; } - // handle_request is ISELED's request-decode entry point — the piece - // this header previously had none of, mirroring rcp::i2c:: - // I2cEndpoint::handle_request's shape (this repo's seventh Table 33 - // Row 2 endpoint type after I2C, ADC, PWM_IN, LIN, CAN, and UART). It - // classifies the incoming request's evt[2:0] field via rcp::endpoint:: - // evt_row2_kind_of before doing anything else, so a Reserved value can - // never reach transact() and be misread as an ordinary transaction, - // and a ConfigWrite value can never be silently accepted or silently - // dropped: - // - Plain (evt[2:0] == 000b): delegates straight to transact(request, - // response) with both arguments unchanged — ISELED's existing - // request/response transaction model (extraction §13.7.12.3) - // already IS this row's correct "plain request" behavior; evt[2:0] - // carries no combinable value or channel selector for this row. - // - Reserved (evt[2:0] in 001b-110b): returns + // Records a response's raw rx bytes as received (see encode_response/ + // decode_response above). Does not itself fire a trigger — ISELED has + // only the one TransferComplete signal, already fired by send() for the + // paired command. + void receive(std::vector rx_data) { last_received_ = std::move(rx_data); } + + // handle_request classifies the incoming request's evt[2:0] field via + // rcp::endpoint::evt_row2_kind_of before doing anything else: + // - Plain (evt[2:0] == 000b): delegates to send() unchanged. + // - Reserved (001b-110b): returns // endpoint::EndpointErrc::reserved_evt_row2 without recording - // anything (last_request_/last_response_ are left exactly as they - // were, and TransferComplete does not fire) — TC18 requires this be - // rejected with error code UNSUPPORTED_CMD. - // - ConfigWrite (evt[2:0] == 111b): §12.7.1's configuration-write - // shape targets the ISELED EP's own functional-config block, not a - // daisy-chain transaction at all. Full handling is deliberately out - // of scope for this milestone (nontrivial — it needs - // EP_functional-config wiring this header does not yet have, the - // same gap I2C's, ADC's, PWM_IN's, LIN's, CAN's, and UART's own - // handle_request comments defer for the identical reason); this - // returns IseledErrc::config_write_not_supported rather than - // crashing, silently accepting the request as a transaction, or - // silently doing nothing. - // - // NOT to be confused with this header's own Figure 40/41 wire-format - // fix directly above (the 12-bit address/data field widths, and the - // removed invented CRC-8): that fix concerns what IseledRequest's and - // IseledResponse's *fields* mean and how wide they are; Table 33's - // evt[2:0] classification is an entirely separate, orthogonal concern - // about which *kind* of request evt[2:0] itself selects. Reading - // evt[2:0] as if it also selected or combined with instruction/address/ - // data, or inventing any encoding of those fields beyond what - // validate_request/validate_response and encode_iseled_request/ - // decode_iseled_request/encode_iseled_response/decode_iseled_response - // already define, would be exactly the kind of invented, non-spec- - // derived encoding this header's own top-of-file comment documents - // having to remove once already. handle_request below calls the same - // shared evt_row2_kind_of every other Row 2 endpoint type uses and - // invents nothing of its own. - std::error_code handle_request(uint8_t evt_op, IseledRequest request, IseledResponse response) { + // anything. + // - ConfigWrite (111b): returns IseledErrc::config_write_not_supported + // — see that enumerator's own comment (and I2cErrc's identical + // one) for why this convenience call's single-tx_data shape cannot + // carry a reconfig payload, and where the real, now-implemented + // mechanism lives (apply_reconfig()/render_registers() above). + std::error_code handle_request(uint8_t evt_op, std::vector tx_data) { switch (endpoint::evt_row2_kind_of(evt_op)) { case endpoint::EvtRow2Kind::Plain: - return transact(std::move(request), std::move(response)); + send(std::move(tx_data)); + return {}; case endpoint::EvtRow2Kind::Reserved: return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); case endpoint::EvtRow2Kind::ConfigWrite: @@ -316,21 +835,24 @@ class IseledEndpoint { return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); // unreachable } - const IseledRequest& last_request() const noexcept { return last_request_; } - const IseledResponse& last_response() const noexcept { return last_response_; } + const std::vector& last_sent() const noexcept { return last_sent_; } + const std::vector& last_received() const noexcept { return last_received_; } endpoint::TriggerRegistry& triggers() noexcept { return triggers_; } private: endpoint::TriggerRegistry triggers_; - IseledRequest last_request_; - IseledResponse last_response_; + std::vector last_sent_; + std::vector last_received_; }; } // namespace iseled } // namespace rcp -// Enable std::error_code construction from rcp::iseled::IseledErrc. +// Enable std::error_code construction from rcp::iseled::IseledErrc / +// IseledReconfigErrc. namespace std { template <> struct is_error_code_enum : true_type {}; +template <> +struct is_error_code_enum : true_type {}; } // namespace std diff --git a/include/rcp/mock.hpp b/include/rcp/mock.hpp index bd636bb..acff7bd 100644 --- a/include/rcp/mock.hpp +++ b/include/rcp/mock.hpp @@ -60,9 +60,12 @@ // extension of a pre-existing dispatch_uart(); see dispatch_uart's own // comment for why UART needed its own req.op-branching shape; ISELED's is // this header's FIRST wiring of rcp::iseled::IseledEndpoint at all — see -// dispatch_iseled's own comment for why it decodes/encodes the wire -// payload via ISELED's own existing Figure 40/41 codec rather than passing -// raw bytes through untouched; MDIO's is this header's FIRST wiring of +// dispatch_iseled's own comment for why it passes the raw byte_msg_payload +// straight through to IseledEndpoint::handle_request, same as dispatch_i2c/ +// dispatch_lin do (Phase 3's rcp/iseled.hpp rewrite replaced its earlier +// structured Address/Data ACF-payload model with the same raw-byte-stream +// codec I2C/LIN already use — see rcp/iseled.hpp's own header comment); +// MDIO's is this header's FIRST wiring of // rcp::mdio::MdioEndpoint at all — see dispatch_mdio's own comment for why, // unlike ISELED, no MDIO byte-level wire codec exists anywhere in this // codebase to decode/encode against, and what deliberately simplified @@ -214,8 +217,6 @@ inline acf::WireErrorCode wire_error_code_for(const std::error_code& ec) noexcep return acf::WireErrorCode::InvalidParameter; // write payload would overflow the TX queue — client-caused, same rationale as read_size_exceeds_bound above if (ec == make_error_code(iseled::IseledErrc::config_write_not_supported)) return acf::WireErrorCode::UnsupportedCmd; // evt[2:0]==111b config-write, not yet implemented by this mock - if (ec == make_error_code(iseled::IseledErrc::field_out_of_range)) - return acf::WireErrorCode::InvalidParameter; // instruction/address/data exceeds its documented wire field width — client-caused, same rationale as avtp::short_buffer's mapping above if (ec == make_error_code(mdio::MdioErrc::config_write_not_supported)) return acf::WireErrorCode::UnsupportedCmd; // evt[2:0]==111b config-write, not yet implemented by this mock if (ec == make_error_code(mdio::MdioErrc::payload_exceeds_mode_width)) @@ -351,19 +352,18 @@ class Server final { // dispatched read request drains whatever rx_fill() has already put // there. - // set_iseled_response scripts the IseledResponse (address/data) value a - // subsequent dispatch()-driven ISELED transaction records — the same - // "test scripts the bus, this mock does not model actual hardware" - // pattern set_i2c_response/set_lin_response above already establish. - // Unlike those two, which script a raw byte vector, ISELED's response - // is already a fixed Address/Data struct (Figure 41), so this scripts - // that struct directly rather than a byte vector. Applies to the next + // set_iseled_response scripts the raw response bytes a subsequent + // dispatch()-driven ISELED read transaction records — the same "test + // scripts the bus, this mock does not model actual hardware" pattern + // set_i2c_response/set_lin_response above already establish (ISELED's + // ACF-level codec is a raw byte stream, same as I2C/LIN — see + // rcp/iseled.hpp's own header comment on why it is not the earlier, + // now-replaced structured Address/Data model). Applies to the next // plain (evt[2:0]==000b) ISELED request only in spirit — like // set_i2c_response, it stays in effect until overwritten, there is no - // auto-clear. Defaults to IseledResponse{0, 0} (a validly in-range, - // all-zero response) until first called. - void set_iseled_response(iseled::IseledResponse response) { - iseled_response_ = response; + // auto-clear. Defaults to an empty response until first called. + void set_iseled_response(std::vector response) { + iseled_response_ = std::move(response); } // No set_mdio_response()-shaped hook here either, but for yet a @@ -744,39 +744,28 @@ class Server final { return {}; } - // ISELED, same as I2C/LIN (extraction §13.7.12.3), pairs a request with - // a full Address/Data response in a single transaction rather than a - // read/write-branched register — this dispatch path does not branch on + // ISELED, same as I2C/LIN (§13.7.12.3), operates on the raw + // byte_msg_payload directly — this dispatch path does not branch on // req.op either, same rationale as dispatch_i2c/dispatch_lin's own - // comment. Unlike I2C/LIN, whose transfer()/handle_request calls - // operate on raw std::vector payload bytes directly, ISELED - // already has a real Figure 40/41 byte-level codec - // (encode_iseled_request/decode_iseled_request, - // encode_iseled_response/decode_iseled_response — see rcp/iseled.hpp's - // own header comment on why those exist and what they do and do not - // cover), so this dispatch path decodes `payload` into an - // IseledRequest via decode_iseled_request and encodes - // IseledEndpoint::handle_request's resulting response back via - // encode_iseled_response, rather than passing raw bytes through - // untouched the way dispatch_i2c/dispatch_lin do — this is calling - // rcp/iseled.hpp's own pre-existing codec, not inventing a new one. - // Table 33 Row 2's 3-way Plain/Reserved/ConfigWrite classification - // (rcp::endpoint::evt_row2_kind_of) is checked by handle_request - // itself, before request/response is ever recorded by transact() — see + // comment, and passes `payload` straight to + // IseledEndpoint::handle_request unchanged, the same "mock has already + // ACF-decoded the frame, so it calls the endpoint's own dispatch entry + // point with the raw payload rather than re-invoking rcp/iseled.hpp's + // own encode/decode_command_request() codec a second time" pattern + // dispatch_i2c uses. Table 33 Row 2's 3-way Plain/Reserved/ConfigWrite + // classification (rcp::endpoint::evt_row2_kind_of) is checked by + // handle_request itself, before anything is recorded — see // handle_request's own doc comment for why a Reserved or ConfigWrite // evt must never reach it. // // This mock has no real ISELED daisy-chain hardware behind it (same // disclaimer every other endpoint type in this file carries), so the - // Address/Data response value transact() records is whatever - // set_iseled_response() last scripted (default-constructed, - // IseledResponse{0, 0}, if never called) — the same "test scripts the - // bus, this mock does not model actual hardware" pattern - // set_i2c_response/set_lin_response already establish for their own - // bus-transfer models. A successful request's response payload is - // whatever handle_request recorded, encoded back via - // encode_iseled_response, answered as a ReadResponse, mirroring - // dispatch_i2c/dispatch_lin's read-response shape. + // response bytes recorded on a successful Plain request are whatever + // set_iseled_response() last scripted (empty if never called) — the + // same "test scripts the bus, this mock does not model actual + // hardware" pattern set_i2c_response/set_lin_response already establish + // for their own bus-transfer models. Answered as a ReadResponse, + // mirroring dispatch_i2c/dispatch_lin's read-response shape. std::error_code dispatch_iseled(const acf::AcfMessageInfo& req, const std::vector& payload, acf::AcfMessageInfo& out_resp, std::vector& out_resp_payload) noexcept { @@ -784,12 +773,10 @@ class Server final { return set_error_response(req, make_error_code(regmap::RegMapErrc::request_rejected), out_resp, out_resp_payload); } - iseled::IseledRequest request; - auto ec = iseled::decode_iseled_request(payload.data(), payload.size(), request); - if (ec) return set_error_response(req, ec, out_resp, out_resp_payload); - ec = iseled_.handle_request(req.evt_op, request, iseled_response_); + auto ec = iseled_.handle_request(req.evt_op, payload); if (ec) return set_error_response(req, ec, out_resp, out_resp_payload); - out_resp_payload = iseled::encode_iseled_response(iseled_.last_response()); + iseled_.receive(iseled_response_); + out_resp_payload = iseled_.last_received(); out_resp = acf::make_response(req, req.evt_ack ? acf::ResponseKind::Acknowledge : acf::ResponseKind::ReadResponse); return {}; @@ -801,9 +788,10 @@ class Server final { // request itself, so read vs write really is a different operation // here, not a single full-duplex/raw-transfer call the way SPI/I2C/ // LIN's dispatch paths stay op-agnostic (see dispatch_uart's own - // comment for the identical rationale). Unlike ISELED, which already - // has a real Figure 40/41 byte-level codec to decode/encode `payload` - // against, NO MDIO byte-level wire codec (Figure 43/Table 60) exists + // comment for the identical rationale). Unlike ISELED/I2C/LIN, whose + // ACF byte_msg_payload IS the raw wire content itself (no further + // decode needed at this dispatch layer), NO MDIO byte-level wire codec + // (Figure 43/Table 60) exists // anywhere in this codebase yet — mdio.hpp deliberately stops at the // (mode, mdio_address, mdio_payload) struct level (see its own header // comment on the addressing-model fix), the same gap dispatch_can's own @@ -876,7 +864,7 @@ class Server final { std::deque adc_samples_{}; std::vector lin_response_{}; bool lin_responded_ = true; - iseled::IseledResponse iseled_response_{}; + std::vector iseled_response_{}; }; } // namespace mock diff --git a/tests/test_i2c.cpp b/tests/test_i2c.cpp index 3eb1cbc..82de407 100644 --- a/tests/test_i2c.cpp +++ b/tests/test_i2c.cpp @@ -5,60 +5,518 @@ // fusa:test REQ-I2C-005 // fusa:test REQ-I2C-006 // fusa:test REQ-I2C-007 +// fusa:test REQ-I2C-008 +// fusa:test REQ-I2C-009 +// fusa:test REQ-I2C-010 +// fusa:test REQ-I2C-011 +// fusa:test REQ-I2C-012 +// fusa:test REQ-I2C-013 +// fusa:test REQ-I2C-014 +// fusa:test REQ-I2C-015 +// fusa:test REQ-I2C-016 +// fusa:test REQ-I2C-017 +// fusa:test REQ-I2C-018 +// fusa:test REQ-I2C-020 +// fusa:test REQ-I2C-021 +// fusa:test REQ-I2C-022 +// fusa:test REQ-I2C-023 +// fusa:test REQ-I2C-024 +// fusa:test REQ-I2C-025 +// fusa:test REQ-I2C-026 -// Tests for rcp/i2c.hpp — the I2C endpoint type (ROADMAP.md milestone 48, -// "Basic Endpoint Types II — I2C, UART, ADC, PWM_OUT, PWM_IN", v2.4.0). +// Tests for rcp/i2c.hpp — the I2C endpoint type (ep_type 0x04), ported from +// c-RCP's tests/test_ep_i2c.c (this project's RC5-spec-conformant +// reference) as part of Phase 3 of the ground-up rewrite (cpp-RCP issue +// #129, ROADMAP.md "Phase 17"). #include +#include #include +#include using namespace rcp::i2c; -// ── i2c_mode open item ─────────────────────────────────────────────────────── +// ── i2c_mode ─────────────────────────────────────────────────────────────────── -TEST_CASE("i2c_mode_of decodes only the coarse high-speed-requested bit", "[i2c][REQ-I2C-001]") { - REQUIRE(i2c_mode_of(false) == I2cMode::Standard); - REQUIRE(i2c_mode_of(true) == I2cMode::HighSpeed); +TEST_CASE("i2c_mode_valid accepts 0..4 and rejects everything else", "[i2c][REQ-I2C-001]") { + for (uint8_t v = 0; v <= 4; ++v) REQUIRE(i2c_mode_valid(v)); + REQUIRE_FALSE(i2c_mode_valid(5)); + REQUIRE_FALSE(i2c_mode_valid(255)); } -// ── Compound-wait arbitrary-bit-sequence match ─────────────────────────────── +// ── Functional config ───────────────────────────────────────────────────────── -TEST_CASE("compound_wait_matches_bits compares an exact whole-byte-multiple bit length", - "[i2c][REQ-I2C-002]") { - std::vector received{0b10110000, 0b11110000}; - std::vector expected{0b10110000, 0b11110000}; - REQUIRE(compound_wait_matches_bits(received, expected, 16)); +TEST_CASE("i2c_functional_cfg_init zeroes every field", "[i2c][REQ-I2C-002]") { + I2cFunctionalCfg cfg; + cfg.ep_enable = cfg.ep_clear_req_storage = cfg.ep_req_crc_enable = true; + cfg.i2c_mode = static_cast(I2cMode::UltraFast); + cfg.ep_status = 0xBEEF; + cfg.clock_divider = 0xAA; + cfg.trail = 0xCC; + + i2c_functional_cfg_init(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); + REQUIRE(cfg.i2c_mode == static_cast(I2cMode::Standard)); + REQUIRE(cfg.ep_status == 0); + REQUIRE(cfg.clock_divider == 0); + REQUIRE(cfg.trail == 0); } -TEST_CASE("compound_wait_matches_bits compares only the top bits of a partial final byte", - "[i2c][REQ-I2C-002]") { - std::vector received{0b10110000, 0b11111111}; // low nibble of byte 1 differs - std::vector expected{0b10110000, 0b11110000}; - REQUIRE(compound_wait_matches_bits(received, expected, 12)); // 8 + top 4 bits of byte 1 +TEST_CASE("i2c_functional_cfg_writable is false in HwUnconfigured regardless of writer", + "[i2c][REQ-I2C-003]") { + rcp::lifecycle::WriterCtx writer; + writer.via_root_client_ep0 = true; + writer.via_owning_stream = true; + REQUIRE_FALSE(i2c_functional_cfg_writable(rcp::lifecycle::ServerState::HwUnconfigured, writer)); } -TEST_CASE("compound_wait_matches_bits reports a mismatch within the compared window", - "[i2c][REQ-I2C-002]") { - std::vector received{0b10110000}; - std::vector expected{0b10100000}; - REQUIRE_FALSE(compound_wait_matches_bits(received, expected, 8)); +TEST_CASE("i2c_functional_cfg_writable in HwConfigured requires EP0/owning-stream/discovery-stream", + "[i2c][REQ-I2C-004]") { + rcp::lifecycle::WriterCtx none, via_ep0, via_stream, via_discovery; + via_ep0.via_root_client_ep0 = true; + via_stream.via_owning_stream = true; + via_discovery.via_discovery_stream = true; + + REQUIRE_FALSE(i2c_functional_cfg_writable(rcp::lifecycle::ServerState::HwConfigured, none)); + REQUIRE(i2c_functional_cfg_writable(rcp::lifecycle::ServerState::HwConfigured, via_ep0)); + REQUIRE(i2c_functional_cfg_writable(rcp::lifecycle::ServerState::HwConfigured, via_stream)); + REQUIRE(i2c_functional_cfg_writable(rcp::lifecycle::ServerState::HwConfigured, via_discovery)); } -TEST_CASE("compound_wait_matches_bits returns false for a zero bit_len or short buffers", - "[i2c][REQ-I2C-002]") { - std::vector received{0x01}; - std::vector expected{0x01}; - REQUIRE_FALSE(compound_wait_matches_bits(received, expected, 0)); - REQUIRE_FALSE(compound_wait_matches_bits(received, expected, 16)); // not enough bytes +TEST_CASE("i2c_functional_cfg_writable in RcpConfigured requires EP0/owning-stream, not discovery", + "[i2c][REQ-I2C-005]") { + rcp::lifecycle::WriterCtx none, via_ep0, via_stream; + via_ep0.via_root_client_ep0 = true; + via_stream.via_owning_stream = true; + + REQUIRE_FALSE(i2c_functional_cfg_writable(rcp::lifecycle::ServerState::RcpConfigured, none)); + REQUIRE(i2c_functional_cfg_writable(rcp::lifecycle::ServerState::RcpConfigured, via_ep0)); + REQUIRE(i2c_functional_cfg_writable(rcp::lifecycle::ServerState::RcpConfigured, via_stream)); } -// ── Controller-only raw byte-stream transfer, address bytes included ──────── +TEST_CASE("set_mode rejects an invalid mode regardless of authorization", "[i2c][REQ-I2C-006]") { + I2cFunctionalCfg cfg; + i2c_functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx authorized; + authorized.via_root_client_ep0 = true; -TEST_CASE("I2cEndpoint::transfer records the raw sent/received byte streams", - "[i2c][REQ-I2C-003]") { + REQUIRE_FALSE(set_mode(cfg, static_cast(99), rcp::lifecycle::ServerState::HwConfigured, authorized)); + REQUIRE(cfg.i2c_mode == static_cast(I2cMode::Standard)); +} + +TEST_CASE("set_mode rejects an unauthorized writer even with a valid mode", "[i2c][REQ-I2C-007]") { + I2cFunctionalCfg cfg; + i2c_functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; + + REQUIRE_FALSE(set_mode(cfg, I2cMode::Fast, rcp::lifecycle::ServerState::RcpConfigured, none)); + REQUIRE(cfg.i2c_mode == static_cast(I2cMode::Standard)); +} + +TEST_CASE("set_mode applies when valid and authorized", "[i2c][REQ-I2C-008]") { + I2cFunctionalCfg cfg; + i2c_functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + + REQUIRE(set_mode(cfg, I2cMode::HighSpeed, rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.i2c_mode == static_cast(I2cMode::HighSpeed)); +} + +// ── The EP_func register block ──────────────────────────────────────────────── + +TEST_CASE("render_registers matches the corrected Table 49 offsets", "[i2c][REQ-I2C-021]") { + I2cFunctionalCfg cfg; + i2c_functional_cfg_init(cfg); + cfg.ep_enable = true; + cfg.ep_status = 0x1234; + cfg.clock_divider = 0x55; + cfg.i2c_mode = static_cast(I2cMode::UltraFast); + cfg.trail = 0x77; + + const auto out = render_registers(cfg); + + REQUIRE(out[kRegEpLen] == static_cast(kEpFuncLen)); + REQUIRE(out[kRegReserved01] == 0); + REQUIRE((out[kRegEpEnableClr] & 0x01) != 0); + REQUIRE(out[kRegBaseClk] == 0); // base_clk always renders 0 + REQUIRE(out[kRegBaseClk + 1] == 0); + REQUIRE(out[kRegEpStatus] == 0x12); + REQUIRE(out[kRegEpStatus + 1] == 0x34); + REQUIRE(out[kRegClockDivider] == 0x55); + REQUIRE(out[kRegMode] == static_cast(I2cMode::UltraFast)); + REQUIRE(out[kRegTrail] == 0x77); + REQUIRE(kEpFuncLen == 0x000Bu); +} + +TEST_CASE("apply_reconfig writes the clock divider register", "[i2c][REQ-I2C-022]") { + I2cFunctionalCfg cfg; + i2c_functional_cfg_init(cfg); + const uint8_t payload[3] = {0x00, static_cast(kRegClockDivider), 0x42}; + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + REQUIRE(cfg.clock_divider == 0x42); +} + +TEST_CASE("apply_reconfig writes a multi-register span", "[i2c][REQ-I2C-022]") { + I2cFunctionalCfg cfg; + i2c_functional_cfg_init(cfg); + const uint8_t payload[6] = {0x00, static_cast(kRegEpStatus), + 0xAB, 0xCD, // ep_status + 0x03, // clock_divider + static_cast(I2cMode::Fast)}; // i2c_mode + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + REQUIRE(cfg.ep_status == 0xABCD); + REQUIRE(cfg.clock_divider == 0x03); + REQUIRE(cfg.i2c_mode == static_cast(I2cMode::Fast)); +} + +TEST_CASE("apply_reconfig ignores read-only registers (EP_LEN/reserved)", "[i2c][REQ-I2C-022]") { + I2cFunctionalCfg cfg; + i2c_functional_cfg_init(cfg); + const uint8_t payload[6] = {0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF}; + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + + const auto out = render_registers(cfg); + REQUIRE(out[kRegEpLen] == static_cast(kEpFuncLen)); + REQUIRE(out[kRegReserved01] == 0); +} + +// MC/DC: the read-only-offset OR chain's base_clk arms need their own +// isolated single-octet writes (offsets 0x00-0x03 alone never reaches +// 0x04/0x05) — matches c-RCP's own dedicated regression for this. +TEST_CASE("apply_reconfig ignores base_clk's own two octets individually", "[i2c][REQ-I2C-021]") { + I2cFunctionalCfg cfg; + i2c_functional_cfg_init(cfg); + const uint8_t payload[4] = {0x00, static_cast(kRegBaseClk), 0xFF, 0xFF}; + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + + const auto out = render_registers(cfg); + REQUIRE(out[kRegBaseClk] == 0); + REQUIRE(out[kRegBaseClk + 1] == 0); +} + +TEST_CASE("apply_reconfig rejects a write extending past EP_LEN", "[i2c][REQ-I2C-022]") { + I2cFunctionalCfg cfg; + i2c_functional_cfg_init(cfg); + const uint8_t payload[3] = {0x00, static_cast(kEpFuncLen), 0xFF}; + + REQUIRE(apply_reconfig(cfg, payload, sizeof(payload)) == make_error_code(I2cReconfigErrc::out_of_range)); + REQUIRE(cfg.trail == 0); +} + +TEST_CASE("apply_reconfig rejects a payload with no data octet", "[i2c][REQ-I2C-022]") { + I2cFunctionalCfg cfg; + i2c_functional_cfg_init(cfg); + const uint8_t addr_only[2] = {0x00, 0x08}; + + REQUIRE(apply_reconfig(cfg, addr_only, sizeof(addr_only)) == + make_error_code(I2cReconfigErrc::short_payload)); + REQUIRE(apply_reconfig(cfg, nullptr, 0) == make_error_code(I2cReconfigErrc::short_payload)); +} + +TEST_CASE("encode_reconfig_request round-trips through acf::decode_acf_abb", "[i2c][REQ-I2C-025]") { + const std::vector data{0xAB, 0xCD}; + const auto frame = encode_reconfig_request(0x03, 0x0006, data, 7); + REQUIRE_FALSE(frame.empty()); + + rcp::acf::AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), hdr, payload)); + REQUIRE(hdr.byte_bus_id == 0x03); + REQUIRE(hdr.op); + REQUIRE(hdr.evt_op == 0x7); + REQUIRE_FALSE(hdr.evt_ack); + REQUIRE(hdr.transaction_num == 7); + REQUIRE(payload == std::vector{0x00, 0x06, 0xAB, 0xCD}); +} + +TEST_CASE("encode_reconfig_request rejects empty data", "[i2c][REQ-I2C-025]") { + REQUIRE(encode_reconfig_request(0x00, 0, {}, 0).empty()); +} + +TEST_CASE("reconfig error category reports a distinct, non-empty message per code", "[i2c][REQ-I2C-026]") { + auto short_ec = make_error_code(I2cReconfigErrc::short_payload); + auto range_ec = make_error_code(I2cReconfigErrc::out_of_range); + REQUIRE_FALSE(short_ec.message().empty()); + REQUIRE_FALSE(range_ec.message().empty()); + REQUIRE(short_ec.message() != range_ec.message()); +} + +// ── I2cErrc category sanity ─────────────────────────────────────────────────── + +TEST_CASE("I2cErrc reports a non-empty, distinct message per code", "[i2c][REQ-I2C-009]") { + const I2cErrc codes[] = {I2cErrc::short_frame, I2cErrc::bad_msg_type, I2cErrc::wrong_bus, + I2cErrc::wrong_op, I2cErrc::bad_evt, I2cErrc::nack, + I2cErrc::config_write_not_supported}; + std::vector seen; + for (auto c : codes) { + auto ec = make_error_code(c); + REQUIRE(ec.category() == i2c_category()); + REQUIRE_FALSE(ec.message().empty()); + for (const auto& s : seen) REQUIRE(s != ec.message()); + seen.push_back(ec.message()); + } + REQUIRE_FALSE(make_error_code(static_cast(999)).message().empty()); +} + +// ── Transfer direction ──────────────────────────────────────────────────────── + +TEST_CASE("encode_transfer_request read direction carries op=read and read_size", "[i2c][REQ-I2C-010]") { + const std::vector tx{0xA3}; // 7-bit address with the payload's own R/W bit set + const auto frame = encode_transfer_request(6, I2cDir::Read, tx, 10, 7); + REQUIRE_FALSE(frame.empty()); + + rcp::acf::AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), hdr, payload)); + REQUIRE_FALSE(hdr.op); // op=false means "read" in this codec's convention + REQUIRE(hdr.read_size_or_segment_num == 10); + REQUIRE(payload[0] == 0xA3); +} + +TEST_CASE("encode_transfer_request write direction carries op=write and no read_size", + "[i2c][REQ-I2C-010]") { + const std::vector tx{0xA2, 0x10, 0x20}; + const auto frame = encode_transfer_request(6, I2cDir::Write, tx, 0, 7); + REQUIRE_FALSE(frame.empty()); + + rcp::acf::AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), hdr, payload)); + REQUIRE(hdr.op); + REQUIRE(hdr.read_size_or_segment_num == 0); + REQUIRE(payload[0] == 0xA2); +} + +TEST_CASE("i2c_dir_valid accepts Write/Read and rejects everything else", "[i2c][REQ-I2C-017]") { + REQUIRE(i2c_dir_valid(I2cDir::Write)); + REQUIRE(i2c_dir_valid(I2cDir::Read)); + REQUIRE_FALSE(i2c_dir_valid(static_cast(2))); + REQUIRE_FALSE(i2c_dir_valid(static_cast(255))); +} + +TEST_CASE("encode_transfer_request rejects invalid direction/read_size combinations", + "[i2c][REQ-I2C-018]") { + const std::vector tx{0xA2}; + + REQUIRE(encode_transfer_request(6, static_cast(2), tx, 0, 0).empty()); + + REQUIRE(encode_transfer_request(6, I2cDir::Read, tx, 0x1000, 0).empty()); + REQUIRE_FALSE(encode_transfer_request(6, I2cDir::Read, tx, kMaxReadSize, 0).empty()); + + // A write request's header slot is a segment_num, not a read_size. + REQUIRE(encode_transfer_request(6, I2cDir::Write, tx, 4, 0).empty()); +} + +// ── Transfer request round trip ─────────────────────────────────────────────── + +TEST_CASE("decode_transfer_request round-trips address bytes unmodified", "[i2c][REQ-I2C-023]") { + const std::vector tx{0xA2, 0x10, 0x20, 0x30}; + const auto frame = encode_transfer_request(6, I2cDir::Write, tx, 0, 7); + REQUIRE_FALSE(frame.empty()); + + I2cDir dir = I2cDir::Read; + std::vector out_tx; + uint16_t read_size = 99; + uint8_t txn = 0; + REQUIRE_FALSE(decode_transfer_request(frame.data(), frame.size(), 6, dir, out_tx, read_size, txn)); + REQUIRE(dir == I2cDir::Write); + REQUIRE(out_tx == tx); + REQUIRE(read_size == 0); + REQUIRE(txn == 7); +} + +TEST_CASE("decode_transfer_request round-trips the read direction", "[i2c][REQ-I2C-011]") { + const std::vector tx{0xF2, 0xA3}; + const auto frame = encode_transfer_request(6, I2cDir::Read, tx, 5, 8); + + I2cDir dir = I2cDir::Write; + std::vector out_tx; + uint16_t read_size = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_transfer_request(frame.data(), frame.size(), 6, dir, out_tx, read_size, txn)); + REQUIRE(dir == I2cDir::Read); + REQUIRE(out_tx == tx); + REQUIRE(read_size == 5); + REQUIRE(txn == 8); +} + +TEST_CASE("decode_transfer_request round-trips an empty payload", "[i2c][REQ-I2C-011]") { + const auto frame = encode_transfer_request(1, I2cDir::Write, {}, 0, 1); + I2cDir dir; + std::vector out_tx{1}; // start non-empty to prove it gets cleared + uint16_t read_size; + uint8_t txn; + REQUIRE_FALSE(decode_transfer_request(frame.data(), frame.size(), 1, dir, out_tx, read_size, txn)); + REQUIRE(out_tx.empty()); +} + +TEST_CASE("decode_transfer_request rejects the wrong bus", "[i2c][REQ-I2C-012]") { + const std::vector tx{0xAB}; + const auto frame = encode_transfer_request(4, I2cDir::Write, tx, 0, 0); + I2cDir dir; + std::vector out_tx; + uint16_t read_size; + uint8_t txn; + REQUIRE(decode_transfer_request(frame.data(), frame.size(), 5, dir, out_tx, read_size, txn) == + make_error_code(I2cErrc::wrong_bus)); +} + +TEST_CASE("decode_transfer_request rejects a reserved evt[2:0] value", "[i2c][REQ-I2C-012]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 4; + hdr.op = true; + hdr.evt_op = 0x3; + const auto frame = rcp::acf::encode_acf_abb(hdr, {}); + + I2cDir dir; + std::vector out_tx; + uint16_t read_size; + uint8_t txn; + REQUIRE(decode_transfer_request(frame.data(), frame.size(), 4, dir, out_tx, read_size, txn) == + make_error_code(I2cErrc::bad_evt)); +} + +TEST_CASE("decode_transfer_request accepts a hand-built read-direction frame", "[i2c][REQ-I2C-023]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 4; + hdr.op = false; // read + hdr.read_size_or_segment_num = 12; + const auto frame = rcp::acf::encode_acf_abb(hdr, {}); + + I2cDir dir = I2cDir::Write; + std::vector out_tx; + uint16_t read_size = 0; + uint8_t txn; + REQUIRE_FALSE(decode_transfer_request(frame.data(), frame.size(), 4, dir, out_tx, read_size, txn)); + REQUIRE(dir == I2cDir::Read); + REQUIRE(read_size == 12); +} + +TEST_CASE("decode_transfer_request rejects a non-ABB frame", "[i2c][REQ-I2C-012]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 4; + hdr.op = true; + const auto frame = rcp::acf::encode_acf_gbb(hdr, 0, {}); + + I2cDir dir; + std::vector out_tx; + uint16_t read_size; + uint8_t txn; + REQUIRE(decode_transfer_request(frame.data(), frame.size(), 4, dir, out_tx, read_size, txn) == + make_error_code(I2cErrc::bad_msg_type)); +} + +TEST_CASE("decode_transfer_request rejects a short frame", "[i2c][REQ-I2C-012]") { + // byte0's top 7 bits must decode as ACF_ABB (0x0E) so short length, not + // an unrecognized message type, is what gets diagnosed. + const uint8_t too_short[3] = {static_cast(rcp::acf::kAcfMsgTypeAbb << 1), 0, 0}; + I2cDir dir; + std::vector out_tx; + uint16_t read_size; + uint8_t txn; + REQUIRE(decode_transfer_request(too_short, sizeof(too_short), 4, dir, out_tx, read_size, txn) == + make_error_code(I2cErrc::short_frame)); +} + +// ── Response round trip ─────────────────────────────────────────────────────── + +TEST_CASE("encode_response/decode_response round-trip untimed", "[i2c][REQ-I2C-014]") { + const std::vector rx{0xDE, 0xAD, 0xBE, 0xEF}; + const auto frame = encode_response(2, I2cDir::Read, rx, 11, false, 0); + REQUIRE_FALSE(frame.empty()); + + I2cDir dir = I2cDir::Write; + std::vector out_rx; + bool timed = true; + uint64_t ts = 1; + uint8_t txn = 0; + REQUIRE_FALSE(decode_response(frame.data(), frame.size(), 2, dir, out_rx, timed, ts, txn)); + REQUIRE(dir == I2cDir::Read); + REQUIRE(out_rx == rx); + REQUIRE_FALSE(timed); + REQUIRE(ts == 0); + REQUIRE(txn == 11); +} + +TEST_CASE("encode_response/decode_response round-trip timed", "[i2c][REQ-I2C-015]") { + const std::vector rx{0x11, 0x22}; + const auto frame = encode_response(2, I2cDir::Read, rx, 200, true, 0x0102030405060708ull); + + I2cDir dir = I2cDir::Write; + std::vector out_rx; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_response(frame.data(), frame.size(), 2, dir, out_rx, timed, ts, txn)); + REQUIRE(dir == I2cDir::Read); + REQUIRE(out_rx == rx); + REQUIRE(timed); + REQUIRE(ts == 0x0102030405060708ull); + REQUIRE(txn == 200); +} + +TEST_CASE("a write response carries op=write and no payload", "[i2c][REQ-I2C-013]") { + const auto frame = encode_response(2, I2cDir::Write, {}, 11, false, 0); + REQUIRE_FALSE(frame.empty()); + + rcp::acf::AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), hdr, payload)); + REQUIRE(hdr.op); + REQUIRE(payload.empty()); + + I2cDir dir = I2cDir::Read; + std::vector out_rx; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE_FALSE(decode_response(frame.data(), frame.size(), 2, dir, out_rx, timed, ts, txn)); + REQUIRE(dir == I2cDir::Write); + REQUIRE(out_rx.empty()); +} + +TEST_CASE("encode_response rejects invalid direction or a write-direction payload", + "[i2c][REQ-I2C-024]") { + REQUIRE(encode_response(2, I2cDir::Write, {0xFF}, 11, false, 0).empty()); + REQUIRE(encode_response(2, static_cast(2), {}, 11, false, 0).empty()); +} + +TEST_CASE("decode_response rejects the wrong bus", "[i2c][REQ-I2C-016]") { + const auto frame = encode_response(2, I2cDir::Read, {}, 0, false, 0); + I2cDir dir; + std::vector out_rx; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE(decode_response(frame.data(), frame.size(), 3, dir, out_rx, timed, ts, txn) == + make_error_code(I2cErrc::wrong_bus)); +} + +TEST_CASE("decode_response rejects a short frame", "[i2c][REQ-I2C-016]") { + const uint8_t too_short[2] = {rcp::acf::kAcfMsgTypeAbb << 1, 0}; + I2cDir dir; + std::vector out_rx; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE(decode_response(too_short, sizeof(too_short), 2, dir, out_rx, timed, ts, txn) == + make_error_code(I2cErrc::short_frame)); +} + +// ── I2cEndpoint (controller-only raw byte-stream transfer, Table 33 Row 2) ─── + +TEST_CASE("I2cEndpoint::transfer records the raw sent/received byte streams", "[i2c][REQ-I2C-003]") { I2cEndpoint ep; - // First byte models the address+R/W bit, per this milestone's "raw byte - // stream including address bytes" scope. auto ec = ep.transfer({0xA0, 0x10}, {0xFF}); REQUIRE_FALSE(ec); REQUIRE(ep.last_sent() == std::vector{0xA0, 0x10}); @@ -93,8 +551,6 @@ TEST_CASE("I2cEndpoint::transfer fires only TransferComplete on a normal acked t REQUIRE(drained[0] == i2c_signal_id(I2cSignal::TransferComplete)); } -// ── Table 33 Row 2 evt[2:0] validation (handle_request) ───────────────────── - TEST_CASE("I2cEndpoint::handle_request delegates a Plain (evt[2:0]==000b) request to transfer()", "[i2c][REQ-I2C-006]") { I2cEndpoint ep; @@ -104,8 +560,6 @@ TEST_CASE("I2cEndpoint::handle_request delegates a Plain (evt[2:0]==000b) reques REQUIRE_FALSE(ec); REQUIRE(ep.last_sent() == std::vector{0xA0, 0x10}); REQUIRE(ep.last_received() == std::vector{0xFF}); - REQUIRE(ep.triggers().drain() == std::vector{ - i2c_signal_id(I2cSignal::TransferComplete)}); } TEST_CASE("I2cEndpoint::handle_request rejects every reserved evt[2:0] value (001b-110b)", @@ -114,15 +568,13 @@ TEST_CASE("I2cEndpoint::handle_request rejects every reserved evt[2:0] value (00 I2cEndpoint ep; auto ec = ep.handle_request(evt_op, {0xA0}, {0xFF}); REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); - // A rejected reserved evt must not record anything as sent/received. REQUIRE(ep.last_sent().empty()); REQUIRE(ep.last_received().empty()); } } -TEST_CASE("I2cEndpoint::handle_request reports config_write_not_supported for evt[2:0]==111b " - "without crashing or touching transfer state", - "[i2c][REQ-I2C-007]") { +TEST_CASE("I2cEndpoint::handle_request reports config_write_not_supported for evt[2:0]==111b", + "[i2c][REQ-I2C-012]") { I2cEndpoint ep; auto ec = ep.handle_request(/*evt_op=*/7, {0x00, 0xAB}, {}); REQUIRE(ec == make_error_code(I2cErrc::config_write_not_supported)); @@ -137,18 +589,3 @@ TEST_CASE("I2cEndpoint::handle_request masks evt_op down to 3 bits before classi auto ec = ep.handle_request(/*evt_op=*/0xF9, {0xA0}, {0xFF}); // low 3 bits 001 -> Reserved REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); } - -// ── I2cErrc category sanity ─────────────────────────────────────────────────── - -TEST_CASE("I2cErrc reports a non-empty message in its own category", "[i2c][REQ-I2C-005]") { - auto ec = make_error_code(I2cErrc::nack); - REQUIRE(ec.category() == i2c_category()); - REQUIRE_FALSE(ec.message().empty()); -} - -TEST_CASE("I2cErrc::config_write_not_supported reports a non-empty message in its own category", - "[i2c][REQ-I2C-007]") { - auto ec = make_error_code(I2cErrc::config_write_not_supported); - REQUIRE(ec.category() == i2c_category()); - REQUIRE_FALSE(ec.message().empty()); -} diff --git a/tests/test_iseled.cpp b/tests/test_iseled.cpp index 8d0004b..52b7484 100644 --- a/tests/test_iseled.cpp +++ b/tests/test_iseled.cpp @@ -5,14 +5,52 @@ // fusa:test REQ-ISELED-005 // fusa:test REQ-ISELED-006 // fusa:test REQ-ISELED-007 - -// Tests for rcp/iseled.hpp — the ISELED endpoint type (ROADMAP.md milestone -// 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN XL), ISELED, MDIO, -// Wakeup Control", v2.7.0). +// fusa:test REQ-ISELED-008 +// fusa:test REQ-ISELED-009 +// fusa:test REQ-ISELED-010 +// fusa:test REQ-ISELED-011 +// fusa:test REQ-ISELED-012 +// fusa:test REQ-ISELED-013 +// fusa:test REQ-ISELED-014 +// fusa:test REQ-ISELED-015 +// fusa:test REQ-ISELED-016 +// fusa:test REQ-ISELED-017 +// fusa:test REQ-ISELED-018 +// fusa:test REQ-ISELED-019 +// fusa:test REQ-ISELED-020 +// fusa:test REQ-ISELED-021 +// fusa:test REQ-ISELED-022 +// fusa:test REQ-ISELED-023 +// fusa:test REQ-ISELED-024 +// fusa:test REQ-ISELED-025 +// fusa:test REQ-ISELED-026 +// fusa:test REQ-ISELED-027 +// fusa:test REQ-ISELED-029 +// fusa:test REQ-ISELED-030 +// fusa:test REQ-ISELED-031 +// fusa:test REQ-ISELED-032 +// fusa:test REQ-ISELED-033 +// fusa:test REQ-ISELED-034 +// fusa:test REQ-ISELED-035 +// fusa:test REQ-ISELED-036 +// fusa:test REQ-ISELED-037 +// fusa:test REQ-ISELED-038 +// fusa:test REQ-ISELED-039 +// fusa:test REQ-ISELED-040 +// fusa:test REQ-ISELED-041 +// fusa:test REQ-ISELED-042 + +// Tests for rcp/iseled.hpp — the ISELED endpoint type (ep_type 0x0C), ported +// from c-RCP's tests/test_ep_iseled.c (this project's RC5-spec-conformant +// reference) as part of Phase 3 of the ground-up rewrite (cpp-RCP issue +// #129, ROADMAP.md "Phase 17"). #include +#include #include +#include #include +#include using namespace rcp::iseled; @@ -22,216 +60,793 @@ TEST_CASE("ISELED's ep_type id is 0x0C", "[iseled][REQ-ISELED-001]") { REQUIRE(rcp::endpoint::kEndpointTypeIseled == 0x0C); } -// ── Instruction/Address/Data request, Address/Data response shape ─────────── +// ── symbol_encode / symbol_decode ───────────────────────────────────────────── -TEST_CASE("IseledEndpoint::transact records the exact request and response fields", - "[iseled][REQ-ISELED-002]") { - IseledEndpoint ep; - IseledRequest req; - req.instruction = 0x03; - req.address = 0x0102; - req.data = {0xDE, 0xAD}; +TEST_CASE("symbol_encode/symbol_decode round-trip every nibble", "[iseled][REQ-ISELED-001]") { + for (uint8_t nibble = 0; nibble <= 0x0F; ++nibble) { + uint8_t symbol = symbol_encode(nibble); + REQUIRE(symbol <= 0x1F); + uint8_t decoded = 0xFF; + REQUIRE(symbol_decode(symbol, decoded)); + REQUIRE(decoded == nibble); + } +} - IseledResponse resp; - resp.address = 0x0102; - resp.data = 0x0BEF; +TEST_CASE("symbol_encode masks high bits of its input", "[iseled][REQ-ISELED-001]") { + REQUIRE(symbol_encode(0x05) == symbol_encode(0xF5)); +} + +TEST_CASE("symbol_encode gives 0x0 and 0xF distinct parity", "[iseled][REQ-ISELED-001]") { + REQUIRE(symbol_encode(0x0) != symbol_encode(0xF)); +} + +TEST_CASE("symbol_decode rejects a corrupted parity bit", "[iseled][REQ-ISELED-002]") { + uint8_t valid = symbol_encode(0x03); + uint8_t corrupted = static_cast(valid ^ 0x10); + uint8_t decoded = 0xFF; + REQUIRE_FALSE(symbol_decode(corrupted, decoded)); +} - REQUIRE_FALSE(ep.transact(req, resp)); - REQUIRE(ep.last_request().instruction == 0x03); - REQUIRE(ep.last_request().address == 0x0102); - REQUIRE(ep.last_request().data == std::vector{0xDE, 0xAD}); - REQUIRE(ep.last_response().address == 0x0102); - REQUIRE(ep.last_response().data == 0x0BEF); +TEST_CASE("symbol_decode accepts valid parity and sets the nibble", "[iseled][REQ-ISELED-032]") { + uint8_t symbol = symbol_encode(0x0A); + uint8_t decoded = 0xFF; + REQUIRE(symbol_decode(symbol, decoded)); + REQUIRE(decoded == 0x0A); } -// ── 12-bit address / 12-bit data field-width fix ───────────────────────────── -// Verified against the spec's "iseled request format" (Figure 40) and -// "iseled response format" (Figure 41): the address field is 12 bits wide -// in both request and response, and a response's data is a single 12-bit -// value, not an open byte vector. +// ── bitframe_encoded_len ────────────────────────────────────────────────────── -TEST_CASE("kIseledFieldMask is 12 bits and kIseledInstructionMask is 4 bits", - "[iseled][REQ-ISELED-003]") { - REQUIRE(kIseledFieldMask == 0x0FFF); - REQUIRE(kIseledInstructionMask == 0x0F); +TEST_CASE("bitframe_encoded_len computes 2*(data_len + crc)", "[iseled][REQ-ISELED-003]") { + REQUIRE(bitframe_encoded_len(0, false) == 0); + REQUIRE(bitframe_encoded_len(0, true) == 2); + REQUIRE(bitframe_encoded_len(3, false) == 6); + REQUIRE(bitframe_encoded_len(3, true) == 8); } -TEST_CASE("validate_request accepts an in-range instruction and address", - "[iseled][REQ-ISELED-003]") { - IseledRequest req; - req.instruction = kIseledInstructionMask; - req.address = kIseledFieldMask; - REQUIRE_FALSE(validate_request(req)); +// ── encode_bitframe / decode_bitframe round trips ───────────────────────────── + +TEST_CASE("encode_bitframe/decode_bitframe round-trip without CRC", "[iseled][REQ-ISELED-004]") { + const std::vector data{0x12, 0xAB, 0x00}; + auto framed = encode_bitframe(data, false); + REQUIRE(framed.size() == 6); + for (auto b : framed) REQUIRE(b <= 0x1F); + + std::vector decoded; + REQUIRE_FALSE(decode_bitframe(framed.data(), framed.size(), false, decoded)); + REQUIRE(decoded == data); } -TEST_CASE("validate_request rejects an instruction wider than 4 bits", "[iseled][REQ-ISELED-003]") { - IseledRequest req; - req.instruction = static_cast(kIseledInstructionMask + 1); - REQUIRE(validate_request(req) == make_error_code(IseledErrc::field_out_of_range)); +TEST_CASE("encode_bitframe/decode_bitframe round-trip with CRC", "[iseled][REQ-ISELED-005]") { + const std::vector data{0xDE, 0xAD, 0xBE, 0xEF}; + auto framed = encode_bitframe(data, true); + REQUIRE(framed.size() == 10); // (4 + 1 trailer) * 2 + + std::vector decoded; + REQUIRE_FALSE(decode_bitframe(framed.data(), framed.size(), true, decoded)); + REQUIRE(decoded == data); } -TEST_CASE("validate_request rejects an address wider than 12 bits", "[iseled][REQ-ISELED-003]") { - IseledRequest req; - req.address = static_cast(kIseledFieldMask + 1); - REQUIRE(validate_request(req) == make_error_code(IseledErrc::field_out_of_range)); +TEST_CASE("encode_bitframe/decode_bitframe handle an empty, no-CRC buffer", "[iseled][REQ-ISELED-020]") { + auto framed = encode_bitframe({}, false); + REQUIRE(framed.empty()); + + std::vector decoded{1, 2, 3}; // start non-empty + REQUIRE_FALSE(decode_bitframe(nullptr, 0, false, decoded)); + REQUIRE(decoded.empty()); } -TEST_CASE("validate_response accepts an in-range address and data", "[iseled][REQ-ISELED-003]") { - IseledResponse resp; - resp.address = kIseledFieldMask; - resp.data = kIseledFieldMask; - REQUIRE_FALSE(validate_response(resp)); +TEST_CASE("decode_bitframe rejects an odd symbol count", "[iseled][REQ-ISELED-016]") { + const uint8_t symbols[3] = {0, 0, 0}; + std::vector decoded; + REQUIRE(decode_bitframe(symbols, sizeof(symbols), false, decoded) == + make_error_code(IseledErrc::odd_symbol_count)); + REQUIRE(decoded.empty()); } -TEST_CASE("validate_response rejects an address wider than 12 bits", "[iseled][REQ-ISELED-003]") { - IseledResponse resp; - resp.address = static_cast(kIseledFieldMask + 1); - REQUIRE(validate_response(resp) == make_error_code(IseledErrc::field_out_of_range)); +TEST_CASE("decode_bitframe rejects a bad hi-nibble symbol", "[iseled][REQ-ISELED-017]") { + const std::vector data{0x42}; + auto framed = encode_bitframe(data, false); + framed[0] = static_cast(framed[0] ^ 0x10); // flip hi symbol's parity bit + + std::vector decoded; + REQUIRE(decode_bitframe(framed.data(), framed.size(), false, decoded) == + make_error_code(IseledErrc::bad_symbol)); + REQUIRE(decoded.empty()); } -TEST_CASE("validate_response rejects a data value wider than 12 bits", "[iseled][REQ-ISELED-003]") { - IseledResponse resp; - resp.data = static_cast(kIseledFieldMask + 1); - REQUIRE(validate_response(resp) == make_error_code(IseledErrc::field_out_of_range)); +// MC/DC: the hi-symbol arm above always short-circuits before the lo-symbol +// operand is evaluated — isolate it with an otherwise-valid hi symbol. +TEST_CASE("decode_bitframe rejects a bad lo-nibble symbol with a valid hi symbol", + "[iseled][REQ-ISELED-017]") { + const std::vector data{0x42}; + auto framed = encode_bitframe(data, false); + framed[1] = static_cast(framed[1] ^ 0x10); // flip lo symbol's parity bit only + + std::vector decoded; + REQUIRE(decode_bitframe(framed.data(), framed.size(), false, decoded) == + make_error_code(IseledErrc::bad_symbol)); + REQUIRE(decoded.empty()); } -TEST_CASE("IseledEndpoint::transact rejects an out-of-range field without recording it", - "[iseled][REQ-ISELED-003]") { - IseledEndpoint ep; - IseledRequest req; - req.address = static_cast(kIseledFieldMask + 1); +TEST_CASE("decode_bitframe rejects a CRC mismatch", "[iseled][REQ-ISELED-019]") { + const std::vector data{0x11, 0x22}; + auto framed = encode_bitframe(data, true); + framed[1] = symbol_encode(0x0F); // corrupt the first content octet's lo nibble - IseledResponse resp; - resp.address = 0x0001; - resp.data = 0x0001; + std::vector decoded; + REQUIRE(decode_bitframe(framed.data(), framed.size(), true, decoded) == + make_error_code(IseledErrc::crc_mismatch)); + REQUIRE(decoded.empty()); +} - auto ec = ep.transact(req, resp); - REQUIRE(ec == make_error_code(IseledErrc::field_out_of_range)); - // Nothing recorded, and no trigger fired for a rejected transaction. - REQUIRE_FALSE(ep.triggers().has_pending()); +TEST_CASE("decode_bitframe rejects a too-short frame when a CRC trailer is expected", + "[iseled][REQ-ISELED-018]") { + std::vector decoded; + REQUIRE(decode_bitframe(nullptr, 0, true, decoded) == make_error_code(IseledErrc::short_frame)); + REQUIRE(decoded.empty()); +} + +// ── crc8 ─────────────────────────────────────────────────────────────────────── + +TEST_CASE("crc8 is deterministic", "[iseled][REQ-ISELED-006]") { + const std::vector a{0x01, 0x02, 0x03}; + REQUIRE(crc8(a) == crc8(a)); +} + +TEST_CASE("crc8 differs for different content", "[iseled][REQ-ISELED-034]") { + const std::vector a{0x01, 0x02, 0x03}; + const std::vector b{0x01, 0x02, 0x04}; + REQUIRE(crc8(a) != crc8(b)); +} + +TEST_CASE("crc8 of an empty input is 0x00", "[iseled][REQ-ISELED-033]") { + REQUIRE(crc8(nullptr, 0) == 0x00); +} + +// ── requires_isp_n ───────────────────────────────────────────────────────────── + +TEST_CASE("requires_isp_n is true iff use_rcv_clk is true", "[iseled][REQ-ISELED-007]") { + REQUIRE(requires_isp_n(true)); + REQUIRE_FALSE(requires_isp_n(false)); +} + +// ── Transmission-complete trigger ───────────────────────────────────────────── + +TEST_CASE("trigger_fires(None, ...) is always false", "[iseled][REQ-ISELED-008]") { + REQUIRE_FALSE(trigger_fires(IseledTrigger::None, true)); + REQUIRE_FALSE(trigger_fires(IseledTrigger::None, false)); +} + +TEST_CASE("trigger_fires(TxComplete, ...) passes tx_complete_event through", "[iseled][REQ-ISELED-035]") { + REQUIRE(trigger_fires(IseledTrigger::TxComplete, true)); + REQUIRE_FALSE(trigger_fires(IseledTrigger::TxComplete, false)); +} + +// ── Functional config ───────────────────────────────────────────────────────── + +TEST_CASE("iseled_functional_cfg_init zeroes every field", "[iseled][REQ-ISELED-009]") { + IseledFunctionalCfg cfg; + cfg.ep_enable = true; + cfg.bit_clk_divider = 42; + cfg.use_rcv_clk = cfg.crc_enable = true; + cfg.trigger = static_cast(IseledTrigger::TxComplete); + cfg.base_clk = cfg.ep_status = cfg.nr_leds = cfg.rcv_timeout = 0xBEEF; + cfg.wire_clk_divider = 0xAA; + cfg.collect_resp = true; + + iseled_functional_cfg_init(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); + REQUIRE(cfg.bit_clk_divider == 0); + REQUIRE_FALSE(cfg.use_rcv_clk); + REQUIRE_FALSE(cfg.crc_enable); + REQUIRE(cfg.trigger == static_cast(IseledTrigger::None)); + REQUIRE(cfg.base_clk == 0); + REQUIRE(cfg.ep_status == 0); + REQUIRE(cfg.wire_clk_divider == 0); + REQUIRE_FALSE(cfg.collect_resp); + REQUIRE(cfg.nr_leds == 0); + REQUIRE(cfg.rcv_timeout == 0); +} + +TEST_CASE("iseled_functional_cfg_writable is false in HwUnconfigured", "[iseled][REQ-ISELED-010]") { + rcp::lifecycle::WriterCtx writer; + writer.via_root_client_ep0 = true; + writer.via_owning_stream = true; + REQUIRE_FALSE(iseled_functional_cfg_writable(rcp::lifecycle::ServerState::HwUnconfigured, writer)); +} + +TEST_CASE("iseled_functional_cfg_writable in HwConfigured requires EP0/owning-stream/discovery-stream", + "[iseled][REQ-ISELED-010]") { + rcp::lifecycle::WriterCtx none, via_ep0, via_stream, via_discovery; + via_ep0.via_root_client_ep0 = true; + via_stream.via_owning_stream = true; + via_discovery.via_discovery_stream = true; + + REQUIRE_FALSE(iseled_functional_cfg_writable(rcp::lifecycle::ServerState::HwConfigured, none)); + REQUIRE(iseled_functional_cfg_writable(rcp::lifecycle::ServerState::HwConfigured, via_ep0)); + REQUIRE(iseled_functional_cfg_writable(rcp::lifecycle::ServerState::HwConfigured, via_stream)); + REQUIRE(iseled_functional_cfg_writable(rcp::lifecycle::ServerState::HwConfigured, via_discovery)); +} + +TEST_CASE("iseled_functional_cfg_writable in RcpConfigured requires EP0/owning-stream, not discovery", + "[iseled][REQ-ISELED-010]") { + rcp::lifecycle::WriterCtx none, via_ep0, via_stream; + via_ep0.via_root_client_ep0 = true; + via_stream.via_owning_stream = true; + + REQUIRE_FALSE(iseled_functional_cfg_writable(rcp::lifecycle::ServerState::RcpConfigured, none)); + REQUIRE(iseled_functional_cfg_writable(rcp::lifecycle::ServerState::RcpConfigured, via_ep0)); + REQUIRE(iseled_functional_cfg_writable(rcp::lifecycle::ServerState::RcpConfigured, via_stream)); +} + +TEST_CASE("set_bit_clk_divider rejects an unauthorized writer", "[iseled][REQ-ISELED-011]") { + IseledFunctionalCfg cfg; + iseled_functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; + REQUIRE_FALSE(set_bit_clk_divider(cfg, 42, rcp::lifecycle::ServerState::RcpConfigured, none)); + REQUIRE(cfg.bit_clk_divider == 0); +} + +TEST_CASE("set_bit_clk_divider applies when authorized", "[iseled][REQ-ISELED-036]") { + IseledFunctionalCfg cfg; + iseled_functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + REQUIRE(set_bit_clk_divider(cfg, 42, rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.bit_clk_divider == 42); +} + +TEST_CASE("set_use_rcv_clk rejects an unauthorized writer", "[iseled][REQ-ISELED-012]") { + IseledFunctionalCfg cfg; + iseled_functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; + REQUIRE_FALSE(set_use_rcv_clk(cfg, true, rcp::lifecycle::ServerState::RcpConfigured, none)); + REQUIRE_FALSE(cfg.use_rcv_clk); +} + +TEST_CASE("set_use_rcv_clk applies when authorized", "[iseled][REQ-ISELED-037]") { + IseledFunctionalCfg cfg; + iseled_functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + REQUIRE(set_use_rcv_clk(cfg, true, rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.use_rcv_clk); +} + +TEST_CASE("set_crc_enable rejects an unauthorized writer", "[iseled][REQ-ISELED-013]") { + IseledFunctionalCfg cfg; + iseled_functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; + REQUIRE_FALSE(set_crc_enable(cfg, true, rcp::lifecycle::ServerState::RcpConfigured, none)); + REQUIRE_FALSE(cfg.crc_enable); +} + +TEST_CASE("set_crc_enable applies when authorized", "[iseled][REQ-ISELED-038]") { + IseledFunctionalCfg cfg; + iseled_functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + REQUIRE(set_crc_enable(cfg, true, rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.crc_enable); +} + +TEST_CASE("set_trigger rejects an unauthorized writer", "[iseled][REQ-ISELED-014]") { + IseledFunctionalCfg cfg; + iseled_functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx none; + REQUIRE_FALSE(set_trigger(cfg, IseledTrigger::TxComplete, rcp::lifecycle::ServerState::RcpConfigured, none)); + REQUIRE(cfg.trigger == static_cast(IseledTrigger::None)); +} + +TEST_CASE("set_trigger applies when authorized", "[iseled][REQ-ISELED-039]") { + IseledFunctionalCfg cfg; + iseled_functional_cfg_init(cfg); + rcp::lifecycle::WriterCtx writer; + writer.via_owning_stream = true; + REQUIRE(set_trigger(cfg, IseledTrigger::TxComplete, rcp::lifecycle::ServerState::HwConfigured, writer)); + REQUIRE(cfg.trigger == static_cast(IseledTrigger::TxComplete)); +} + +// ── The EP_func register block ────────────────────────────────────────────── + +TEST_CASE("render_registers matches the corrected Table 58 offsets", "[iseled][REQ-ISELED-029]") { + IseledFunctionalCfg cfg; + iseled_functional_cfg_init(cfg); + cfg.ep_enable = true; + cfg.ep_status = 0x1234; + cfg.wire_clk_divider = 0x55; + cfg.collect_resp = true; + cfg.use_rcv_clk = true; + cfg.nr_leds = 0xABCD; + cfg.rcv_timeout = 0x9876; + + const auto out = render_registers(cfg); + + REQUIRE(out[kRegEpLen] == static_cast(kEpFuncLen)); + REQUIRE(out[kRegReserved01] == 0); + REQUIRE((out[kRegEpEnableClr] & 0x01) != 0); + REQUIRE(out[kRegBaseClk] == 0); + REQUIRE(out[kRegBaseClk + 1] == 0); + REQUIRE(out[kRegEpStatus] == 0x12); + REQUIRE(out[kRegEpStatus + 1] == 0x34); + REQUIRE(out[kRegClkDivider] == 0x55); + REQUIRE((out[kRegFlags] & kFlagCollectResp) != 0); + REQUIRE((out[kRegFlags] & kFlagUseRcvClk) != 0); + REQUIRE(out[kRegNrLeds] == 0xAB); + REQUIRE(out[kRegNrLeds + 1] == 0xCD); + REQUIRE(out[kRegRcvTimeout] == 0x98); + REQUIRE(out[kRegRcvTimeout + 1] == 0x76); + REQUIRE(kEpFuncLen == 0x000Eu); +} + +TEST_CASE("render_registers never touches crc_enable", "[iseled][REQ-ISELED-029]") { + IseledFunctionalCfg off, on; + iseled_functional_cfg_init(off); + iseled_functional_cfg_init(on); + on.crc_enable = true; + + REQUIRE(render_registers(off) == render_registers(on)); +} + +TEST_CASE("apply_reconfig writes a multi-register span", "[iseled][REQ-ISELED-029]") { + IseledFunctionalCfg cfg; + iseled_functional_cfg_init(cfg); + const uint8_t payload[8] = {0x00, static_cast(kRegEpStatus), + 0xAB, 0xCD, // ep_status + 0x11, // wire_clk_divider + kFlagCollectResp, // flags + 0x22, 0x33}; // nr_leds + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + REQUIRE(cfg.ep_status == 0xABCD); + REQUIRE(cfg.wire_clk_divider == 0x11); + REQUIRE(cfg.collect_resp); + REQUIRE_FALSE(cfg.use_rcv_clk); + REQUIRE(cfg.nr_leds == 0x2233); +} + +TEST_CASE("apply_reconfig ignores read-only registers (EP_LEN/reserved)", "[iseled][REQ-ISELED-029]") { + IseledFunctionalCfg cfg; + iseled_functional_cfg_init(cfg); + const uint8_t payload[6] = {0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF}; + + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + + const auto out = render_registers(cfg); + REQUIRE(out[kRegEpLen] == static_cast(kEpFuncLen)); + REQUIRE(out[kRegReserved01] == 0); + REQUIRE(out[kRegBaseClk] == 0); + REQUIRE(out[kRegBaseClk + 1] == 0); +} + +TEST_CASE("apply_reconfig ignores base_clk's own two octets individually", "[iseled][REQ-ISELED-029]") { + IseledFunctionalCfg cfg; + iseled_functional_cfg_init(cfg); + { + const uint8_t payload[3] = {0x00, static_cast(kRegBaseClk), 0xFF}; + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + REQUIRE(cfg.base_clk == 0); + REQUIRE(cfg.wire_clk_divider == 0); + } + { + const uint8_t payload[3] = {0x00, static_cast(kRegBaseClk + 1), 0xFF}; + REQUIRE_FALSE(apply_reconfig(cfg, payload, sizeof(payload))); + REQUIRE(cfg.base_clk == 0); + REQUIRE(cfg.wire_clk_divider == 0); + } } -// ── ACF byte_msg_payload codec (Figure 40/41; issue cpp-RCP-A4-iseled) ─────── +TEST_CASE("apply_reconfig rejects a write extending past EP_LEN", "[iseled][REQ-ISELED-029]") { + IseledFunctionalCfg cfg; + iseled_functional_cfg_init(cfg); + const uint8_t payload[3] = {0x00, static_cast(kEpFuncLen), 0xFF}; -TEST_CASE("encode_iseled_request / decode_iseled_request round-trip Instruction/Address/Data", - "[iseled][REQ-ISELED-002]") { - IseledRequest req; - req.instruction = 0x0A; // 4 bits - req.address = 0x0ABC; // 12 bits - req.data = {0xDE, 0xAD, 0xBE}; + REQUIRE(apply_reconfig(cfg, payload, sizeof(payload)) == + make_error_code(IseledReconfigErrc::out_of_range)); + REQUIRE(cfg.rcv_timeout == 0); +} - auto buf = encode_iseled_request(req); - REQUIRE(buf.size() == kIseledRequestFixedLen + req.data.size()); +TEST_CASE("apply_reconfig rejects a payload with no data octet", "[iseled][REQ-ISELED-029]") { + IseledFunctionalCfg cfg; + iseled_functional_cfg_init(cfg); + const uint8_t addr_only[2] = {0x00, 0x08}; - IseledRequest out; - REQUIRE_FALSE(decode_iseled_request(buf.data(), buf.size(), out)); - REQUIRE(out.instruction == req.instruction); - REQUIRE(out.address == req.address); - REQUIRE(out.data == req.data); + REQUIRE(apply_reconfig(cfg, addr_only, sizeof(addr_only)) == + make_error_code(IseledReconfigErrc::short_payload)); + REQUIRE(apply_reconfig(cfg, nullptr, 0) == make_error_code(IseledReconfigErrc::short_payload)); } -TEST_CASE("encode_iseled_request hand-computed expected byte sequence", "[iseled][REQ-ISELED-002]") { - IseledRequest req; - req.instruction = 0x3; // 0b0011 - req.address = 0x0102; // 0b0001_0000_0010 - req.data = {0x11, 0x22}; +TEST_CASE("encode_reconfig_request round-trips through acf::decode_acf_abb", "[iseled][REQ-ISELED-029]") { + const std::vector data{0xAB, 0xCD}; + const auto frame = encode_reconfig_request(0x03, 0x0006, data, 7); + REQUIRE_FALSE(frame.empty()); + + rcp::acf::AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), hdr, payload)); + REQUIRE(hdr.byte_bus_id == 0x03); + REQUIRE(hdr.op); + REQUIRE(hdr.evt_op == 0x7); + REQUIRE(hdr.transaction_num == 7); + REQUIRE(payload == std::vector{0x00, 0x06, 0xAB, 0xCD}); +} - // byte0 = (instruction[3:0] << 4) | address[11:8] = (0x3 << 4) | 0x1 = 0x31 - // byte1 = address[7:0] = 0x02 - const std::vector expected{0x31, 0x02, 0x11, 0x22}; - REQUIRE(encode_iseled_request(req) == expected); +TEST_CASE("encode_reconfig_request rejects empty data", "[iseled][REQ-ISELED-029]") { + REQUIRE(encode_reconfig_request(0x00, 0, {}, 0).empty()); } -TEST_CASE("decode_iseled_request rejects a buffer shorter than Instruction+Address", - "[iseled][REQ-ISELED-002]") { - uint8_t short_buf[1] = {0x00}; - IseledRequest out; - auto ec = decode_iseled_request(short_buf, sizeof(short_buf), out); - REQUIRE(ec == rcp::avtp::make_error_code(rcp::avtp::AvtpErrc::short_buffer)); +TEST_CASE("reconfig error category reports a distinct, non-empty message per code", + "[iseled][REQ-ISELED-042]") { + auto short_ec = make_error_code(IseledReconfigErrc::short_payload); + auto range_ec = make_error_code(IseledReconfigErrc::out_of_range); + REQUIRE_FALSE(short_ec.message().empty()); + REQUIRE_FALSE(range_ec.message().empty()); + REQUIRE(short_ec.message() != range_ec.message()); +} + +// ── IseledErrc category sanity ──────────────────────────────────────────────── + +TEST_CASE("IseledErrc reports a non-empty, distinct message per code", "[iseled][REQ-ISELED-015]") { + const IseledErrc codes[] = { + IseledErrc::short_frame, IseledErrc::bad_msg_type, IseledErrc::wrong_bus, + IseledErrc::wrong_op, IseledErrc::bad_symbol, IseledErrc::crc_mismatch, + IseledErrc::odd_symbol_count, IseledErrc::bad_evt, IseledErrc::config_write_not_supported, + }; + std::vector seen; + for (auto c : codes) { + auto ec = make_error_code(c); + REQUIRE(ec.category() == iseled_category()); + REQUIRE_FALSE(ec.message().empty()); + for (const auto& s : seen) REQUIRE(s != ec.message()); + seen.push_back(ec.message()); + } + REQUIRE_FALSE(make_error_code(static_cast(999)).message().empty()); } -TEST_CASE("encode_iseled_response / decode_iseled_response round-trip Address/Data[11:0]", - "[iseled][REQ-ISELED-002]") { - IseledResponse resp; - resp.address = kIseledFieldMask; // max 12-bit value - resp.data = 0x0ABC; +// ── Command request round trip (write direction) ────────────────────────────── - auto buf = encode_iseled_response(resp); - REQUIRE(buf.size() == kIseledResponseLen); +TEST_CASE("encode_command_request/decode_command_request round-trip raw bytes", + "[iseled][REQ-ISELED-021]") { + const std::vector tx{0x01, 0x02, 0x10, 0x20}; + const auto frame = encode_command_request(6, tx, 7); + REQUIRE_FALSE(frame.empty()); - IseledResponse out; - REQUIRE_FALSE(decode_iseled_response(buf.data(), buf.size(), out)); - REQUIRE(out.address == resp.address); - REQUIRE(out.data == resp.data); + std::vector out_tx; + uint8_t txn = 0; + REQUIRE_FALSE(decode_command_request(frame.data(), frame.size(), 6, out_tx, txn)); + REQUIRE(out_tx == tx); + REQUIRE(txn == 7); } -TEST_CASE("encode_iseled_response hand-computed expected byte sequence", "[iseled][REQ-ISELED-002]") { - IseledResponse resp; - resp.address = 0x0102; // 0b0001_0000_0010 - resp.data = 0x0BEF; // 0b1011_1110_1111 +TEST_CASE("encode_command_request/decode_command_request round-trip an empty payload", + "[iseled][REQ-ISELED-022]") { + const auto frame = encode_command_request(1, {}, 1); + std::vector out_tx{1}; + uint8_t txn = 0; + REQUIRE_FALSE(decode_command_request(frame.data(), frame.size(), 1, out_tx, txn)); + REQUIRE(out_tx.empty()); +} - // byte0 = address[11:4] = 0b0001_0000 = 0x10 - // byte1 = (address[3:0] << 4) | data[11:8] = (0x2 << 4) | 0xB = 0x2B - // byte2 = data[7:0] = 0xEF - const std::vector expected{0x10, 0x2B, 0xEF}; - REQUIRE(encode_iseled_response(resp) == expected); +TEST_CASE("decode_command_request rejects the wrong bus", "[iseled][REQ-ISELED-022]") { + const std::vector tx{0xAB}; + const auto frame = encode_command_request(4, tx, 0); + std::vector out_tx; + uint8_t txn; + REQUIRE(decode_command_request(frame.data(), frame.size(), 5, out_tx, txn) == + make_error_code(IseledErrc::wrong_bus)); } -TEST_CASE("decode_iseled_response rejects a buffer not exactly 3 bytes", "[iseled][REQ-ISELED-002]") { - uint8_t short_buf[2] = {0x00, 0x00}; - IseledResponse out; - REQUIRE(decode_iseled_response(short_buf, sizeof(short_buf), out)); +TEST_CASE("decode_command_request rejects op=read", "[iseled][REQ-ISELED-022]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 4; + hdr.op = false; // not a command request + const auto frame = rcp::acf::encode_acf_abb(hdr, {}); + std::vector out_tx; + uint8_t txn; + REQUIRE(decode_command_request(frame.data(), frame.size(), 4, out_tx, txn) == + make_error_code(IseledErrc::wrong_op)); +} - uint8_t long_buf[4] = {0x00, 0x00, 0x00, 0x00}; - REQUIRE(decode_iseled_response(long_buf, sizeof(long_buf), out)); +TEST_CASE("decode_command_request rejects a nonzero evt[2:0]", "[iseled][REQ-ISELED-022]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 4; + hdr.op = true; + hdr.evt_op = 0x2; + const auto frame = rcp::acf::encode_acf_abb(hdr, {}); + std::vector out_tx; + uint8_t txn; + REQUIRE(decode_command_request(frame.data(), frame.size(), 4, out_tx, txn) == + make_error_code(IseledErrc::bad_evt)); } -// ── Trigger signals ─────────────────────────────────────────────────────────── +TEST_CASE("decode_command_request rejects a non-ABB frame", "[iseled][REQ-ISELED-022]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 4; + hdr.op = true; + const auto frame = rcp::acf::encode_acf_gbb(hdr, 0, {}); + std::vector out_tx; + uint8_t txn; + REQUIRE(decode_command_request(frame.data(), frame.size(), 4, out_tx, txn) == + make_error_code(IseledErrc::bad_msg_type)); +} -TEST_CASE("IseledEndpoint::transact fires TransferComplete on a valid transaction", - "[iseled][REQ-ISELED-004]") { +TEST_CASE("decode_command_request rejects a short frame", "[iseled][REQ-ISELED-022]") { + const uint8_t too_short[3] = {static_cast(rcp::acf::kAcfMsgTypeAbb << 1), 0, 0}; + std::vector out_tx; + uint8_t txn; + REQUIRE(decode_command_request(too_short, sizeof(too_short), 4, out_tx, txn) == + make_error_code(IseledErrc::short_frame)); +} + +TEST_CASE("the write-direction command-request codec is unchanged by the read-request addition", + "[iseled][REQ-ISELED-030]") { + const std::vector tx{0x01, 0x02, 0x03}; + const auto frame = encode_command_request(2, tx, 9); + REQUIRE_FALSE(frame.empty()); + + std::vector out_tx; + uint16_t read_size = 0; + uint8_t txn = 0; + REQUIRE(decode_read_request(frame.data(), frame.size(), 2, out_tx, read_size, txn) == + make_error_code(IseledErrc::wrong_op)); + + REQUIRE_FALSE(decode_command_request(frame.data(), frame.size(), 2, out_tx, txn)); + REQUIRE(out_tx == tx); + REQUIRE(txn == 9); +} + +// ── Read request (read direction) ───────────────────────────────────────────── + +TEST_CASE("encode_read_request/decode_read_request round-trip address and read_size", + "[iseled][REQ-ISELED-030]") { + const std::vector tx{0x03, 0x40}; + const auto frame = encode_read_request(6, tx, 12, 7); + REQUIRE_FALSE(frame.empty()); + + std::vector out_tx; + uint16_t read_size = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_read_request(frame.data(), frame.size(), 6, out_tx, read_size, txn)); + REQUIRE(out_tx == tx); + REQUIRE(read_size == 12); + REQUIRE(txn == 7); +} + +TEST_CASE("encode_read_request/decode_read_request round-trip an empty payload", + "[iseled][REQ-ISELED-031]") { + const auto frame = encode_read_request(1, {}, 64, 1); + std::vector out_tx{1}; + uint16_t read_size = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_read_request(frame.data(), frame.size(), 1, out_tx, read_size, txn)); + REQUIRE(out_tx.empty()); + REQUIRE(read_size == 64); +} + +TEST_CASE("read_size == 0 is a legal, well-formed read request", "[iseled][REQ-ISELED-030]") { + const auto frame = encode_read_request(3, {}, 0, 2); + std::vector out_tx; + uint16_t read_size = 0xFFFF; + uint8_t txn; + REQUIRE_FALSE(decode_read_request(frame.data(), frame.size(), 3, out_tx, read_size, txn)); + REQUIRE(read_size == 0); +} + +TEST_CASE("read_size at kMaxReadSize round-trips; one above is rejected at encode", + "[iseled][REQ-ISELED-030]") { + const auto frame = encode_read_request(3, {}, kMaxReadSize, 2); + std::vector out_tx; + uint16_t read_size = 0; + uint8_t txn; + REQUIRE_FALSE(decode_read_request(frame.data(), frame.size(), 3, out_tx, read_size, txn)); + REQUIRE(read_size == kMaxReadSize); + + REQUIRE(encode_read_request(3, {}, static_cast(kMaxReadSize + 1), 2).empty()); +} + +TEST_CASE("decode_read_request rejects the wrong bus", "[iseled][REQ-ISELED-031]") { + const std::vector tx{0xAB}; + const auto frame = encode_read_request(4, tx, 8, 0); + std::vector out_tx; + uint16_t read_size; + uint8_t txn; + REQUIRE(decode_read_request(frame.data(), frame.size(), 5, out_tx, read_size, txn) == + make_error_code(IseledErrc::wrong_bus)); +} + +TEST_CASE("decode_read_request rejects op=write", "[iseled][REQ-ISELED-031]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 4; + hdr.op = true; // not a read request + const auto frame = rcp::acf::encode_acf_abb(hdr, {}); + std::vector out_tx; + uint16_t read_size; + uint8_t txn; + REQUIRE(decode_read_request(frame.data(), frame.size(), 4, out_tx, read_size, txn) == + make_error_code(IseledErrc::wrong_op)); +} + +TEST_CASE("decode_read_request rejects a nonzero evt[2:0]", "[iseled][REQ-ISELED-031]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 4; + hdr.op = false; + hdr.evt_op = 0x3; + const auto frame = rcp::acf::encode_acf_abb(hdr, {}); + std::vector out_tx; + uint16_t read_size; + uint8_t txn; + REQUIRE(decode_read_request(frame.data(), frame.size(), 4, out_tx, read_size, txn) == + make_error_code(IseledErrc::bad_evt)); +} + +TEST_CASE("decode_read_request rejects a non-ABB frame", "[iseled][REQ-ISELED-031]") { + rcp::acf::AcfMessageInfo hdr; + hdr.byte_bus_id = 4; + hdr.op = false; + const auto frame = rcp::acf::encode_acf_gbb(hdr, 0, {}); + std::vector out_tx; + uint16_t read_size; + uint8_t txn; + REQUIRE(decode_read_request(frame.data(), frame.size(), 4, out_tx, read_size, txn) == + make_error_code(IseledErrc::bad_msg_type)); +} + +TEST_CASE("decode_read_request rejects a short frame", "[iseled][REQ-ISELED-031]") { + const uint8_t too_short[3] = {static_cast(rcp::acf::kAcfMsgTypeAbb << 1), 0, 0}; + std::vector out_tx; + uint16_t read_size; + uint8_t txn; + REQUIRE(decode_read_request(too_short, sizeof(too_short), 4, out_tx, read_size, txn) == + make_error_code(IseledErrc::short_frame)); +} + +// ── Response round trip ─────────────────────────────────────────────────────── + +TEST_CASE("encode_response/decode_response round-trip untimed", "[iseled][REQ-ISELED-023]") { + const std::vector rx{0xDE, 0xAD, 0xBE, 0xEF}; + const auto frame = encode_response(2, rx, 11, false, 0); + std::vector out_rx; + bool timed = true; + uint64_t ts = 1; + uint8_t txn = 0; + REQUIRE_FALSE(decode_response(frame.data(), frame.size(), 2, out_rx, timed, ts, txn)); + REQUIRE(out_rx == rx); + REQUIRE_FALSE(timed); + REQUIRE(ts == 0); + REQUIRE(txn == 11); +} + +TEST_CASE("encode_response/decode_response round-trip timed", "[iseled][REQ-ISELED-023]") { + const std::vector rx{0x11, 0x22}; + const auto frame = encode_response(2, rx, 200, true, 0x0102030405060708ull); + std::vector out_rx; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_response(frame.data(), frame.size(), 2, out_rx, timed, ts, txn)); + REQUIRE(out_rx == rx); + REQUIRE(timed); + REQUIRE(ts == 0x0102030405060708ull); + REQUIRE(txn == 200); +} + +TEST_CASE("decode_response rejects the wrong bus", "[iseled][REQ-ISELED-024]") { + const auto frame = encode_response(2, {}, 0, false, 0); + std::vector out_rx; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE(decode_response(frame.data(), frame.size(), 3, out_rx, timed, ts, txn) == + make_error_code(IseledErrc::wrong_bus)); +} + +TEST_CASE("decode_response rejects a short frame", "[iseled][REQ-ISELED-024]") { + const uint8_t too_short[2] = {rcp::acf::kAcfMsgTypeAbb << 1, 0}; + std::vector out_rx; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE(decode_response(too_short, sizeof(too_short), 2, out_rx, timed, ts, txn) == + make_error_code(IseledErrc::short_frame)); +} + +// ── REQ-ISELED-025/040: response fragmentation, bounded by read_size ───────── + +TEST_CASE("response_fragment_count is 1 when capped data fits in one fragment", + "[iseled][REQ-ISELED-040]") { + REQUIRE(response_fragment_count(10, 10, 100) == 1); +} + +TEST_CASE("response_fragment_count respects the read_size ceiling, not available_len", + "[iseled][REQ-ISELED-040]") { + REQUIRE(response_fragment_count(1000, 10, 100) == 1); +} + +TEST_CASE("response_fragment_count splits the capped data across frames", "[iseled][REQ-ISELED-040]") { + REQUIRE(response_fragment_count(1000, 250, 100) == 3); // 100 + 100 + 50 +} + +// End-to-end: encode a response whose available data exceeds both read_size +// and max_fragment_payload, fragment it, and reassemble every fragment via +// rcp/fragment.hpp's own Reassembler plus this module's own unmodified +// decode_response() as the per-fragment decoder. +TEST_CASE("fragment_worst_case_response_round_trip respects read_size", "[iseled][REQ-ISELED-025]") { + std::vector rx(300); + for (size_t i = 0; i < rx.size(); ++i) rx[i] = static_cast(i * 5 + 1); + + const uint16_t read_size = 200; + const size_t max_fragment_payload = 64; + + REQUIRE(response_fragment_count(rx.size(), read_size, max_fragment_payload) == 4); // ceil(200/64) + + auto frames = encode_response_fragmented(9, rx, read_size, 42, false, 0, max_fragment_payload); + REQUIRE(frames.size() == 4); + + rcp::fragment::Reassembler reasm(read_size); + for (size_t i = 0; i < frames.size(); ++i) { + std::vector out_rx; + bool timed; + uint64_t ts; + uint8_t txn; + REQUIRE_FALSE(decode_response(frames[i].data(), frames[i].size(), 9, out_rx, timed, ts, txn)); + REQUIRE(txn == 42); + REQUIRE_FALSE(timed); + + rcp::acf::AcfMessageInfo hdr; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frames[i].data(), frames[i].size(), hdr, payload)); + + auto rc = reasm.feed(hdr.ms, hdr.read_size_or_segment_num, payload.data(), payload.size()); + if (i + 1 < frames.size()) { + REQUIRE(rc == rcp::fragment::ReasmResult::kContinue); + } else { + REQUIRE(rc == rcp::fragment::ReasmResult::kComplete); + } + } + + REQUIRE(reasm.size() == read_size); + REQUIRE(std::vector(reasm.data(), reasm.data() + reasm.size()) == + std::vector(rx.begin(), rx.begin() + read_size)); +} + +// ── IseledEndpoint convenience wrapper ──────────────────────────────────────── + +TEST_CASE("IseledEndpoint::send records the raw sent bytes and fires TransferComplete", + "[iseled][REQ-ISELED-008]") { IseledEndpoint ep; ep.triggers().enable(iseled_signal_id(IseledSignal::TransferComplete)); - IseledRequest req; - IseledResponse resp; - resp.address = 0x0010; - resp.data = 0x0022; - - REQUIRE_FALSE(ep.transact(req, resp)); + ep.send({0x01, 0x02, 0x10, 0x20}); + REQUIRE(ep.last_sent() == std::vector{0x01, 0x02, 0x10, 0x20}); auto drained = ep.triggers().drain(); REQUIRE(drained.size() == 1); REQUIRE(drained[0] == iseled_signal_id(IseledSignal::TransferComplete)); } -// ── IseledErrc category sanity ──────────────────────────────────────────────── +TEST_CASE("IseledEndpoint::receive records the raw received bytes without firing a trigger", + "[iseled][REQ-ISELED-008]") { + IseledEndpoint ep; + ep.triggers().enable(iseled_signal_id(IseledSignal::TransferComplete)); -TEST_CASE("IseledErrc reports a non-empty message in its own category", "[iseled][REQ-ISELED-005]") { - auto ec = make_error_code(IseledErrc::field_out_of_range); - REQUIRE(ec.category() == iseled_category()); - REQUIRE_FALSE(ec.message().empty()); + ep.receive({0xDE, 0xAD}); + REQUIRE(ep.last_received() == std::vector{0xDE, 0xAD}); + REQUIRE_FALSE(ep.triggers().has_pending()); } -// ── Table 33 Row 2 evt[2:0] validation (handle_request) ───────────────────── - -TEST_CASE("IseledEndpoint::handle_request delegates a Plain (evt[2:0]==000b) request to transact()", +TEST_CASE("IseledEndpoint::handle_request delegates a Plain (evt[2:0]==000b) request to send()", "[iseled][REQ-ISELED-006]") { IseledEndpoint ep; - IseledRequest req; - req.instruction = 0x03; - req.address = 0x0102; - req.data = {0xDE, 0xAD}; - - IseledResponse resp; - resp.address = 0x0102; - resp.data = 0x0BEF; + ep.triggers().enable(iseled_signal_id(IseledSignal::TransferComplete)); - auto ec = ep.handle_request(/*evt_op=*/0, req, resp); + auto ec = ep.handle_request(/*evt_op=*/0, {0x01, 0x02, 0x10, 0x20}); REQUIRE_FALSE(ec); - REQUIRE(ep.last_request().instruction == 0x03); - REQUIRE(ep.last_request().address == 0x0102); - REQUIRE(ep.last_response().data == 0x0BEF); + REQUIRE(ep.last_sent() == std::vector{0x01, 0x02, 0x10, 0x20}); } TEST_CASE("IseledEndpoint::handle_request rejects every reserved evt[2:0] value (001b-110b) " @@ -239,78 +854,26 @@ TEST_CASE("IseledEndpoint::handle_request rejects every reserved evt[2:0] value "[iseled][REQ-ISELED-006]") { for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { IseledEndpoint ep; - IseledRequest req; - req.address = 0x0010; - IseledResponse resp; - resp.address = 0x0020; - resp.data = 0x0030; - - auto ec = ep.handle_request(evt_op, req, resp); + auto ec = ep.handle_request(evt_op, {0x01, 0x02}); REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); - // A rejected reserved evt must not record anything — last_request()/ - // last_response() stay at their default-constructed values, and no - // trigger fires. - REQUIRE(ep.last_request().address == 0); - REQUIRE(ep.last_response().address == 0); - REQUIRE(ep.last_response().data == 0); + REQUIRE(ep.last_sent().empty()); REQUIRE_FALSE(ep.triggers().has_pending()); } } -TEST_CASE("IseledEndpoint::handle_request reports config_write_not_supported for evt[2:0]==111b " - "without crashing or recording anything", - "[iseled][REQ-ISELED-007]") { +TEST_CASE("IseledEndpoint::handle_request reports config_write_not_supported for evt[2:0]==111b", + "[iseled][REQ-ISELED-022]") { IseledEndpoint ep; - IseledRequest req; - req.instruction = 0x1; - req.address = 0x0010; - IseledResponse resp; - resp.address = 0x0020; - resp.data = 0x0030; - - auto ec = ep.handle_request(/*evt_op=*/7, req, resp); + auto ec = ep.handle_request(/*evt_op=*/7, {0x00, 0xAB}); REQUIRE(ec == make_error_code(IseledErrc::config_write_not_supported)); - REQUIRE(ep.last_request().address == 0); - REQUIRE(ep.last_response().address == 0); + REQUIRE(ep.last_sent().empty()); REQUIRE_FALSE(ep.triggers().has_pending()); } TEST_CASE("IseledEndpoint::handle_request masks evt_op down to 3 bits before classifying", "[iseled][REQ-ISELED-006]") { IseledEndpoint ep; - IseledRequest req; - IseledResponse resp; - REQUIRE_FALSE(ep.handle_request(/*evt_op=*/0xF8, req, resp)); // low 3 bits 000 -> Plain - auto ec = ep.handle_request(/*evt_op=*/0xF9, req, resp); // low 3 bits 001 -> Reserved + REQUIRE_FALSE(ep.handle_request(/*evt_op=*/0xF8, {0x01})); // low 3 bits 000 -> Plain + auto ec = ep.handle_request(/*evt_op=*/0xF9, {0x01}); // low 3 bits 001 -> Reserved REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); } - -TEST_CASE("IseledEndpoint::handle_request Reserved/ConfigWrite classification is independent of " - "instruction/address/data — evt[2:0] carries no field-value selector", - "[iseled][REQ-ISELED-006]") { - // Guards against confusing Table 33's evt[2:0] classification with the - // request's own instruction/address/data fields, or the response's own - // address/data fields (see handle_request's own header comment): a - // Reserved/ConfigWrite evt is rejected identically no matter what those - // fields carry. - IseledEndpoint ep; - IseledRequest req; - req.instruction = kIseledInstructionMask; - req.address = kIseledFieldMask; - req.data = {0x01, 0x02, 0x03}; - IseledResponse resp; - resp.address = kIseledFieldMask; - resp.data = kIseledFieldMask; - - REQUIRE(ep.handle_request(/*evt_op=*/3, req, resp) == - rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); - REQUIRE(ep.handle_request(/*evt_op=*/7, req, resp) == - make_error_code(IseledErrc::config_write_not_supported)); -} - -TEST_CASE("IseledErrc::config_write_not_supported reports a non-empty message in its own category", - "[iseled][REQ-ISELED-007]") { - auto ec = make_error_code(IseledErrc::config_write_not_supported); - REQUIRE(ec.category() == iseled_category()); - REQUIRE_FALSE(ec.message().empty()); -} diff --git a/tests/test_mock.cpp b/tests/test_mock.cpp index d1fe0f4..2901cea 100644 --- a/tests/test_mock.cpp +++ b/tests/test_mock.cpp @@ -760,42 +760,32 @@ TEST_CASE("UART request is rejected before RCP_CONFIGURED, same operational gati // ── ISELED ──────────────────────────────────────────────────────────────────── // Table 30/33 Row 2 evt[2:0] validation, seventh endpoint type after I2C, ADC, // PWM_IN, LIN, CAN, and UART: IseledEndpoint wired into dispatch() at -// byte_bus_id 9 for the FIRST time (this file had zero ISELED wiring before -// this pass — see mock.hpp's own header comment and dispatch_iseled's own -// comment). Unlike CAN's fire-a-frame-with-no-readback shape, ISELED pairs a -// request with a full Address/Data response, decoded/encoded through -// rcp/iseled.hpp's own pre-existing Figure 40/41 codec — see -// dispatch_iseled's and set_iseled_response's own comments in rcp/mock.hpp -// for why there is a set_iseled_response() hook and why a successful Plain -// request answers ReadResponse with the encoded response payload. - -TEST_CASE("ISELED plain request (evt[2:0]==000b) decodes the payload, transacts against the " - "scripted response, and answers ReadResponse with the encoded response", +// byte_bus_id 9. Phase 3's rcp/iseled.hpp rewrite replaced its earlier +// structured Address/Data ACF-payload model with the same raw-byte-stream +// codec I2C/LIN already use — see rcp/iseled.hpp's own header comment — +// so, like dispatch_i2c/dispatch_lin, dispatch_iseled passes the raw +// byte_msg_payload straight to IseledEndpoint::handle_request rather than +// decoding/encoding it through a struct-based codec. + +TEST_CASE("ISELED plain request (evt[2:0]==000b) records the sent bytes, transacts against the " + "scripted response, and answers ReadResponse with the scripted response bytes", "[mock][REQ-MOCK-023]") { mock::Server server; REQUIRE_FALSE(server.advance_to_rcp_configured()); - iseled::IseledResponse scripted; - scripted.address = 0x0102; - scripted.data = 0x0BEF; + const std::vector scripted{0x01, 0x02, 0xBE, 0xEF}; server.set_iseled_response(scripted); - iseled::IseledRequest request; - request.instruction = 0x3; - request.address = 0x0102; - request.data = {0x11, 0x22}; - auto payload = iseled::encode_iseled_request(request); + const std::vector payload{0x03, 0x01, 0x02, 0x11, 0x22}; auto req = standard_request(mock::kIseledByteBusId, /*write=*/true, /*evt_op=*/0); acf::AcfMessageInfo resp; std::vector resp_payload; REQUIRE_FALSE(server.dispatch(0, req, payload, resp, resp_payload)); REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ReadResponse); - REQUIRE(resp_payload == iseled::encode_iseled_response(scripted)); - REQUIRE(server.iseled().last_request().instruction == 0x3); - REQUIRE(server.iseled().last_request().address == 0x0102); - REQUIRE(server.iseled().last_request().data == std::vector{0x11, 0x22}); - REQUIRE(server.iseled().last_response().data == 0x0BEF); + REQUIRE(resp_payload == scripted); + REQUIRE(server.iseled().last_sent() == payload); + REQUIRE(server.iseled().last_received() == scripted); } TEST_CASE("ISELED request with a reserved evt[2:0] (001b-110b) is rejected with wire error code " @@ -804,9 +794,7 @@ TEST_CASE("ISELED request with a reserved evt[2:0] (001b-110b) is rejected with mock::Server server; REQUIRE_FALSE(server.advance_to_rcp_configured()); - iseled::IseledRequest request; - request.address = 0x0010; - auto payload = iseled::encode_iseled_request(request); + const std::vector payload{0x00, 0x10}; for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { auto req = standard_request(mock::kIseledByteBusId, /*write=*/true, evt_op); @@ -817,8 +805,8 @@ TEST_CASE("ISELED request with a reserved evt[2:0] (001b-110b) is rejected with REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); REQUIRE(resp_payload == std::vector{static_cast(acf::WireErrorCode::UnsupportedCmd)}); - REQUIRE(server.iseled().last_request().address == 0); - REQUIRE(server.iseled().last_response().address == 0); + REQUIRE(server.iseled().last_sent().empty()); + REQUIRE(server.iseled().last_received().empty()); } } @@ -828,9 +816,7 @@ TEST_CASE("ISELED request with evt[2:0]==111b (config-write) is rejected with wi mock::Server server; REQUIRE_FALSE(server.advance_to_rcp_configured()); - iseled::IseledRequest request; - request.address = 0x0010; - auto payload = iseled::encode_iseled_request(request); + const std::vector payload{0x00, 0x10}; auto req = standard_request(mock::kIseledByteBusId, /*write=*/true, /*evt_op=*/7); acf::AcfMessageInfo resp; @@ -840,7 +826,7 @@ TEST_CASE("ISELED request with evt[2:0]==111b (config-write) is rejected with wi REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); REQUIRE(resp_payload == std::vector{static_cast(acf::WireErrorCode::UnsupportedCmd)}); - REQUIRE(server.iseled().last_request().address == 0); + REQUIRE(server.iseled().last_sent().empty()); } TEST_CASE("ISELED request is rejected before RCP_CONFIGURED, same operational gating as " @@ -849,8 +835,7 @@ TEST_CASE("ISELED request is rejected before RCP_CONFIGURED, same operational ga mock::Server server; REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); - iseled::IseledRequest request; - auto payload = iseled::encode_iseled_request(request); + const std::vector payload{}; auto req = standard_request(mock::kIseledByteBusId, /*write=*/true, /*evt_op=*/0); acf::AcfMessageInfo resp; std::vector resp_payload;