diff --git a/.fusa-reqs.json b/.fusa-reqs.json index e94e358..2cda440 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -82,8 +82,8 @@ }, { "id": "REQ-MOCK-001", - "title": "Server starts HW_UNCONFIGURED with a representative eight-endpoint register map", - "text": "rcp::mock::Server shall construct with rcp::lifecycle::ServerLifecycle in state HwUnconfigured and an rcp::regmap::RegisterMap whose endpoint_count, generic_configs, functional_configs, and ep_id_mapping describe exactly eight endpoints: GPIO at endpoint id/byte_bus_id 1, SPI at endpoint id/byte_bus_id 2, I2C at endpoint id/byte_bus_id 3, ADC at endpoint id/byte_bus_id 4, PWM_IN at endpoint id/byte_bus_id 5, LIN at endpoint id/byte_bus_id 6, CAN at endpoint id/byte_bus_id 7, and UART at endpoint id/byte_bus_id 8.", + "title": "Server starts HW_UNCONFIGURED with a representative nine-endpoint register map", + "text": "rcp::mock::Server shall construct with rcp::lifecycle::ServerLifecycle in state HwUnconfigured and an rcp::regmap::RegisterMap whose endpoint_count, generic_configs, functional_configs, and ep_id_mapping describe exactly nine endpoints: GPIO at endpoint id/byte_bus_id 1, SPI at endpoint id/byte_bus_id 2, I2C at endpoint id/byte_bus_id 3, ADC at endpoint id/byte_bus_id 4, PWM_IN at endpoint id/byte_bus_id 5, LIN at endpoint id/byte_bus_id 6, CAN at endpoint id/byte_bus_id 7, UART at endpoint id/byte_bus_id 8, and ISELED at endpoint id/byte_bus_id 9.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -268,6 +268,24 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-MOCK-023", + "title": "ISELED dispatch applies Table 33 Row 2 evt[2:0] classification before touching endpoint state, decoding/encoding through ISELED's own Figure 40/41 codec", + "text": "rcp::mock::Server::dispatch, for a request addressed to the ISELED endpoint while RcpConfigured, shall decode the request's byte_msg_payload via iseled::decode_iseled_request into an IseledRequest and drive iseled::IseledEndpoint::handle_request with the request's evt[2:0] field: a Plain (000b) request shall transact the decoded IseledRequest against whatever IseledResponse set_iseled_response() last scripted (default IseledResponse{0, 0}) and be answered as a ReadResponse whose byte_msg_payload is iseled::encode_iseled_response(IseledEndpoint::last_response()), 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 iseled::IseledErrc::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-024", + "title": "Operational ISELED requests are rejected before RCP_CONFIGURED, same gating as GPIO/SPI/I2C/ADC/PWM_IN/LIN/CAN/UART", + "text": "rcp::mock::Server::dispatch, for a request addressed to the ISELED 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 already apply to GPIO/SPI/I2C/ADC/PWM_IN/LIN/CAN/UART.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, { "id": "REQ-UDP-001", "title": "Frame round-trips an NTSCF-framed ACF_ABB request", @@ -3390,6 +3408,27 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-ISELED-006", + "title": "IseledEndpoint::handle_request applies Table 33 Row 2 evt[2:0] classification before transacting", + "text": "rcp::iseled::IseledEndpoint::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, response) with both arguments unchanged; for EvtRow2Kind::Reserved, shall return endpoint::EndpointErrc::reserved_evt_row2 without recording anything as the last request/response (last_request()/last_response() left untouched) 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\"), 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-ISELED-007", + "title": "IseledEndpoint::handle_request reports config-write (evt[2:0]==111b) as not yet supported", + "text": "rcp::iseled::IseledEndpoint::handle_request, for EvtRow2Kind::ConfigWrite (evt[2:0]==111b), shall return IseledErrc::config_write_not_supported without recording anything as the last request/response 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, and UART's UartErrc::config_write_not_supported already made. IseledErrc::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-MDIO-001", "title": "MDIO's ep_type id is 0x0D", diff --git a/include/rcp/iseled.hpp b/include/rcp/iseled.hpp index 952a825..cba30c3 100644 --- a/include/rcp/iseled.hpp +++ b/include/rcp/iseled.hpp @@ -3,6 +3,8 @@ // fusa:req REQ-ISELED-003 // fusa:req REQ-ISELED-004 // fusa:req REQ-ISELED-005 +// fusa:req REQ-ISELED-006 +// fusa:req REQ-ISELED-007 // ISELED endpoint (ep_type 0x0C) — the OPEN Alliance TC18 Remote Control // Protocol Specification v0.5.1_RC's native ISELED daisy-chain framing: a @@ -45,12 +47,30 @@ // 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(). +// // 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, and rcp/e2e.hpp. +// 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. #pragma once #include @@ -91,6 +111,15 @@ struct IseledResponse { 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, }; inline const std::error_category& iseled_category() noexcept { @@ -100,6 +129,8 @@ inline const std::error_category& iseled_category() noexcept { switch (static_cast(ev)) { case IseledErrc::field_out_of_range: return "rcp/iseled: instruction/address/data exceeds its wire field width"; + 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"; } @@ -228,6 +259,63 @@ class IseledEndpoint { 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 + // 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) { + switch (endpoint::evt_row2_kind_of(evt_op)) { + case endpoint::EvtRow2Kind::Plain: + return transact(std::move(request), std::move(response)); + case endpoint::EvtRow2Kind::Reserved: + return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); + case endpoint::EvtRow2Kind::ConfigWrite: + return make_error_code(IseledErrc::config_write_not_supported); + } + 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_; } endpoint::TriggerRegistry& triggers() noexcept { return triggers_; } diff --git a/include/rcp/mock.hpp b/include/rcp/mock.hpp index 914fcfc..c632588 100644 --- a/include/rcp/mock.hpp +++ b/include/rcp/mock.hpp @@ -20,6 +20,8 @@ // fusa:req REQ-MOCK-020 // fusa:req REQ-MOCK-021 // fusa:req REQ-MOCK-022 +// fusa:req REQ-MOCK-023 +// fusa:req REQ-MOCK-024 // In-process RC Server simulator — a small, representative OPEN Alliance // TC18 Remote Control Protocol Specification v0.5.1_RC server built @@ -45,16 +47,21 @@ // 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 eight fully-built endpoint types — rcp::gpio::GpioEndpoint and +// each of nine 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, and rcp::uart::UartEndpoint (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, and UART follow-ups, in that order — UART's is 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) — as its -// representative endpoint set. dispatch() below is the single +// rcp::can::CanEndpoint, rcp::uart::UartEndpoint, and +// rcp::iseled::IseledEndpoint (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 +// 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 // 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), @@ -75,15 +82,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), 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 +// 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, // rcp/gpio.hpp, rcp/spi.hpp, rcp/i2c.hpp, rcp/adc.hpp, rcp/pwm.hpp, -// rcp/lin.hpp, rcp/can.hpp, and rcp/uart.hpp. +// rcp/lin.hpp, rcp/can.hpp, rcp/uart.hpp, and rcp/iseled.hpp. #pragma once #include @@ -93,6 +100,7 @@ #include #include #include +#include #include #include #include @@ -129,6 +137,7 @@ constexpr EndpointId kPwmInEndpointId = 5; constexpr EndpointId kLinEndpointId = 6; constexpr EndpointId kCanEndpointId = 7; constexpr EndpointId kUartEndpointId = 8; +constexpr EndpointId kIseledEndpointId = 9; constexpr avtp::ByteBusId kGpioByteBusId = static_cast(kGpioEndpointId); constexpr avtp::ByteBusId kSpiByteBusId = static_cast(kSpiEndpointId); constexpr avtp::ByteBusId kI2cByteBusId = static_cast(kI2cEndpointId); @@ -137,6 +146,7 @@ constexpr avtp::ByteBusId kPwmInByteBusId = static_cast(kPwmInE constexpr avtp::ByteBusId kLinByteBusId = static_cast(kLinEndpointId); constexpr avtp::ByteBusId kCanByteBusId = static_cast(kCanEndpointId); constexpr avtp::ByteBusId kUartByteBusId = static_cast(kUartEndpointId); +constexpr avtp::ByteBusId kIseledByteBusId = static_cast(kIseledEndpointId); // A discovery-shaped EP0 read only ever answers with the register map's // magic number below — see this header's own scope note above. @@ -193,6 +203,10 @@ inline acf::WireErrorCode wire_error_code_for(const std::error_code& ec) noexcep return acf::WireErrorCode::InvalidParameter; // requested read_size exceeds kMaxReadSize — client-caused, mirrors avtp::short_buffer's mapping above if (ec == make_error_code(uart::UartErrc::tx_queue_overflow)) 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 return acf::WireErrorCode::UnsupportedCmd; } @@ -227,6 +241,7 @@ class Server final { lin::LinEndpoint& lin() noexcept { return lin_; } can::CanEndpoint& can() noexcept { return can_; } uart::UartEndpoint& uart() noexcept { return uart_; } + iseled::IseledEndpoint& iseled() noexcept { return iseled_; } // set_spi_poci scripts the bytes a subsequent dispatch()/transfer() // call on `channel` reads back as POCI-in data. A real SPI peripheral's @@ -322,6 +337,21 @@ 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 + // 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; + } + // 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 @@ -363,6 +393,7 @@ class Server final { if (req.byte_bus_id == kLinByteBusId) return dispatch_lin(req, req_payload, out_resp, out_resp_payload); 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); return set_error_response(req, make_error_code(regmap::RegMapErrc::invalid_parameter), out_resp, out_resp_payload); @@ -383,9 +414,9 @@ class Server final { static regmap::RegisterMap make_initial_register_map() { regmap::RegisterMap regs; - regs.endpoint_count = 8; - regs.generic_configs.resize(8); - regs.functional_configs.resize(8); + regs.endpoint_count = 9; + regs.generic_configs.resize(9); + regs.functional_configs.resize(9); regs.ep_id_mapping = { {kGpioEndpointId, kGpioByteBusId}, {kSpiEndpointId, kSpiByteBusId}, @@ -395,6 +426,7 @@ class Server final { {kLinEndpointId, kLinByteBusId}, {kCanEndpointId, kCanByteBusId}, {kUartEndpointId, kUartByteBusId}, + {kIseledEndpointId, kIseledByteBusId}, }; return regs; } @@ -686,6 +718,57 @@ 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 + // 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 + // 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. + std::error_code dispatch_iseled(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); + } + 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_); + if (ec) return set_error_response(req, ec, out_resp, out_resp_payload); + out_resp_payload = iseled::encode_iseled_response(iseled_.last_response()); + 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_; @@ -697,12 +780,14 @@ class Server final { lin::LinEndpoint lin_; can::CanEndpoint can_; uart::UartEndpoint uart_; + iseled::IseledEndpoint iseled_; std::array, spi::kMaxChannels> spi_poci_{}; std::vector i2c_response_{}; bool i2c_acked_ = true; std::deque adc_samples_{}; std::vector lin_response_{}; bool lin_responded_ = true; + iseled::IseledResponse iseled_response_{}; }; } // namespace mock diff --git a/tests/test_iseled.cpp b/tests/test_iseled.cpp index 671a685..8d0004b 100644 --- a/tests/test_iseled.cpp +++ b/tests/test_iseled.cpp @@ -3,6 +3,8 @@ // fusa:test REQ-ISELED-003 // fusa:test REQ-ISELED-004 // 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, @@ -210,3 +212,105 @@ TEST_CASE("IseledErrc reports a non-empty message in its own category", "[iseled REQUIRE(ec.category() == iseled_category()); REQUIRE_FALSE(ec.message().empty()); } + +// ── 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()", + "[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; + + auto ec = ep.handle_request(/*evt_op=*/0, req, resp); + REQUIRE_FALSE(ec); + REQUIRE(ep.last_request().instruction == 0x03); + REQUIRE(ep.last_request().address == 0x0102); + REQUIRE(ep.last_response().data == 0x0BEF); +} + +TEST_CASE("IseledEndpoint::handle_request rejects every reserved evt[2:0] value (001b-110b) " + "without recording anything", + "[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); + 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_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]") { + 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); + REQUIRE(ec == make_error_code(IseledErrc::config_write_not_supported)); + REQUIRE(ep.last_request().address == 0); + REQUIRE(ep.last_response().address == 0); + 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(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 dbcf87c..255c2e9 100644 --- a/tests/test_mock.cpp +++ b/tests/test_mock.cpp @@ -20,6 +20,8 @@ // fusa:test REQ-MOCK-020 // fusa:test REQ-MOCK-021 // fusa:test REQ-MOCK-022 +// fusa:test REQ-MOCK-023 +// fusa:test REQ-MOCK-024 // Tests for rcp/mock.hpp — the in-process RC Server simulator (ROADMAP.md // milestone 56, "Test & Simulation Harness Rebuild", v2.12.0). See @@ -46,14 +48,14 @@ acf::AcfMessageInfo standard_request(avtp::ByteBusId bus_id, bool write, uint8_t // ── Construction / register map ────────────────────────────────────────────── -TEST_CASE("Server starts HW_UNCONFIGURED with an eight-endpoint register map", +TEST_CASE("Server starts HW_UNCONFIGURED with a nine-endpoint register map", "[mock][REQ-MOCK-001]") { mock::Server server; REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); - REQUIRE(server.registers().endpoint_count == 8); - REQUIRE(server.registers().generic_configs.size() == 8); - REQUIRE(server.registers().functional_configs.size() == 8); - REQUIRE(server.registers().ep_id_mapping.size() == 8); + 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().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); @@ -70,6 +72,8 @@ TEST_CASE("Server starts HW_UNCONFIGURED with an eight-endpoint register map", REQUIRE(server.registers().ep_id_mapping[6].byte_bus_id == mock::kCanByteBusId); REQUIRE(server.registers().ep_id_mapping[7].ep_id == mock::kUartEndpointId); 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); } TEST_CASE("advance_to_rcp_configured drives the lifecycle straight to RCP_CONFIGURED", @@ -749,6 +753,108 @@ TEST_CASE("UART request is rejected before RCP_CONFIGURED, same operational gati REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); } +// ── 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", + "[mock][REQ-MOCK-023]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + iseled::IseledResponse scripted; + scripted.address = 0x0102; + scripted.data = 0x0BEF; + 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); + + 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); +} + +TEST_CASE("ISELED 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-023]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + iseled::IseledRequest request; + request.address = 0x0010; + auto payload = iseled::encode_iseled_request(request); + + for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { + auto req = standard_request(mock::kIseledByteBusId, /*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.iseled().last_request().address == 0); + REQUIRE(server.iseled().last_response().address == 0); + } +} + +TEST_CASE("ISELED 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-023]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + iseled::IseledRequest request; + request.address = 0x0010; + auto payload = iseled::encode_iseled_request(request); + + auto req = standard_request(mock::kIseledByteBusId, /*write=*/true, /*evt_op=*/7); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, payload, resp, resp_payload); + REQUIRE(ec == iseled::make_error_code(iseled::IseledErrc::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.iseled().last_request().address == 0); +} + +TEST_CASE("ISELED request is rejected before RCP_CONFIGURED, same operational gating as " + "GPIO/SPI/I2C/ADC/PWM_IN/LIN/CAN/UART", + "[mock][REQ-MOCK-024]") { + mock::Server server; + REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); + + iseled::IseledRequest request; + auto payload = iseled::encode_iseled_request(request); + auto req = standard_request(mock::kIseledByteBusId, /*write=*/true, /*evt_op=*/0); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, payload, 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",