diff --git a/.fusa-reqs.json b/.fusa-reqs.json index c214b6d..27d00c7 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 four-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 four 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, and ADC at endpoint id/byte_bus_id 4.", + "title": "Server starts HW_UNCONFIGURED with a representative six-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 six 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, and LIN at endpoint id/byte_bus_id 6.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -214,6 +214,24 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-MOCK-017", + "title": "LIN dispatch applies Table 33 Row 2 evt[2:0] classification before touching endpoint state", + "text": "rcp::mock::Server::dispatch, for a request addressed to the LIN endpoint while RcpConfigured, shall decode the request's evt[2:0] field via endpoint::evt_row2_kind_of and drive lin::LinEndpoint::handle_request with it: a Plain (000b) request shall be answered as a ReadResponse carrying whatever bytes set_lin_response last scripted, a Reserved (001b-110b) evt shall be rejected with endpoint::EndpointErrc::reserved_evt_row2 (wire error code UNSUPPORTED_CMD) without recording anything as sent/received, and a ConfigWrite (111b) evt shall be rejected with lin::LinErrc::config_write_not_supported (wire error code UNSUPPORTED_CMD) rather than crashing or being treated as a plain transfer.", + "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-018", + "title": "Operational LIN requests are rejected before RCP_CONFIGURED, same gating as GPIO/SPI/I2C/ADC/PWM_IN", + "text": "rcp::mock::Server::dispatch, for a request addressed to the LIN 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 already apply to GPIO/SPI/I2C/ADC/PWM_IN.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, { "id": "REQ-UDP-001", "title": "Frame round-trips an NTSCF-framed ACF_ABB request", @@ -3159,6 +3177,27 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-LINEP-005", + "title": "LinEndpoint::handle_request applies Table 33 Row 2 evt[2:0] classification before transferring", + "text": "rcp::lin::LinEndpoint::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 transfer() with out_bytes/in_bytes/responded unchanged; for EvtRow2Kind::Reserved, shall return endpoint::EndpointErrc::reserved_evt_row2 without recording anything as sent or received.", + "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-LINEP-006", + "title": "LinEndpoint::handle_request reports config-write (evt[2:0]==111b) as not yet supported", + "text": "rcp::lin::LinEndpoint::handle_request, for EvtRow2Kind::ConfigWrite (evt[2:0]==111b), shall return LinErrc::config_write_not_supported without recording anything as sent or received and without crashing, rather than silently accepting the request as an ordinary transfer 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, and PWM_IN's PwmErrc::config_write_not_supported already made. LinErrc::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-CANEP-001", "title": "CAN's ep_type id is 0x0B", diff --git a/include/rcp/lin.hpp b/include/rcp/lin.hpp index 2fd2d91..b75d68a 100644 --- a/include/rcp/lin.hpp +++ b/include/rcp/lin.hpp @@ -2,6 +2,8 @@ // fusa:req REQ-LINEP-002 // fusa:req REQ-LINEP-003 // fusa:req REQ-LINEP-004 +// fusa:req REQ-LINEP-005 +// fusa:req REQ-LINEP-006 // LIN commander endpoint (ep_type 0x06) — the OPEN Alliance TC18 Remote // Control Protocol Specification v0.5.1_RC's raw-byte-pusher model for LIN: @@ -30,12 +32,49 @@ // transfer`'s raw byte-stream shape for exactly this reason: both endpoint // types push/pull opaque bytes and leave interpretation to the caller. // +// Table 30/33 Row 2 evt[2:0] validation (post-v2.7.0, fourth endpoint type +// after I2C, ADC, and PWM_IN): LinEndpoint::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 LIN's request decode, following the exact shape +// rcp/i2c.hpp's I2cEndpoint::handle_request, rcp/adc.hpp's +// AdcEndpoint::handle_request, and rcp/pwm.hpp's PwmInEndpoint:: +// handle_request established. Plain (evt[2:0]==000b) delegates straight to +// the existing transfer() above, unchanged — LIN's raw-byte-pusher model +// already IS this row's correct "plain request" behavior, the same +// reasoning I2C's own handle_request gave for its identical shape. Reserved +// (001b-110b) is rejected with endpoint::EndpointErrc::reserved_evt_row2 +// without touching out_bytes/in_bytes or any transfer state. ConfigWrite +// (evt[2:0]==111b, §12.7.1) is reported as the new +// LinErrc::config_write_not_supported rather than crashing or silently +// accepted as a plain transfer — LIN has no EP_functional-config wiring in +// this codebase yet (same gap I2C's, ADC's, and PWM_IN's own handle_request +// comments call out for their own endpoint types), so full §12.7.1 handling +// is out of scope here too. +// +// NOT to be confused with §13.7.10.1's separate "conditions given by +// evt[2:0]" text describing pending-read-request byte-sequence matching +// against bus traffic (LIN's own analog of I2C's compound_wait_matches_bits +// — a different mechanism, out of this milestone's scope, not modeled by +// this header at all): that text is about compound-wait match conditions, +// not about Table 33's top-level Plain/Reserved/ConfigWrite request +// classification handle_request implements below. Reading the two as one +// "evt[2:0] selects a comparison mode" scheme would be exactly the kind of +// invented, non-spec-derived encoding this codebase has had to remove +// elsewhere once discovered (e.g. rcp/iseled.hpp's and rcp/mdio.hpp's own +// header comments on previously invented, non-spec-derived field encodings +// later corrected) — handle_request below calls the same shared +// evt_row2_kind_of every other Row 2 endpoint type uses and invents nothing +// of its own; §13.7.10.1's pending-read match semantics remain unimplemented +// here, called out rather than silently guessed at. +// // 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, and rcp/i2c.hpp. +// rcp/regmap.hpp, rcp/endpoint.hpp, rcp/i2c.hpp, rcp/adc.hpp, and +// rcp/pwm.hpp. #pragma once #include @@ -57,6 +96,14 @@ namespace lin { enum class LinErrc : int { no_response = 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 transfer or silently ignored, same as I2C's, ADC's, and + // PWM_IN's own config_write_not_supported variants. + config_write_not_supported = 2, }; inline const std::error_category& lin_category() noexcept { @@ -65,7 +112,9 @@ inline const std::error_category& lin_category() noexcept { std::string message(int ev) const override { switch (static_cast(ev)) { case LinErrc::no_response: return "rcp/lin: no response observed on the bus"; - default: return "rcp/lin: unknown error"; + case LinErrc::config_write_not_supported: + return "rcp/lin: evt[2:0]=111b configuration-write requests are not yet implemented"; + default: return "rcp/lin: unknown error"; } } }; @@ -114,6 +163,47 @@ class LinEndpoint { return {}; } + // handle_request is LIN's request-decode entry point — the piece this + // header previously had none of, mirroring rcp::i2c::I2cEndpoint:: + // handle_request's shape exactly (this repo's fourth Table 33 Row 2 + // endpoint type after I2C, ADC, and PWM_IN). 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 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`/`responded` unchanged — LIN's existing + // raw-byte-pusher transfer model (extraction §13.7.10.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 + // 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 LIN 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 + // (nontrivial — it needs EP_functional-config wiring this header + // does not yet have, the same gap I2C's, ADC's, and PWM_IN's own + // handle_request comments defer for the identical reason); this + // returns LinErrc::config_write_not_supported rather than + // crashing, silently accepting the request as a transfer, or + // silently doing nothing. + std::error_code handle_request(uint8_t evt_op, std::vector out_bytes, + std::vector in_bytes, bool responded = true) { + switch (endpoint::evt_row2_kind_of(evt_op)) { + case endpoint::EvtRow2Kind::Plain: + return transfer(std::move(out_bytes), std::move(in_bytes), responded); + case endpoint::EvtRow2Kind::Reserved: + return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); + case endpoint::EvtRow2Kind::ConfigWrite: + return make_error_code(LinErrc::config_write_not_supported); + } + return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); // unreachable + } + const std::vector& last_sent() const noexcept { return last_out_; } const std::vector& last_received() const noexcept { return last_in_; } endpoint::TriggerRegistry& triggers() noexcept { return triggers_; } diff --git a/include/rcp/mock.hpp b/include/rcp/mock.hpp index 0a510b9..502daab 100644 --- a/include/rcp/mock.hpp +++ b/include/rcp/mock.hpp @@ -14,6 +14,8 @@ // fusa:req REQ-MOCK-014 // fusa:req REQ-MOCK-015 // fusa:req REQ-MOCK-016 +// fusa:req REQ-MOCK-017 +// fusa:req REQ-MOCK-018 // In-process RC Server simulator — a small, representative OPEN Alliance // TC18 Remote Control Protocol Specification v0.5.1_RC server built @@ -39,12 +41,12 @@ // 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 five fully-built endpoint types — rcp::gpio::GpioEndpoint and +// each of six fully-built endpoint types — rcp::gpio::GpioEndpoint and // rcp::spi::SpiEndpoint (both v2.3.0), plus rcp::i2c::I2cEndpoint, -// rcp::adc::AdcEndpoint, and rcp::pwm::PwmInEndpoint (v2.4.0, wired in by -// the Table 30/33 Row 2 evt[2:0] validation pilot and its ADC and PWM_IN -// follow-ups, in that order) — as its representative endpoint set. -// dispatch() below is the single +// rcp::adc::AdcEndpoint, rcp::pwm::PwmInEndpoint, and rcp::lin::LinEndpoint +// (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, and LIN follow-ups, in that order) +// — 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), @@ -65,14 +67,14 @@ // 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), 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, and rcp/pwm.hpp. +// 4, PWM_IN at 5, LIN at 6), 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, and rcp/lin.hpp. #pragma once #include @@ -82,6 +84,7 @@ #include #include #include +#include #include #include #include @@ -112,11 +115,13 @@ constexpr EndpointId kSpiEndpointId = 2; constexpr EndpointId kI2cEndpointId = 3; constexpr EndpointId kAdcEndpointId = 4; constexpr EndpointId kPwmInEndpointId = 5; +constexpr EndpointId kLinEndpointId = 6; constexpr avtp::ByteBusId kGpioByteBusId = static_cast(kGpioEndpointId); constexpr avtp::ByteBusId kSpiByteBusId = static_cast(kSpiEndpointId); constexpr avtp::ByteBusId kI2cByteBusId = static_cast(kI2cEndpointId); constexpr avtp::ByteBusId kAdcByteBusId = static_cast(kAdcEndpointId); constexpr avtp::ByteBusId kPwmInByteBusId = static_cast(kPwmInEndpointId); +constexpr avtp::ByteBusId kLinByteBusId = static_cast(kLinEndpointId); // A discovery-shaped EP0 read only ever answers with the register map's // magic number below — see this header's own scope note above. @@ -155,6 +160,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(pwm::PwmErrc::no_signal)) return acf::WireErrorCode::PwmInNoSignal; // Table 27's own dedicated PWM_IN_NO_SIGNAL(9) code — unlike adc::AdcErrc::no_signal above, TC18 defines a real numbered code for this condition, so this maps to it directly rather than falling back to EpError + if (ec == make_error_code(lin::LinErrc::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(lin::LinErrc::no_response)) + return acf::WireErrorCode::EpError; // internal no-response condition, not a TC18-defined LIN error code — mirrors i2c::I2cErrc::nack's mapping above return acf::WireErrorCode::UnsupportedCmd; } @@ -186,6 +195,7 @@ class Server final { i2c::I2cEndpoint& i2c() noexcept { return i2c_; } adc::AdcEndpoint& adc() noexcept { return adc_; } pwm::PwmInEndpoint& pwm_in() noexcept { return pwm_in_; } + lin::LinEndpoint& lin() noexcept { return lin_; } // set_spi_poci scripts the bytes a subsequent dispatch()/transfer() // call on `channel` reads back as POCI-in data. A real SPI peripheral's @@ -241,6 +251,19 @@ class Server final { pwm_in_.record_measurement(value); } + // set_lin_response scripts the bytes and responded/no-response outcome + // a subsequent dispatch()-driven LIN transfer reads back — the same + // "test scripts the bus, this mock does not model actual hardware" + // pattern set_spi_poci/set_i2c_response above already establish + // (LinEndpoint::transfer mirrors I2cEndpoint::transfer's shape, so this + // mirrors set_i2c_response's shape too). Applies to the next plain + // (evt[2:0]==000b) LIN request only in spirit — like set_i2c_response, + // it stays in effect until overwritten, there is no auto-clear. + void set_lin_response(std::vector data, bool responded = true) { + lin_response_ = std::move(data); + lin_responded_ = responded; + } + // 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 @@ -279,6 +302,7 @@ class Server final { if (req.byte_bus_id == kI2cByteBusId) return dispatch_i2c(req, req_payload, out_resp, out_resp_payload); if (req.byte_bus_id == kAdcByteBusId) return dispatch_adc(req, req_payload, out_resp, out_resp_payload); if (req.byte_bus_id == kPwmInByteBusId) return dispatch_pwm_in(req, req_payload, out_resp, out_resp_payload); + if (req.byte_bus_id == kLinByteBusId) return dispatch_lin(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); @@ -299,15 +323,16 @@ class Server final { static regmap::RegisterMap make_initial_register_map() { regmap::RegisterMap regs; - regs.endpoint_count = 5; - regs.generic_configs.resize(5); - regs.functional_configs.resize(5); + regs.endpoint_count = 6; + regs.generic_configs.resize(6); + regs.functional_configs.resize(6); regs.ep_id_mapping = { {kGpioEndpointId, kGpioByteBusId}, {kSpiEndpointId, kSpiByteBusId}, {kI2cEndpointId, kI2cByteBusId}, {kAdcEndpointId, kAdcByteBusId}, {kPwmInEndpointId, kPwmInByteBusId}, + {kLinEndpointId, kLinByteBusId}, }; return regs; } @@ -477,6 +502,30 @@ class Server final { return {}; } + // LIN, same as I2C (extraction §13.7.10.3), is a raw byte-stream + // transfer rather than a read/write-branched register — this dispatch + // path does not branch on req.op either, same rationale as + // dispatch_i2c's own comment. Unlike SPI, LIN's evt[2:0] does not + // select a channel; it is Table 33 Row 2's 3-way + // Plain/Reserved/ConfigWrite classification (rcp::endpoint:: + // evt_row2_kind_of), which LinEndpoint::handle_request checks before + // ever touching `payload` as transfer data — see its own doc comment + // for why a Reserved or ConfigWrite evt must never reach transfer(). + std::error_code dispatch_lin(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); + } + auto ec = lin_.handle_request(req.evt_op, payload, lin_response_, lin_responded_); + if (ec) return set_error_response(req, ec, out_resp, out_resp_payload); + out_resp_payload = lin_.last_received(); + 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_; @@ -485,10 +534,13 @@ class Server final { i2c::I2cEndpoint i2c_; adc::AdcEndpoint adc_; pwm::PwmInEndpoint pwm_in_; + lin::LinEndpoint lin_; 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; }; } // namespace mock diff --git a/tests/test_lin.cpp b/tests/test_lin.cpp index 5aca766..3d8633c 100644 --- a/tests/test_lin.cpp +++ b/tests/test_lin.cpp @@ -2,6 +2,8 @@ // fusa:test REQ-LINEP-002 // fusa:test REQ-LINEP-003 // fusa:test REQ-LINEP-004 +// fusa:test REQ-LINEP-005 +// fusa:test REQ-LINEP-006 // Tests for rcp/lin.hpp — the LIN commander endpoint type (ROADMAP.md // milestone 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN XL), @@ -73,6 +75,51 @@ TEST_CASE("LinEndpoint::transfer fires only TransferComplete on a normal respond REQUIRE(drained[0] == lin_signal_id(LinSignal::TransferComplete)); } +// ── Table 33 Row 2 evt[2:0] validation (handle_request) ───────────────────── + +TEST_CASE("LinEndpoint::handle_request delegates a Plain (evt[2:0]==000b) request to transfer()", + "[lin][REQ-LINEP-005]") { + LinEndpoint ep; + ep.triggers().enable(lin_signal_id(LinSignal::TransferComplete)); + + auto ec = ep.handle_request(/*evt_op=*/0, {0x55, 0x21}, {0xAA, 0xBB}); + REQUIRE_FALSE(ec); + REQUIRE(ep.last_sent() == std::vector{0x55, 0x21}); + REQUIRE(ep.last_received() == std::vector{0xAA, 0xBB}); + REQUIRE(ep.triggers().drain() == std::vector{ + lin_signal_id(LinSignal::TransferComplete)}); +} + +TEST_CASE("LinEndpoint::handle_request rejects every reserved evt[2:0] value (001b-110b)", + "[lin][REQ-LINEP-005]") { + for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { + LinEndpoint ep; + auto ec = ep.handle_request(evt_op, {0x55}, {0xAA}); + 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("LinEndpoint::handle_request reports config_write_not_supported for evt[2:0]==111b " + "without crashing or touching transfer state", + "[lin][REQ-LINEP-006]") { + LinEndpoint ep; + auto ec = ep.handle_request(/*evt_op=*/7, {0x00, 0xAB}, {}); + REQUIRE(ec == make_error_code(LinErrc::config_write_not_supported)); + REQUIRE(ep.last_sent().empty()); + REQUIRE(ep.last_received().empty()); +} + +TEST_CASE("LinEndpoint::handle_request masks evt_op down to 3 bits before classifying", + "[lin][REQ-LINEP-005]") { + LinEndpoint ep; + REQUIRE_FALSE(ep.handle_request(/*evt_op=*/0xF8, {0x55}, {0xAA})); // low 3 bits 000 -> Plain + auto ec = ep.handle_request(/*evt_op=*/0xF9, {0x55}, {0xAA}); // low 3 bits 001 -> Reserved + REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); +} + // ── LinErrc category sanity ─────────────────────────────────────────────────── TEST_CASE("LinErrc reports a non-empty message in its own category", "[lin][REQ-LINEP-004]") { @@ -80,3 +127,10 @@ TEST_CASE("LinErrc reports a non-empty message in its own category", "[lin][REQ- REQUIRE(ec.category() == lin_category()); REQUIRE_FALSE(ec.message().empty()); } + +TEST_CASE("LinErrc::config_write_not_supported reports a non-empty message in its own category", + "[lin][REQ-LINEP-006]") { + auto ec = make_error_code(LinErrc::config_write_not_supported); + REQUIRE(ec.category() == lin_category()); + REQUIRE_FALSE(ec.message().empty()); +} diff --git a/tests/test_mock.cpp b/tests/test_mock.cpp index 72bb369..7ca03df 100644 --- a/tests/test_mock.cpp +++ b/tests/test_mock.cpp @@ -14,6 +14,8 @@ // fusa:test REQ-MOCK-014 // fusa:test REQ-MOCK-015 // fusa:test REQ-MOCK-016 +// fusa:test REQ-MOCK-017 +// fusa:test REQ-MOCK-018 // Tests for rcp/mock.hpp — the in-process RC Server simulator (ROADMAP.md // milestone 56, "Test & Simulation Harness Rebuild", v2.12.0). See @@ -40,14 +42,14 @@ acf::AcfMessageInfo standard_request(avtp::ByteBusId bus_id, bool write, uint8_t // ── Construction / register map ────────────────────────────────────────────── -TEST_CASE("Server starts HW_UNCONFIGURED with a five-endpoint register map", +TEST_CASE("Server starts HW_UNCONFIGURED with a six-endpoint register map", "[mock][REQ-MOCK-001]") { mock::Server server; REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); - REQUIRE(server.registers().endpoint_count == 5); - REQUIRE(server.registers().generic_configs.size() == 5); - REQUIRE(server.registers().functional_configs.size() == 5); - REQUIRE(server.registers().ep_id_mapping.size() == 5); + REQUIRE(server.registers().endpoint_count == 6); + REQUIRE(server.registers().generic_configs.size() == 6); + REQUIRE(server.registers().functional_configs.size() == 6); + REQUIRE(server.registers().ep_id_mapping.size() == 6); 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); @@ -58,6 +60,8 @@ TEST_CASE("Server starts HW_UNCONFIGURED with a five-endpoint register map", REQUIRE(server.registers().ep_id_mapping[3].byte_bus_id == mock::kAdcByteBusId); REQUIRE(server.registers().ep_id_mapping[4].ep_id == mock::kPwmInEndpointId); REQUIRE(server.registers().ep_id_mapping[4].byte_bus_id == mock::kPwmInByteBusId); + REQUIRE(server.registers().ep_id_mapping[5].ep_id == mock::kLinEndpointId); + REQUIRE(server.registers().ep_id_mapping[5].byte_bus_id == mock::kLinByteBusId); } TEST_CASE("advance_to_rcp_configured drives the lifecycle straight to RCP_CONFIGURED", @@ -477,6 +481,76 @@ TEST_CASE("PWM_IN request is rejected before RCP_CONFIGURED, same operational ga REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); } +// ── LIN ─────────────────────────────────────────────────────────────────────── +// Table 30/33 Row 2 evt[2:0] validation, fourth endpoint type after I2C, +// ADC, and PWM_IN: LinEndpoint wired into dispatch() at byte_bus_id 6. + +TEST_CASE("LIN plain request (evt[2:0]==000b) answers with the scripted response bytes", + "[mock][REQ-MOCK-017]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + server.set_lin_response({0xAA, 0xBB}); + + auto req = standard_request(mock::kLinByteBusId, /*write=*/true, /*evt_op=*/0); + std::vector out_bytes{0x55, 0x21}; + acf::AcfMessageInfo resp; + std::vector resp_payload; + REQUIRE_FALSE(server.dispatch(0, req, out_bytes, resp, resp_payload)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ReadResponse); + REQUIRE(resp_payload == std::vector{0xAA, 0xBB}); + REQUIRE(server.lin().last_sent() == out_bytes); +} + +TEST_CASE("LIN 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-017]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { + auto req = standard_request(mock::kLinByteBusId, /*write=*/true, evt_op); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {0x55}, 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.lin().last_sent().empty()); + } +} + +TEST_CASE("LIN 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 transfer", + "[mock][REQ-MOCK-017]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + auto req = standard_request(mock::kLinByteBusId, /*write=*/true, /*evt_op=*/7); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {0x00, 0xAB}, resp, resp_payload); + REQUIRE(ec == lin::make_error_code(lin::LinErrc::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.lin().last_sent().empty()); +} + +TEST_CASE("LIN request is rejected before RCP_CONFIGURED, same operational gating as GPIO/SPI/I2C/ADC/PWM_IN", + "[mock][REQ-MOCK-018]") { + mock::Server server; + REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); + + auto req = standard_request(mock::kLinByteBusId, /*write=*/true, /*evt_op=*/0); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {0x55}, 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",