From 309f9b888bb6b5ca17843ee6ed5fdddc3a698687 Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Thu, 20 Aug 2026 09:10:36 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20implement=20Table=2030/33=20Row-2=20evt?= =?UTF-8?q?[2:0]=20validation=20=E2=80=94=20MDIO=20(8th=20and=20LAST=20end?= =?UTF-8?q?point=20type)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements TC18 (OPEN Alliance TC18 Remote Control Protocol Specification v0.5.1_RC) §13.5 Table 33's "Row 2" evt[2:0] request classification for the MDIO endpoint type (include/rcp/mdio.hpp), the eighth and LAST of the eight endpoint types sharing this row ({ADC, PWM_IN, I2C, LIN, CAN, UART, ISELED, MDIO}), following the pattern established by I2C (#114), ADC (#115), PWM_IN (#116), LIN (#117), CAN (#118), UART (#119), and ISELED (#120). Classification rule (via the shared rcp::endpoint::evt_row2_kind_of / EndpointErrc::reserved_evt_row2 predicate already in rcp/endpoint.hpp — not reinvented here): - evt[2:0] == 000b -> Plain: delegates to the existing transaction logic - evt[2:0] in 001b-110b -> Reserved: rejected with EndpointErrc::reserved_evt_row2 (wire error code UNSUPPORTED_CMD) - evt[2:0] == 111b -> ConfigWrite (§12.7.1, not yet implemented): reported via new MdioErrc::config_write_not_supported ## handle_request -> transact rename (naming collision) MdioEndpoint already had a pre-existing method literally named `handle_request(MdioRequest, MdioResponse&)` — the read/write register transaction logic, unrelated to evt[2:0] classification, predating this milestone. To free that name for the new evt[2:0]-classifying entry point: 1. Renamed the existing method to `transact(MdioRequest, MdioResponse&)`, matching the name rcp/iseled.hpp's IseledEndpoint::transact already established for the identical "existing single-call transaction method, now wrapped by a classifying handle_request" role. 2. Updated all 8 call sites in tests/test_mdio.cpp from `.handle_request(` to `.transact(`. Grepped the whole repo (including mock.hpp/mdio_.*) to confirm no other call sites existed before this PR (mock.hpp had zero MDIO wiring at all prior to this change). 3. Added a NEW `handle_request(uint8_t evt_op, MdioRequest, MdioResponse&)` that classifies evt_op first: Plain delegates straight to transact() unchanged, Reserved returns EndpointErrc::reserved_evt_row2 without calling transact(), ConfigWrite returns MdioErrc::config_write_not_supported. Confirmed clean: `grep -rn "\.handle_request("` across the repo shows zero remaining 2-argument MdioEndpoint::handle_request call sites; every call site now either invokes transact() directly (register-access tests) or the new 3-argument evt[2:0]-classifying handle_request (classification tests). ## Addressing model — untouched include/rcp/mdio.hpp's own header comment documents that this header previously modeled MDIO addressing as an invented IEEE 802.3 Clause 22/Clause 45-style PHY-address scheme with no basis in TC18, corrected in issue #72 to the real mdio_mode/mdio_address/mdio_payload model. This PR does NOT touch, reinterpret, or extend MdioRequest's/MdioResponse's field shapes or the mdio_mode/register_key logic in any way — Table 33's evt[2:0] classification is a completely separate, orthogonal concern, and no new MDIO wire encoding is invented anywhere in mdio.hpp itself. ## mock.hpp wiring (10th mock endpoint, byte_bus_id=10) Wires MdioEndpoint into rcp/mock.hpp for the FIRST time (zero prior MDIO wiring, verified via `grep -n "mdio\|Mdio\|MDIO" include/rcp/mock.hpp` on main returning nothing), following UART's (#119) and ISELED's (#120) wiring shape: - byte_bus_id/endpoint id 10, after ISELED's 9 - REQ-MOCK-001's endpoint count updated eight -> nine -> ten (nine was already current on main from the ISELED PR) - new dispatch_mdio(), branching on req.op like dispatch_uart (since MdioRequest::is_write is intrinsic to the request itself, unlike I2C/LIN's op-agnostic full-duplex/raw-transfer dispatch) - Unlike ISELED, NO MDIO byte-level wire codec (Figure 43/Table 60) exists anywhere in this codebase — mdio.hpp deliberately stops at the (mode, mdio_address, mdio_payload) struct level. dispatch_mdio makes the same kind of deliberately simple, explicitly disclaimed choice dispatch_can makes for CAN's fixed CanIdentifier/FrameFormat: a fixed representative MdioMode::MmdSingleWord/mdio_address=0, with mdio_payload packed/unpacked via avtp::detail::put_u16/get_u16 (the same generic helper dispatch_ep0 already uses for its own magic-number encoding) purely so the mock has *some* round-trippable wire value — this is the mock's own simulation choice, not a TC18-derived encoding. - No set_mdio_response() hook: MDIO's register model is self-contained (a write round-trips straight back out on a subsequent read), unlike I2C/LIN/ISELED's external-bus scripting hooks. ## .fusa-reqs.json Added REQ-MDIO-006/007 (evt[2:0] classification + config-write-not-yet- supported) and REQ-MOCK-025/026 (mock dispatch classification + pre- RCP_CONFIGURED gating), reusing TC18-13.5-001 and TC18-12.7.1-001 — independently re-verified against RELAY's docs/tc18-master-catalog.json (both entries confirm MDIO by name in Table 33's Row 2 list) and against the actual TC18.txt line ranges cited (L4085-4092 and L2844-2865 respectively; both match the source text exactly). Also updated REQ-MDIO-003/004's requirement text (transact rename), since those describe the exact method being renamed. Tagged REQ-MDIO-006 (the reserved-value-rejection requirement) severity: cybersecurity, matching every prior Row-2 endpoint's pattern; sec-tested ratio after this change is 72/410 = 17.56%, above cpfusa's 17% floor. ## Pre-existing citation spot-check (not touched by this diff) Spot-checked MDIO's own PRE-EXISTING TC18.txt citations against the actual document content at /Users/matt/Coding/SoundMatt/TC18.txt: - REQ-MDIO-001 (§13.2 Table 32, "L3998-4017"): confirmed exact match — Table 32's "0x0D MDIO" row is at line 4017, table starts ~3998. - REQ-MDIO-002 (§13.7.13.3 Table 60, "L6080-6088"): confirmed exact match — Table 60's mdio_mode/mdio_address/mdio_payload rows and its own caption line are within 6081-6088. Unlike ISELED's citations in the rust-RCP repo (found ~400 lines stale in PR #161), MDIO's pre-existing citations in cpp-RCP show NO drift against the current TC18.txt — both line ranges verified correct as-is. No corrective action taken (not needed). Separately (not a TC18-citation issue, but noted for completeness): REQ-MDIO-005's requirement text still references `MdioErrc::phy_address_out_of_range`, a symbol that no longer exists in mdio.hpp's enum — a leftover from the pre-issue-#72 Clause 22/Clause 45 addressing model that was apparently never updated when MdioErrc was rebuilt. This requirement's text/enum-name mismatch is NOT part of this PR's diff and is left as-is per the same "flag, don't silently rewrite" policy applied to citation staleness; worth a small follow-up. ## Testing - tests/test_mdio.cpp: renamed 8 call sites to transact(); added 6 new TEST_CASEs covering Plain/Reserved(x6)/ConfigWrite/masking/field- independence classification, plus a config_write_not_supported message sanity check. - tests/test_mock.cpp: REQ-MOCK-001 nine -> ten endpoints; new MDIO section mirroring CAN/UART/ISELED's shape (write-then-read round-trip through dispatch(), Reserved x6, ConfigWrite, pre-RCP_CONFIGURED gating). - Full build (cmake+Ninja) and `ctest --test-dir build`: 53/53 suites pass, 0 failures. - `cpfusa check --strict`: 0 errors (2 warnings/1 info are pre-existing baseline conditions, confirmed identical on main before this change — FUSA004 missing .fusa-evidence.json, HARA005 hazard ASIL mismatch). - `cpfusa trace`: 410/410 requirements 100% annotated, 100% tested; `--sec-tested 17` passes (17.56% cybersecurity-tagged ratio). - Mutation test: temporarily changed the ConfigWrite branch in MdioEndpoint::handle_request to call transact() instead of returning the error; rebuilt; confirmed 2 test_mdio assertions and 1 test_mock assertion failed as expected; reverted cleanly; confirmed all tests pass again and `grep -n "MUTATION-TEST"` finds nothing left behind. Co-Authored-By: Claude Sonnet 5 Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- .fusa-reqs.json | 43 +++++++++++++- include/rcp/mdio.hpp | 115 ++++++++++++++++++++++++++++++++++++- include/rcp/mock.hpp | 131 ++++++++++++++++++++++++++++++++++++------- tests/test_mdio.cpp | 131 +++++++++++++++++++++++++++++++++++++++---- tests/test_mock.cpp | 118 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 495 insertions(+), 43 deletions(-) diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 2cda440..4bb7884 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -286,6 +286,24 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-MOCK-025", + "title": "MDIO dispatch applies Table 33 Row 2 evt[2:0] classification before touching endpoint state, packing/unpacking mdio_payload through this mock's own u16 wire shape", + "text": "rcp::mock::Server::dispatch, for a request addressed to the MDIO endpoint while RcpConfigured, shall build an MdioRequest with a fixed MdioMode::MmdSingleWord mode and mdio_address 0, is_write taken from the request's op bit, and mdio_payload decoded from the request's byte_msg_payload via avtp::detail::get_u16 (0 if fewer than 2 bytes are present), and drive mdio::MdioEndpoint::handle_request with the request's evt[2:0] field: a Plain (000b) request shall transact against the mock's own self-contained MdioEndpoint register map and be answered as a WriteResponse (write) or a ReadResponse whose byte_msg_payload is avtp::detail::put_u16(MdioEndpoint::last_response().mdio_payload) (read), a Reserved (001b-110b) evt shall be rejected with endpoint::EndpointErrc::reserved_evt_row2 (wire error code UNSUPPORTED_CMD) without recording anything, and a ConfigWrite (111b) evt shall be rejected with mdio::MdioErrc::config_write_not_supported (wire error code UNSUPPORTED_CMD) rather than crashing or being treated as a plain transaction.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.5 Table 33 (\"ADC, PWM_IN, I²C, LIN, CAN, UART, ISELED, MDIO: 000b to 110b reserved – request to be rejected with error code = UNSUPPORTED_CMD\" / \"111b: The byte_msg_payload is not presented to the interface but used to change the configuration of the endpoint (see 12.7.1)\"), TC18.txt L4085-4092; corroborating worked example resolving the evt=000b ambiguity in Table 33's own favor: §13.7.9.3 Figure 34 (\"RC Client sends a standard read request\", ADC endpoint, evt field all-zero), TC18.txt L5590-5607", + "tc18_master_id": "TC18-13.5-001" + }, + { + "id": "REQ-MOCK-026", + "title": "Operational MDIO requests are rejected before RCP_CONFIGURED, same gating as GPIO/SPI/I2C/ADC/PWM_IN/LIN/CAN/UART/ISELED", + "text": "rcp::mock::Server::dispatch, for a request addressed to the MDIO endpoint while the server's lifecycle state is not RcpConfigured, shall return regmap::RegMapErrc::request_rejected and an ErrorResponse without touching the endpoint's live state, the same operational-request gating REQ-MOCK-006/REQ-MOCK-012/REQ-MOCK-014/REQ-MOCK-016/REQ-MOCK-018/REQ-MOCK-020/REQ-MOCK-022/REQ-MOCK-024 already apply to GPIO/SPI/I2C/ADC/PWM_IN/LIN/CAN/UART/ISELED.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, { "id": "REQ-UDP-001", "title": "Frame round-trips an NTSCF-framed ACF_ABB request", @@ -3452,7 +3470,7 @@ { "id": "REQ-MDIO-003", "title": "MDIO register write-then-read round-trip", - "text": "rcp::mdio::MdioEndpoint::handle_request shall, for a given (mdio_mode, mdio_address) key, return the value most recently written to that register on a subsequent read, and shall return 0 for a register that has never been written.", + "text": "rcp::mdio::MdioEndpoint::transact shall, for a given (mdio_mode, mdio_address) key, return the value most recently written to that register on a subsequent read, and shall return 0 for a register that has never been written.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3460,7 +3478,7 @@ { "id": "REQ-MDIO-004", "title": "MDIO mdio_mode keeps each mode's register space distinct", - "text": "rcp::mdio::MdioEndpoint::handle_request shall key a register access on (mdio_mode, mdio_address) such that it never observes a value written under a different mdio_mode at the same mdio_address, and shall notify TransferComplete on every request regardless of mdio_mode.", + "text": "rcp::mdio::MdioEndpoint::transact shall key a register access on (mdio_mode, mdio_address) such that it never observes a value written under a different mdio_mode at the same mdio_address, and shall notify TransferComplete on every request regardless of mdio_mode.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3473,6 +3491,27 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-MDIO-006", + "title": "MdioEndpoint::handle_request applies Table 33 Row 2 evt[2:0] classification before transacting", + "text": "rcp::mdio::MdioEndpoint::handle_request shall classify its evt_op argument via rcp::endpoint::evt_row2_kind_of (masking to the low 3 bits first) and, for EvtRow2Kind::Plain, shall delegate to transact(request, out) with both arguments unchanged; for EvtRow2Kind::Reserved, shall return endpoint::EndpointErrc::reserved_evt_row2 without recording anything as the last request (last_request() left untouched, no register written or read) and without firing TransferComplete.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.5 Table 33 (\"ADC, PWM_IN, I²C, LIN, CAN, UART, ISELED, MDIO: 000b to 110b reserved – request to be rejected with error code = UNSUPPORTED_CMD\" / \"111b: The byte_msg_payload is not presented to the interface but used to change the configuration of the endpoint (see 12.7.1)\"), TC18.txt L4085-4092; corroborating worked example resolving the evt=000b ambiguity in Table 33's own favor: §13.7.9.3 Figure 34 (\"RC Client sends a standard read request\", ADC endpoint, evt field all-zero), TC18.txt L5590-5607", + "tc18_master_id": "TC18-13.5-001", + "severity": "cybersecurity" + }, + { + "id": "REQ-MDIO-007", + "title": "MdioEndpoint::handle_request reports config-write (evt[2:0]==111b) as not yet supported", + "text": "rcp::mdio::MdioEndpoint::handle_request, for EvtRow2Kind::ConfigWrite (evt[2:0]==111b), shall return MdioErrc::config_write_not_supported without recording anything as the last request and without crashing, rather than silently accepting the request as an ordinary transaction or silently doing nothing; full §12.7.1 configuration-write handling is deliberately deferred to a follow-up milestone, the same scope decision I2C's I2cErrc::config_write_not_supported, ADC's AdcErrc::config_write_not_supported, PWM_IN's PwmErrc::config_write_not_supported, LIN's LinErrc::config_write_not_supported, CAN's CanErrc::config_write_not_supported, UART's UartErrc::config_write_not_supported, and ISELED's IseledErrc::config_write_not_supported already made. MdioErrc::config_write_not_supported shall report a non-empty diagnostic message in its own error_category.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§12.7.1 (\"This can be done by setting evt[2:0] = 111b in request as described above.\"; Figure 19 – \"Configuration request\"; \"The data of the byte_msg_payload from a write request is written into the section of the EP that is addressed by the byte_bus_id.\"), TC18.txt L2844-2865", + "tc18_master_id": "TC18-12.7.1-001" + }, { "id": "REQ-WAKEUP-001", "title": "Wakeup control's ep_type id is 0x01", diff --git a/include/rcp/mdio.hpp b/include/rcp/mdio.hpp index a22523f..132f6bf 100644 --- a/include/rcp/mdio.hpp +++ b/include/rcp/mdio.hpp @@ -3,6 +3,8 @@ // fusa:req REQ-MDIO-003 // fusa:req REQ-MDIO-004 // fusa:req REQ-MDIO-005 +// fusa:req REQ-MDIO-006 +// fusa:req REQ-MDIO-007 // MDIO endpoint (ep_type 0x0D) — the OPEN Alliance TC18 Remote Control // Protocol Specification v0.5.1_RC's mdio_mode-selected register access @@ -58,12 +60,43 @@ // MdioRequest::mms_is_0_or_1 below is the caller-supplied fact // payload_width_bits needs for the MmsMultiWord case. // +// Table 30/33 Row 2 evt[2:0] validation (post-v2.7.0, EIGHTH and last +// endpoint type in this row, after I2C, ADC, PWM_IN, LIN, CAN, UART, and +// ISELED): MdioEndpoint::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 +// MDIO's request handling, 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, +// rcp/uart.hpp's UartEndpoint::handle_request, and rcp/iseled.hpp's +// IseledEndpoint::handle_request established. +// +// NAMING: this header's own single-transaction method — the read/write +// register access above, predating this milestone entirely — was previously +// itself named `handle_request(MdioRequest, MdioResponse&)`. That name is +// now reused for the NEW evt[2:0]-classifying entry point below, so the old +// method is renamed to `transact`, matching the name rcp/iseled.hpp's +// IseledEndpoint::transact already established for the identical "existing +// single-call transaction method, now wrapped by a classifying +// handle_request" role. Every call site (tests/test_mdio.cpp) is updated to +// call transact() directly where it exercises the register-access behavior +// itself, unrelated to evt[2:0] classification. +// +// This is a completely separate, TC18-Table-33-derived concern from this +// header's own addressing-model fix directly above — it does not touch, +// reinterpret, or extend MdioRequest's/MdioResponse's field shapes, or the +// mdio_mode/mdio_address/mdio_payload/register_key model, in any way. See +// handle_request's own doc comment below for why a Reserved or ConfigWrite +// evt must never reach transact(). +// // 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 register-key // composition chosen in this file is this implementation's own, same as the -// equivalent disclaimers in rcp/avtp.hpp, rcp/regmap.hpp, and -// rcp/endpoint.hpp. +// equivalent disclaimers in rcp/avtp.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, +// rcp/i2c.hpp, rcp/adc.hpp, rcp/pwm.hpp, rcp/lin.hpp, rcp/can.hpp, +// rcp/uart.hpp, and rcp/iseled.hpp. #pragma once #include @@ -72,6 +105,7 @@ #include #include #include +#include namespace rcp { namespace mdio { @@ -112,6 +146,15 @@ struct MdioResponse { enum class MdioErrc : int { payload_exceeds_mode_width = 1, // mdio_payload does not fit payload_width_bits(mode, mms_is_0_or_1) + // 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, UART's, and ISELED's own + // config_write_not_supported variants. + config_write_not_supported = 2, }; inline const std::error_category& mdio_category() noexcept { @@ -121,6 +164,8 @@ inline const std::error_category& mdio_category() noexcept { switch (static_cast(ev)) { case MdioErrc::payload_exceeds_mode_width: return "rcp/mdio: mdio_payload exceeds the width mdio_mode assigns it"; + case MdioErrc::config_write_not_supported: + return "rcp/mdio: evt[2:0]=111b configuration-write requests are not yet implemented"; default: return "rcp/mdio: unknown error"; } @@ -160,7 +205,15 @@ constexpr endpoint::TriggerRegistry::SignalId mdio_signal_id(MdioSignal sig) noe // devices, per the spec's own MMD/MMS split in Table 57). class MdioEndpoint { public: - std::error_code handle_request(MdioRequest req, MdioResponse& out) { + // transact is MDIO's own pre-existing single-call register access — a + // write stores req.mdio_payload under (mode, mdio_address) and echoes it + // back via `out`; a read returns whatever was last stored there, or 0 if + // never written. Renamed from this method's pre-milestone name + // `handle_request` (see this header's own top comment) so that name is + // free for the evt[2:0]-classifying entry point below, matching + // rcp/iseled.hpp's IseledEndpoint::transact naming for the identical + // role. + std::error_code transact(MdioRequest req, MdioResponse& out) { auto ec = validate_request(req); if (ec) return ec; @@ -177,6 +230,62 @@ class MdioEndpoint { return {}; } + // handle_request is MDIO's request-decode entry point — the piece this + // header previously had none of, mirroring rcp::i2c:: + // I2cEndpoint::handle_request's shape (this repo's eighth and LAST + // Table 33 Row 2 endpoint type, after I2C, ADC, PWM_IN, LIN, CAN, UART, + // and ISELED). 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, + // out) with both arguments unchanged — MDIO's existing + // mdio_mode-keyed request/response transaction model (extraction + // §13.7.13.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 + // endpoint::EndpointErrc::reserved_evt_row2 without recording + // anything (last_request_ is left exactly as it was, no register is + // written or read, 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 MDIO EP's own functional-config block, not an + // mdio_mode-selected register access 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, UART's, and + // ISELED's own handle_request comments defer for the identical + // reason); this returns MdioErrc::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 addressing-model fix above + // (issue #72's mdio_mode/mdio_address/mdio_payload rebuild replacing the + // invented Clause 22/Clause 45 scheme): that fix concerns what + // MdioRequest's and MdioResponse'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 mode/address/payload, + // or inventing any encoding of those fields beyond what + // validate_request/register_key already define, would be exactly the + // kind of invented, non-spec-derived encoding this header's own + // addressing-model-fix 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, MdioRequest req, MdioResponse& out) { + switch (endpoint::evt_row2_kind_of(evt_op)) { + case endpoint::EvtRow2Kind::Plain: + return transact(std::move(req), out); + case endpoint::EvtRow2Kind::Reserved: + return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); + case endpoint::EvtRow2Kind::ConfigWrite: + return make_error_code(MdioErrc::config_write_not_supported); + } + return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); // unreachable + } + const MdioRequest& last_request() const noexcept { return last_request_; } endpoint::TriggerRegistry& triggers() noexcept { return triggers_; } diff --git a/include/rcp/mock.hpp b/include/rcp/mock.hpp index c632588..bd636bb 100644 --- a/include/rcp/mock.hpp +++ b/include/rcp/mock.hpp @@ -22,6 +22,8 @@ // fusa:req REQ-MOCK-022 // fusa:req REQ-MOCK-023 // fusa:req REQ-MOCK-024 +// fusa:req REQ-MOCK-025 +// fusa:req REQ-MOCK-026 // In-process RC Server simulator — a small, representative OPEN Alliance // TC18 Remote Control Protocol Specification v0.5.1_RC server built @@ -47,21 +49,25 @@ // mock::Server holds a real rcp::lifecycle::ServerLifecycle (v2.1.0), a // real rcp::regmap::RegisterMap plus rcp::regmap::Ep0 (v2.1.0, including // EP0 whole-map-read and root-client write semantics), and one instance -// each of nine fully-built endpoint types — rcp::gpio::GpioEndpoint and +// each of ten fully-built endpoint types — rcp::gpio::GpioEndpoint and // rcp::spi::SpiEndpoint (both v2.3.0), plus rcp::i2c::I2cEndpoint, // rcp::adc::AdcEndpoint, rcp::pwm::PwmInEndpoint, rcp::lin::LinEndpoint, -// rcp::can::CanEndpoint, rcp::uart::UartEndpoint, and -// rcp::iseled::IseledEndpoint (v2.4.0/post-v2.7.0, wired in by the Table +// rcp::can::CanEndpoint, rcp::uart::UartEndpoint, rcp::iseled::IseledEndpoint, +// and rcp::mdio::MdioEndpoint (v2.4.0/post-v2.7.0, wired in by the Table // 30/33 Row 2 evt[2:0] validation pilot and its ADC, PWM_IN, LIN, CAN, -// UART, and ISELED follow-ups, in that order — UART's was this header's -// FIRST wiring of rcp::uart::UartEndpoint at all, not merely an 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 +// UART, ISELED, and MDIO follow-ups, in that order — UART's was this +// header's FIRST wiring of rcp::uart::UartEndpoint at all, not merely an +// 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) — as its representative endpoint set. -// dispatch() below is the single +// raw bytes through untouched; 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 +// choice this dispatch path makes instead) — as its representative +// endpoint set. dispatch() below is the single // request/response entry point a test drives, decoding the standard // request kind's evt[2:0]/op fields (rcp/acf.hpp, v2.0.0) the same way a // real request-dispatch loop would. Conditional request kinds (v2.5.0), @@ -82,15 +88,15 @@ // in an internal structured extraction of the specification named above; // no text from that document is reproduced here. The concrete endpoint // numbering (GPIO at endpoint id / byte_bus_id 1, SPI at 2, I2C at 3, ADC at -// 4, PWM_IN at 5, LIN at 6, CAN at 7, UART at 8, ISELED at 9), access-policy -// choice for operational requests (gated on lifecycle state only, not -// per-endpoint ownership — see dispatch()'s own comment), and EP0 partial-read -// encoding chosen in this file are this implementation's own, purely for the -// purposes of being a usable in-process simulator — full bit-for-bit -// conformance against other TC18 implementations is not claimed, same as -// the equivalent disclaimers in rcp/regmap.hpp, rcp/lifecycle.hpp, +// 4, PWM_IN at 5, LIN at 6, CAN at 7, UART at 8, ISELED at 9, MDIO at 10), +// access-policy choice for operational requests (gated on lifecycle state +// only, not per-endpoint ownership — see dispatch()'s own comment), and EP0 +// partial-read encoding chosen in this file are this implementation's own, +// purely for the purposes of being a usable in-process simulator — full +// bit-for-bit conformance against other TC18 implementations is not claimed, +// same as the equivalent disclaimers in rcp/regmap.hpp, rcp/lifecycle.hpp, // rcp/gpio.hpp, rcp/spi.hpp, rcp/i2c.hpp, rcp/adc.hpp, rcp/pwm.hpp, -// rcp/lin.hpp, rcp/can.hpp, rcp/uart.hpp, and rcp/iseled.hpp. +// rcp/lin.hpp, rcp/can.hpp, rcp/uart.hpp, rcp/iseled.hpp, and rcp/mdio.hpp. #pragma once #include @@ -103,6 +109,7 @@ #include #include #include +#include #include #include #include @@ -138,6 +145,7 @@ constexpr EndpointId kLinEndpointId = 6; constexpr EndpointId kCanEndpointId = 7; constexpr EndpointId kUartEndpointId = 8; constexpr EndpointId kIseledEndpointId = 9; +constexpr EndpointId kMdioEndpointId = 10; constexpr avtp::ByteBusId kGpioByteBusId = static_cast(kGpioEndpointId); constexpr avtp::ByteBusId kSpiByteBusId = static_cast(kSpiEndpointId); constexpr avtp::ByteBusId kI2cByteBusId = static_cast(kI2cEndpointId); @@ -147,6 +155,7 @@ constexpr avtp::ByteBusId kLinByteBusId = static_cast(kLinEnd constexpr avtp::ByteBusId kCanByteBusId = static_cast(kCanEndpointId); constexpr avtp::ByteBusId kUartByteBusId = static_cast(kUartEndpointId); constexpr avtp::ByteBusId kIseledByteBusId = static_cast(kIseledEndpointId); +constexpr avtp::ByteBusId kMdioByteBusId = static_cast(kMdioEndpointId); // A discovery-shaped EP0 read only ever answers with the register map's // magic number below — see this header's own scope note above. @@ -207,6 +216,10 @@ inline acf::WireErrorCode wire_error_code_for(const std::error_code& ec) noexcep 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)) + return acf::WireErrorCode::InvalidParameter; // mdio_payload exceeds the width mdio_mode assigns it — client-caused, same rationale as avtp::short_buffer's mapping above return acf::WireErrorCode::UnsupportedCmd; } @@ -242,6 +255,7 @@ class Server final { can::CanEndpoint& can() noexcept { return can_; } uart::UartEndpoint& uart() noexcept { return uart_; } iseled::IseledEndpoint& iseled() noexcept { return iseled_; } + mdio::MdioEndpoint& mdio() noexcept { return mdio_; } // set_spi_poci scripts the bytes a subsequent dispatch()/transfer() // call on `channel` reads back as POCI-in data. A real SPI peripheral's @@ -352,6 +366,16 @@ class Server final { iseled_response_ = response; } + // No set_mdio_response()-shaped hook here either, but for yet a + // different reason than set_can_response's/the UART note's own: MDIO's + // model (rcp/mdio.hpp) is a self-contained (mode, mdio_address)-keyed + // register map, not an external bus — a value dispatch_mdio() writes + // round-trips straight back out of mdio::MdioEndpoint itself on a + // subsequent read (see MdioEndpoint::transact's own comment and + // REQ-MDIO-003), so there is nothing external left for a test to + // script. mdio() above already exposes MdioEndpoint directly for any + // test that wants to call transact()/handle_request() itself. + // advance_to_rcp_configured is a convenience for tests/simulators that // don't care about exercising ServerLifecycle's intermediate // plausibility-check gating themselves and just want a fully live @@ -394,6 +418,7 @@ class Server final { if (req.byte_bus_id == kCanByteBusId) return dispatch_can(req, req_payload, out_resp, out_resp_payload); if (req.byte_bus_id == kUartByteBusId) return dispatch_uart(req, req_payload, out_resp, out_resp_payload); if (req.byte_bus_id == kIseledByteBusId) return dispatch_iseled(req, req_payload, out_resp, out_resp_payload); + if (req.byte_bus_id == kMdioByteBusId) return dispatch_mdio(req, req_payload, out_resp, out_resp_payload); return set_error_response(req, make_error_code(regmap::RegMapErrc::invalid_parameter), out_resp, out_resp_payload); @@ -414,9 +439,9 @@ class Server final { static regmap::RegisterMap make_initial_register_map() { regmap::RegisterMap regs; - regs.endpoint_count = 9; - regs.generic_configs.resize(9); - regs.functional_configs.resize(9); + regs.endpoint_count = 10; + regs.generic_configs.resize(10); + regs.functional_configs.resize(10); regs.ep_id_mapping = { {kGpioEndpointId, kGpioByteBusId}, {kSpiEndpointId, kSpiByteBusId}, @@ -427,6 +452,7 @@ class Server final { {kCanEndpointId, kCanByteBusId}, {kUartEndpointId, kUartByteBusId}, {kIseledEndpointId, kIseledByteBusId}, + {kMdioEndpointId, kMdioByteBusId}, }; return regs; } @@ -769,6 +795,68 @@ class Server final { return {}; } + // MDIO, this header's own eighth and LAST Table 33 Row 2 endpoint type + // (extraction §13.7.13.3), branches on req.op the same way + // dispatch_uart does — MdioRequest::is_write is intrinsic to the + // 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 + // 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 + // comment flags for CAN's Figure 40. This dispatch path makes the same + // kind of deliberately simple, explicitly disclaimed choice + // dispatch_can makes for its own fixed CanIdentifier/FrameFormat: a + // fixed representative MdioMode::MmdSingleWord mode and mdio_address 0 + // (the mode/address bit-level decode Table 60/Figure 43 would require + // is out of scope for this evt[2:0] classification pass), with + // mdio_payload packed/unpacked via avtp::detail::put_u16/get_u16 — the + // same generic byte-packing helper dispatch_ep0 above already uses for + // its own magic-number encoding — purely so this mock's own + // request/response payload has *some* round-trippable value to exercise + // end-to-end. This is this mock's own simulation choice for its own + // wire shape, not a TC18-derived MDIO byte-level encoding of Figure + // 43's actual field layout; full bit-for-bit conformance is not + // claimed, same as every other disclaimer in this file. Table 33 Row + // 2's 3-way Plain/Reserved/ConfigWrite classification (rcp::endpoint:: + // evt_row2_kind_of) is checked by handle_request itself, before + // transact() ever touches the register map — see handle_request's own + // doc comment for why a Reserved or ConfigWrite evt must never reach + // it. No set_mdio_response() hook exists — see mdio()'s own comment + // above for why MDIO's self-contained register model needs none. + std::error_code dispatch_mdio(const acf::AcfMessageInfo& req, const std::vector& payload, + acf::AcfMessageInfo& out_resp, + std::vector& out_resp_payload) noexcept { + if (!operational_requests_allowed()) { + return set_error_response(req, make_error_code(regmap::RegMapErrc::request_rejected), + out_resp, out_resp_payload); + } + mdio::MdioRequest request; + request.mode = mdio::MdioMode::MmdSingleWord; + request.mdio_address = 0; + request.mms_is_0_or_1 = false; + request.is_write = req.op; + request.mdio_payload = (req.op && payload.size() >= 2) ? avtp::detail::get_u16(payload.data()) : 0; + + mdio::MdioResponse response; + auto ec = mdio_.handle_request(req.evt_op, request, response); + if (ec) return set_error_response(req, ec, out_resp, out_resp_payload); + + if (req.op) { + out_resp = acf::make_response(req, req.evt_ack ? acf::ResponseKind::Acknowledge + : acf::ResponseKind::WriteResponse); + } else { + out_resp_payload.resize(2); + avtp::detail::put_u16(out_resp_payload.data(), static_cast(response.mdio_payload)); + out_resp = acf::make_response(req, req.evt_ack ? acf::ResponseKind::Acknowledge + : acf::ResponseKind::ReadResponse); + } + return {}; + } + lifecycle::ServerLifecycle lifecycle_; regmap::RegisterMap regs_; regmap::Ep0 ep0_; @@ -781,6 +869,7 @@ class Server final { can::CanEndpoint can_; uart::UartEndpoint uart_; iseled::IseledEndpoint iseled_; + mdio::MdioEndpoint mdio_; std::array, spi::kMaxChannels> spi_poci_{}; std::vector i2c_response_{}; bool i2c_acked_ = true; diff --git a/tests/test_mdio.cpp b/tests/test_mdio.cpp index 84453b7..2006f20 100644 --- a/tests/test_mdio.cpp +++ b/tests/test_mdio.cpp @@ -3,6 +3,8 @@ // fusa:test REQ-MDIO-003 // fusa:test REQ-MDIO-004 // fusa:test REQ-MDIO-005 +// fusa:test REQ-MDIO-006 +// fusa:test REQ-MDIO-007 // Tests for rcp/mdio.hpp — the MDIO endpoint type (ROADMAP.md milestone 51, // "Remaining Endpoint Types — LIN, CAN (incl. CAN XL), ISELED, MDIO, Wakeup @@ -68,7 +70,7 @@ TEST_CASE("validate_request rejects a payload exceeding its mode's width", "[mdi // ── Register write-then-read round-trip, keyed on (mode, mdio_address) ────── -TEST_CASE("MdioEndpoint::handle_request write then read round-trips the value", +TEST_CASE("MdioEndpoint::transact write then read round-trips the value", "[mdio][REQ-MDIO-003]") { MdioEndpoint ep; MdioRequest write_req; @@ -78,17 +80,17 @@ TEST_CASE("MdioEndpoint::handle_request write then read round-trips the value", write_req.mdio_payload = 0xBEEF; MdioResponse out; - REQUIRE_FALSE(ep.handle_request(write_req, out)); + REQUIRE_FALSE(ep.transact(write_req, out)); REQUIRE(out.mdio_payload == 0xBEEF); MdioRequest read_req = write_req; read_req.is_write = false; read_req.mdio_payload = 0; - REQUIRE_FALSE(ep.handle_request(read_req, out)); + REQUIRE_FALSE(ep.transact(read_req, out)); REQUIRE(out.mdio_payload == 0xBEEF); } -TEST_CASE("MdioEndpoint::handle_request reads an unwritten register as zero", "[mdio][REQ-MDIO-003]") { +TEST_CASE("MdioEndpoint::transact reads an unwritten register as zero", "[mdio][REQ-MDIO-003]") { MdioEndpoint ep; MdioRequest req; req.mode = MdioMode::MmsSingleWord; @@ -96,13 +98,13 @@ TEST_CASE("MdioEndpoint::handle_request reads an unwritten register as zero", "[ req.is_write = false; MdioResponse out; - REQUIRE_FALSE(ep.handle_request(req, out)); + REQUIRE_FALSE(ep.transact(req, out)); REQUIRE(out.mdio_payload == 0); } // ── mdio_mode keeps each mode's register space distinct ───────────────────── -TEST_CASE("MdioEndpoint::handle_request keys on mdio_mode in addition to mdio_address, " +TEST_CASE("MdioEndpoint::transact keys on mdio_mode in addition to mdio_address, " "so different modes at the same address never collide", "[mdio][REQ-MDIO-004]") { MdioEndpoint ep; @@ -113,7 +115,7 @@ TEST_CASE("MdioEndpoint::handle_request keys on mdio_mode in addition to mdio_ad mmd_write.is_write = true; mmd_write.mdio_payload = 0x1111; MdioResponse out; - REQUIRE_FALSE(ep.handle_request(mmd_write, out)); + REQUIRE_FALSE(ep.transact(mmd_write, out)); // Same mdio_address, but MmsSingleWord mode — must not read back the // MMD write above. @@ -121,29 +123,29 @@ TEST_CASE("MdioEndpoint::handle_request keys on mdio_mode in addition to mdio_ad mms_read.mode = MdioMode::MmsSingleWord; mms_read.mdio_address = 4; mms_read.is_write = false; - REQUIRE_FALSE(ep.handle_request(mms_read, out)); + REQUIRE_FALSE(ep.transact(mms_read, out)); REQUIRE(out.mdio_payload == 0); MdioRequest mms_write = mms_read; mms_write.is_write = true; mms_write.mdio_payload = 0x2222; - REQUIRE_FALSE(ep.handle_request(mms_write, out)); + REQUIRE_FALSE(ep.transact(mms_write, out)); REQUIRE(out.mdio_payload == 0x2222); // The earlier MMD write is unaffected. MdioRequest mmd_read = mmd_write; mmd_read.is_write = false; - REQUIRE_FALSE(ep.handle_request(mmd_read, out)); + REQUIRE_FALSE(ep.transact(mmd_read, out)); REQUIRE(out.mdio_payload == 0x1111); } -TEST_CASE("MdioEndpoint::handle_request fires TransferComplete on every request", "[mdio][REQ-MDIO-004]") { +TEST_CASE("MdioEndpoint::transact fires TransferComplete on every request", "[mdio][REQ-MDIO-004]") { MdioEndpoint ep; ep.triggers().enable(mdio_signal_id(MdioSignal::TransferComplete)); MdioRequest req; MdioResponse out; - REQUIRE_FALSE(ep.handle_request(req, out)); + REQUIRE_FALSE(ep.transact(req, out)); auto drained = ep.triggers().drain(); REQUIRE(drained.size() == 1); @@ -157,3 +159,108 @@ TEST_CASE("MdioErrc reports a non-empty message in its own category", "[mdio][RE REQUIRE(ec.category() == mdio_category()); REQUIRE_FALSE(ec.message().empty()); } + +// ── Table 33 Row 2 evt[2:0] validation (handle_request) ───────────────────── + +TEST_CASE("MdioEndpoint::handle_request delegates a Plain (evt[2:0]==000b) request to transact()", + "[mdio][REQ-MDIO-006]") { + MdioEndpoint ep; + MdioRequest req; + req.mode = MdioMode::MmdSingleWord; + req.mdio_address = 5; + req.is_write = true; + req.mdio_payload = 0xBEEF; + + MdioResponse out; + auto ec = ep.handle_request(/*evt_op=*/0, req, out); + REQUIRE_FALSE(ec); + REQUIRE(out.mdio_payload == 0xBEEF); + REQUIRE(ep.last_request().mdio_address == 5); +} + +TEST_CASE("MdioEndpoint::handle_request rejects every reserved evt[2:0] value (001b-110b) " + "without recording anything", + "[mdio][REQ-MDIO-006]") { + for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { + MdioEndpoint ep; + MdioRequest req; + req.mode = MdioMode::MmdSingleWord; + req.mdio_address = 9; + req.is_write = true; + req.mdio_payload = 0x1234; + + MdioResponse out; + auto ec = ep.handle_request(evt_op, req, out); + REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); + // A rejected reserved evt must not record anything — last_request() + // stays at its default-constructed value, no register is written, + // and no trigger fires. + REQUIRE(ep.last_request().mdio_address == 0); + REQUIRE_FALSE(ep.triggers().has_pending()); + + // The register that would have been written must genuinely be + // untouched: a subsequent Plain read at the same address must not + // observe req.mdio_payload above. + MdioRequest read_back = req; + read_back.is_write = false; + read_back.mdio_payload = 0; + REQUIRE_FALSE(ep.handle_request(/*evt_op=*/0, read_back, out)); + REQUIRE(out.mdio_payload == 0); + } +} + +TEST_CASE("MdioEndpoint::handle_request reports config_write_not_supported for evt[2:0]==111b " + "without crashing or recording anything", + "[mdio][REQ-MDIO-007]") { + MdioEndpoint ep; + MdioRequest req; + req.mode = MdioMode::MmsSingleWord; + req.mdio_address = 2; + req.is_write = true; + req.mdio_payload = 0x00FF; + + MdioResponse out; + auto ec = ep.handle_request(/*evt_op=*/7, req, out); + REQUIRE(ec == make_error_code(MdioErrc::config_write_not_supported)); + REQUIRE(ep.last_request().mdio_address == 0); + REQUIRE_FALSE(ep.triggers().has_pending()); +} + +TEST_CASE("MdioEndpoint::handle_request masks evt_op down to 3 bits before classifying", + "[mdio][REQ-MDIO-006]") { + MdioEndpoint ep; + MdioRequest req; + MdioResponse out; + REQUIRE_FALSE(ep.handle_request(/*evt_op=*/0xF8, req, out)); // low 3 bits 000 -> Plain + auto ec = ep.handle_request(/*evt_op=*/0xF9, req, out); // low 3 bits 001 -> Reserved + REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); +} + +TEST_CASE("MdioEndpoint::handle_request Reserved/ConfigWrite classification is independent of " + "mode/mdio_address/mdio_payload — evt[2:0] carries no field-value selector", + "[mdio][REQ-MDIO-006]") { + // Guards against confusing Table 33's evt[2:0] classification with the + // request's own mode/mdio_address/mdio_payload fields (see + // handle_request's own header comment): a Reserved/ConfigWrite evt is + // rejected identically no matter what those fields carry. + MdioEndpoint ep; + MdioRequest req; + req.mode = MdioMode::MmsMultiWord; + req.mms_is_0_or_1 = true; + req.mdio_address = 0xFFFF; + req.is_write = true; + req.mdio_payload = 0xFFFFFFFFu; + + MdioResponse out; + REQUIRE(ep.handle_request(/*evt_op=*/3, req, out) == + rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); + REQUIRE(ep.handle_request(/*evt_op=*/7, req, out) == + make_error_code(MdioErrc::config_write_not_supported)); +} + +TEST_CASE("MdioErrc::config_write_not_supported reports a non-empty message in its own category", + "[mdio][REQ-MDIO-007]") { + auto ec = make_error_code(MdioErrc::config_write_not_supported); + REQUIRE(ec.category() == mdio_category()); + REQUIRE_FALSE(ec.message().empty()); +} diff --git a/tests/test_mock.cpp b/tests/test_mock.cpp index 255c2e9..d1fe0f4 100644 --- a/tests/test_mock.cpp +++ b/tests/test_mock.cpp @@ -22,6 +22,8 @@ // fusa:test REQ-MOCK-022 // fusa:test REQ-MOCK-023 // fusa:test REQ-MOCK-024 +// fusa:test REQ-MOCK-025 +// fusa:test REQ-MOCK-026 // Tests for rcp/mock.hpp — the in-process RC Server simulator (ROADMAP.md // milestone 56, "Test & Simulation Harness Rebuild", v2.12.0). See @@ -48,14 +50,14 @@ acf::AcfMessageInfo standard_request(avtp::ByteBusId bus_id, bool write, uint8_t // ── Construction / register map ────────────────────────────────────────────── -TEST_CASE("Server starts HW_UNCONFIGURED with a nine-endpoint register map", +TEST_CASE("Server starts HW_UNCONFIGURED with a ten-endpoint register map", "[mock][REQ-MOCK-001]") { mock::Server server; REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); - REQUIRE(server.registers().endpoint_count == 9); - REQUIRE(server.registers().generic_configs.size() == 9); - REQUIRE(server.registers().functional_configs.size() == 9); - REQUIRE(server.registers().ep_id_mapping.size() == 9); + REQUIRE(server.registers().endpoint_count == 10); + REQUIRE(server.registers().generic_configs.size() == 10); + REQUIRE(server.registers().functional_configs.size() == 10); + REQUIRE(server.registers().ep_id_mapping.size() == 10); REQUIRE(server.registers().ep_id_mapping[0].ep_id == mock::kGpioEndpointId); REQUIRE(server.registers().ep_id_mapping[0].byte_bus_id == mock::kGpioByteBusId); REQUIRE(server.registers().ep_id_mapping[1].ep_id == mock::kSpiEndpointId); @@ -74,6 +76,8 @@ TEST_CASE("Server starts HW_UNCONFIGURED with a nine-endpoint register map", REQUIRE(server.registers().ep_id_mapping[7].byte_bus_id == mock::kUartByteBusId); REQUIRE(server.registers().ep_id_mapping[8].ep_id == mock::kIseledEndpointId); REQUIRE(server.registers().ep_id_mapping[8].byte_bus_id == mock::kIseledByteBusId); + REQUIRE(server.registers().ep_id_mapping[9].ep_id == mock::kMdioEndpointId); + REQUIRE(server.registers().ep_id_mapping[9].byte_bus_id == mock::kMdioByteBusId); } TEST_CASE("advance_to_rcp_configured drives the lifecycle straight to RCP_CONFIGURED", @@ -855,6 +859,110 @@ TEST_CASE("ISELED request is rejected before RCP_CONFIGURED, same operational ga REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); } +// ── MDIO ────────────────────────────────────────────────────────────────────── +// Table 30/33 Row 2 evt[2:0] validation, EIGHTH and LAST endpoint type after +// I2C, ADC, PWM_IN, LIN, CAN, UART, and ISELED: MdioEndpoint wired into +// dispatch() at byte_bus_id 10 for the FIRST time (this file had zero MDIO +// wiring before this pass — see mock.hpp's own header comment and +// dispatch_mdio's own comment). Unlike ISELED, no MDIO byte-level wire codec +// exists in this codebase, so dispatch_mdio fixes mode/mdio_address and +// packs/unpacks mdio_payload via avtp::detail::put_u16/get_u16 purely for +// this mock's own round-trippable wire shape — see dispatch_mdio's own +// comment in rcp/mock.hpp for why. There is no set_mdio_response() hook: +// MDIO's register model is self-contained (a write round-trips straight +// back out on a subsequent read), unlike I2C/LIN/ISELED's external-bus +// scripting hooks. + +TEST_CASE("MDIO plain write then read round-trips the value through avtp::detail's u16 packing", + "[mock][REQ-MOCK-025]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + std::vector write_payload(2); + avtp::detail::put_u16(write_payload.data(), 0xBEEF); + auto write_req = standard_request(mock::kMdioByteBusId, /*write=*/true, /*evt_op=*/0); + acf::AcfMessageInfo write_resp; + std::vector write_resp_payload; + REQUIRE_FALSE(server.dispatch(0, write_req, write_payload, write_resp, write_resp_payload)); + REQUIRE(acf::response_kind_of(write_resp) == acf::ResponseKind::WriteResponse); + REQUIRE(write_resp_payload.empty()); + REQUIRE(server.mdio().last_request().is_write); + REQUIRE(server.mdio().last_request().mdio_payload == 0xBEEF); + + auto read_req = standard_request(mock::kMdioByteBusId, /*write=*/false, /*evt_op=*/0); + acf::AcfMessageInfo read_resp; + std::vector read_resp_payload; + REQUIRE_FALSE(server.dispatch(0, read_req, {}, read_resp, read_resp_payload)); + REQUIRE(acf::response_kind_of(read_resp) == acf::ResponseKind::ReadResponse); + REQUIRE(read_resp_payload.size() == 2); + REQUIRE(avtp::detail::get_u16(read_resp_payload.data()) == 0xBEEF); +} + +TEST_CASE("MDIO request with a reserved evt[2:0] (001b-110b) is rejected with wire error code " + "UNSUPPORTED_CMD and does not touch endpoint state", + "[mock][REQ-MOCK-025]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + std::vector payload(2); + avtp::detail::put_u16(payload.data(), 0x1234); + + for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { + auto req = standard_request(mock::kMdioByteBusId, /*write=*/true, evt_op); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, payload, resp, resp_payload); + REQUIRE(ec == endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); + REQUIRE(resp_payload == + std::vector{static_cast(acf::WireErrorCode::UnsupportedCmd)}); + REQUIRE(server.mdio().last_request().mdio_address == 0); + REQUIRE_FALSE(server.mdio().last_request().is_write); + } + + // Confirm the reserved-evt requests above genuinely never wrote anything + // — a subsequent Plain read at the same (fixed) address reads back 0. + auto read_req = standard_request(mock::kMdioByteBusId, /*write=*/false, /*evt_op=*/0); + acf::AcfMessageInfo read_resp; + std::vector read_resp_payload; + REQUIRE_FALSE(server.dispatch(0, read_req, {}, read_resp, read_resp_payload)); + REQUIRE(avtp::detail::get_u16(read_resp_payload.data()) == 0); +} + +TEST_CASE("MDIO request with evt[2:0]==111b (config-write) is rejected with wire error code " + "UNSUPPORTED_CMD rather than crashing or being treated as a plain transaction", + "[mock][REQ-MOCK-025]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + std::vector payload(2); + avtp::detail::put_u16(payload.data(), 0x00FF); + + auto req = standard_request(mock::kMdioByteBusId, /*write=*/true, /*evt_op=*/7); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, payload, resp, resp_payload); + REQUIRE(ec == mdio::make_error_code(mdio::MdioErrc::config_write_not_supported)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); + REQUIRE(resp_payload == + std::vector{static_cast(acf::WireErrorCode::UnsupportedCmd)}); + REQUIRE(server.mdio().last_request().mdio_address == 0); +} + +TEST_CASE("MDIO request is rejected before RCP_CONFIGURED, same operational gating as " + "GPIO/SPI/I2C/ADC/PWM_IN/LIN/CAN/UART/ISELED", + "[mock][REQ-MOCK-026]") { + mock::Server server; + REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); + + auto req = standard_request(mock::kMdioByteBusId, /*write=*/true, /*evt_op=*/0); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {0xAA, 0xBB}, resp, resp_payload); + REQUIRE(ec == regmap::make_error_code(regmap::RegMapErrc::request_rejected)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); +} + // ── Unmapped endpoints ──────────────────────────────────────────────────────── TEST_CASE("dispatch to an unmapped byte_bus_id reports invalid_parameter",