diff --git a/.fusa-reqs.json b/.fusa-reqs.json index dfad576..c214b6d 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -196,6 +196,24 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-MOCK-015", + "title": "PWM_IN dispatch applies Table 33 Row 2 evt[2:0] classification before touching the scripted measurement", + "text": "rcp::mock::Server::dispatch, for a request addressed to the PWM_IN endpoint while RcpConfigured, shall decode the request's evt[2:0] field via endpoint::evt_row2_kind_of and drive pwm::PwmInEndpoint::handle_request with it: a Plain (000b) request shall be answered as a ReadResponse carrying the 4-byte big-endian encoding (pwm::encode_pwm_payload) of the value set_pwm_in_response last scripted (or PwmErrc::no_signal / wire error code PwmInNoSignal if none has been scripted yet), a Reserved (001b-110b) evt shall be rejected with endpoint::EndpointErrc::reserved_evt_row2 (wire error code UNSUPPORTED_CMD) without touching the scripted measurement, and a ConfigWrite (111b) evt shall be rejected with pwm::PwmErrc::config_write_not_supported (wire error code UNSUPPORTED_CMD) rather than crashing or being treated as a plain read.", + "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; PWM_IN's own request-handling section explicitly defers to this same rule: §13.7.6.3 (\"The interpretation of the evt-bits is described in the section 'The usage of evt bits in requests'\"), TC18.txt L5168-5180", + "tc18_master_id": "TC18-13.5-001" + }, + { + "id": "REQ-MOCK-016", + "title": "Operational PWM_IN requests are rejected before RCP_CONFIGURED, same gating as GPIO/SPI/I2C/ADC", + "text": "rcp::mock::Server::dispatch, for a request addressed to the PWM_IN endpoint while the server's lifecycle state is not RcpConfigured, shall return regmap::RegMapErrc::request_rejected and an ErrorResponse without touching the scripted measurement or otherwise touching the endpoint's live state, the same operational-request gating REQ-MOCK-006/REQ-MOCK-012/REQ-MOCK-014 already apply to GPIO/SPI/I2C/ADC.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, { "id": "REQ-UDP-001", "title": "Frame round-trips an NTSCF-framed ACF_ABB request", @@ -2964,6 +2982,27 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-PWM-008", + "title": "PwmInEndpoint::handle_request applies Table 33 Row 2 evt[2:0] classification before reading", + "text": "rcp::pwm::PwmInEndpoint::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 handle_read() with out_value unchanged; for EvtRow2Kind::Reserved, shall return endpoint::EndpointErrc::reserved_evt_row2 without touching out_value or any measured state.", + "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; PWM_IN's own request-handling section explicitly defers to this same rule rather than redescribing it: §13.7.6.3 (\"The interpretation of the evt-bits is described in the section 'The usage of evt bits in requests'\"), TC18.txt L5168-5180; 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-PWM-009", + "title": "PwmInEndpoint::handle_request reports config-write (evt[2:0]==111b) as not yet supported", + "text": "rcp::pwm::PwmInEndpoint::handle_request, for EvtRow2Kind::ConfigWrite (evt[2:0]==111b), shall return PwmErrc::config_write_not_supported without touching out_value or any measured state and without crashing, rather than silently accepting the request as an ordinary read 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 and ADC's AdcErrc::config_write_not_supported already made. PwmErrc::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-SEQ-001", "title": "message_timestamp-repurposing request_type decode/encode (mtv=0)", diff --git a/include/rcp/mock.hpp b/include/rcp/mock.hpp index 2b47921..0a510b9 100644 --- a/include/rcp/mock.hpp +++ b/include/rcp/mock.hpp @@ -12,6 +12,8 @@ // fusa:req REQ-MOCK-012 // fusa:req REQ-MOCK-013 // fusa:req REQ-MOCK-014 +// fusa:req REQ-MOCK-015 +// fusa:req REQ-MOCK-016 // In-process RC Server simulator — a small, representative OPEN Alliance // TC18 Remote Control Protocol Specification v0.5.1_RC server built @@ -37,11 +39,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 four fully-built endpoint types — rcp::gpio::GpioEndpoint and -// rcp::spi::SpiEndpoint (both v2.3.0), plus rcp::i2c::I2cEndpoint and -// rcp::adc::AdcEndpoint (v2.4.0, wired in by the Table 30/33 Row 2 evt[2:0] -// validation pilot and its ADC follow-up, in that order) — as its -// representative endpoint set. dispatch() below is the single +// each of five 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 // 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), @@ -62,14 +65,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), 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 +// 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, -// and rcp/adc.hpp. +// rcp/adc.hpp, and rcp/pwm.hpp. #pragma once #include @@ -79,6 +82,7 @@ #include #include #include +#include #include #include @@ -103,14 +107,16 @@ namespace mock { using regmap::EndpointId; -constexpr EndpointId kGpioEndpointId = 1; -constexpr EndpointId kSpiEndpointId = 2; -constexpr EndpointId kI2cEndpointId = 3; -constexpr EndpointId kAdcEndpointId = 4; -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 EndpointId kGpioEndpointId = 1; +constexpr EndpointId kSpiEndpointId = 2; +constexpr EndpointId kI2cEndpointId = 3; +constexpr EndpointId kAdcEndpointId = 4; +constexpr EndpointId kPwmInEndpointId = 5; +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); // A discovery-shaped EP0 read only ever answers with the register map's // magic number below — see this header's own scope note above. @@ -145,6 +151,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(adc::AdcErrc::no_signal)) return acf::WireErrorCode::EpError; // internal no-valid-sample condition, not a TC18-defined ADC error code (see adc.hpp's own comment) — mirrors i2c::I2cErrc::nack's mapping above + if (ec == make_error_code(pwm::PwmErrc::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(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 return acf::WireErrorCode::UnsupportedCmd; } @@ -175,6 +185,7 @@ class Server final { spi::SpiEndpoint& spi() noexcept { return spi_; } i2c::I2cEndpoint& i2c() noexcept { return i2c_; } adc::AdcEndpoint& adc() noexcept { return adc_; } + pwm::PwmInEndpoint& pwm_in() noexcept { return pwm_in_; } // set_spi_poci scripts the bytes a subsequent dispatch()/transfer() // call on `channel` reads back as POCI-in data. A real SPI peripheral's @@ -212,6 +223,24 @@ class Server final { adc_samples_.assign(samples.begin(), samples.end()); } + // set_pwm_in_response scripts the PwmValue (period, active_duration) a + // subsequent dispatch()-driven plain (evt[2:0]==000b) PWM_IN request + // answers with — the same "test scripts the sensor, this mock does not + // model actual hardware" pattern set_spi_poci/set_i2c_response/ + // set_adc_response above already establish. Unlike set_adc_response's + // FIFO queue, this calls pwm::PwmInEndpoint::record_measurement + // directly: PWM_IN's own read model (rcp/pwm.hpp) reports the most + // recently recorded measurement persistently, not a one-shot consumed + // sample the way ADC's take_sample callback models, so there is + // nothing to queue — the scripted value stays in effect (and fires + // both Table 44 trigger signals, per record_measurement's own + // contract) until this is called again, or pwm_in().clear_signal() is + // used to model signal loss (a subsequent plain request then answers + // PwmErrc::no_signal / wire error code PWM_IN_NO_SIGNAL again). + void set_pwm_in_response(pwm::PwmValue value) { + pwm_in_.record_measurement(value); + } + // 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 @@ -249,6 +278,7 @@ class Server final { if (req.byte_bus_id == kSpiByteBusId) return dispatch_spi(req, req_payload, out_resp, out_resp_payload); 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); return set_error_response(req, make_error_code(regmap::RegMapErrc::invalid_parameter), out_resp, out_resp_payload); @@ -269,14 +299,15 @@ class Server final { static regmap::RegisterMap make_initial_register_map() { regmap::RegisterMap regs; - regs.endpoint_count = 4; - regs.generic_configs.resize(4); - regs.functional_configs.resize(4); + regs.endpoint_count = 5; + regs.generic_configs.resize(5); + regs.functional_configs.resize(5); regs.ep_id_mapping = { - {kGpioEndpointId, kGpioByteBusId}, - {kSpiEndpointId, kSpiByteBusId}, - {kI2cEndpointId, kI2cByteBusId}, - {kAdcEndpointId, kAdcByteBusId}, + {kGpioEndpointId, kGpioByteBusId}, + {kSpiEndpointId, kSpiByteBusId}, + {kI2cEndpointId, kI2cByteBusId}, + {kAdcEndpointId, kAdcByteBusId}, + {kPwmInEndpointId, kPwmInByteBusId}, }; return regs; } @@ -412,6 +443,40 @@ class Server final { return {}; } + // PWM_IN is a response-only read model (§13.7.6.3, rcp/pwm.hpp's own + // header comment) with no functional request payload at all — this + // dispatch path does not branch on req.op either, same rationale as + // dispatch_adc's own comment (no write semantics implemented for this + // endpoint type, not an inherently full-duplex protocol). Every + // request addressed here drives one PwmInEndpoint::handle_request, + // which checks Table 33 Row 2's 3-way Plain/Reserved/ConfigWrite + // classification (rcp::endpoint::evt_row2_kind_of) before ever + // touching the endpoint's last-measured state — so a Reserved or + // ConfigWrite evt can never read or disturb it. Unlike dispatch_adc, + // there is no scripted-sample queue to consume: PWM_IN's read model + // reports the most recently recorded measurement persistently (see + // set_pwm_in_response's own comment), so a Plain request simply + // answers whatever is currently recorded, or PwmErrc::no_signal + // (wire error code PwmInNoSignal) if nothing has been recorded yet or + // the signal was cleared. `payload` is unused: §13.7.6.3 states the + // PWM_IN request itself carries no functional byte_msg_payload. + std::error_code dispatch_pwm_in(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); + } + pwm::PwmValue value{}; + auto ec = pwm_in_.handle_request(req.evt_op, value); + if (ec) return set_error_response(req, ec, out_resp, out_resp_payload); + auto wire = pwm::encode_pwm_payload(value); + out_resp_payload.assign(wire.begin(), wire.end()); + 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_; @@ -419,6 +484,7 @@ class Server final { spi::SpiEndpoint spi_; i2c::I2cEndpoint i2c_; adc::AdcEndpoint adc_; + pwm::PwmInEndpoint pwm_in_; std::array, spi::kMaxChannels> spi_poci_{}; std::vector i2c_response_{}; bool i2c_acked_ = true; diff --git a/include/rcp/pwm.hpp b/include/rcp/pwm.hpp index 3dc0dd7..dd72283 100644 --- a/include/rcp/pwm.hpp +++ b/include/rcp/pwm.hpp @@ -5,6 +5,8 @@ // fusa:req REQ-PWM-005 // fusa:req REQ-PWM-006 // fusa:req REQ-PWM-007 +// fusa:req REQ-PWM-008 +// fusa:req REQ-PWM-009 // PWM_OUT (ep_type 0x07) and PWM_IN (ep_type 0x08) endpoints — the shared // period/active-duration two-field payload shape, PWM_OUT's fixed 4-byte @@ -58,6 +60,32 @@ // separate, larger gap common to every endpoint but one in this codebase, // not specific to PWM_OUT's write-semantics bug this fixes). // +// Table 30/33 Row 2 evt[2:0] validation (post-v2.4.0, third endpoint type +// after I2C and ADC): PwmInEndpoint::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 PWM_IN's request decode, following the exact shape +// rcp/i2c.hpp's I2cEndpoint::handle_request and rcp/adc.hpp's +// AdcEndpoint::handle_request established. PWM_IN's own request-handling +// section (§13.7.6.3) does not redescribe evt-bit semantics itself — it +// says only "The interpretation of the evt-bits is described in the +// section 'The usage of evt bits in requests'", i.e. §13.5 Table 33 — so +// this milestone's classification is not a new spec fact, just PWM_IN's +// own wiring of the rule Table 33 already states. Plain (evt[2:0]==000b) +// delegates to handle_read (PWM_IN's existing response-only read model +// above, unchanged by this milestone): the most recently recorded +// measurement, or PwmErrc::no_signal if none has ever been captured (or +// the signal was subsequently lost). Reserved (001b-110b) is rejected +// with endpoint::EndpointErrc::reserved_evt_row2 without touching +// `out_value`. ConfigWrite (evt[2:0]==111b, §12.7.1) is reported as +// PwmErrc::config_write_not_supported rather than crashing or silently +// accepted as a plain read — PWM_IN has no EP_functional-config wiring in +// this codebase yet (same gap I2C's and ADC's own handle_request comments +// call out for their own endpoint types), so full §12.7.1 handling is out +// of scope here too. PWM_OUT's own write path (PwmOutEndpoint:: +// handle_write, GPIO/PWM_OUT's Table 33 row) is unrelated and untouched by +// this milestone. +// // 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 period/duration @@ -65,7 +93,7 @@ // rcp/regmap.hpp's own field-width disclaimers) and trigger-signal id // scheme 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/gpio.hpp. +// rcp/gpio.hpp, rcp/i2c.hpp, and rcp/adc.hpp. #pragma once #include @@ -123,6 +151,15 @@ inline std::error_code decode_pwm_payload(const uint8_t* buf, size_t len, PwmVal enum class PwmErrc : int { no_signal = 1, // PWM_IN_NO_SIGNAL: no pulse has been measured yet + // evt_row2_kind_of classified the request as ConfigWrite (evt[2:0] == + // 111b, §12.7.1). This milestone deliberately does not implement the + // configuration-write shape (relative EP_functional-config start + // address + configuration data) — see PwmInEndpoint::handle_request's + // own comment. Reported explicitly rather than silently accepted as a + // plain read or silently ignored, same as I2C's + // I2cErrc::config_write_not_supported and ADC's + // AdcErrc::config_write_not_supported. + config_write_not_supported = 2, }; inline const std::error_category& pwm_category() noexcept { @@ -131,7 +168,9 @@ inline const std::error_category& pwm_category() noexcept { std::string message(int ev) const override { switch (static_cast(ev)) { case PwmErrc::no_signal: return "rcp/pwm: PWM_IN_NO_SIGNAL — no pulse measured"; - default: return "rcp/pwm: unknown error"; + case PwmErrc::config_write_not_supported: + return "rcp/pwm: evt[2:0]=111b configuration-write requests are not yet implemented"; + default: return "rcp/pwm: unknown error"; } } }; @@ -255,6 +294,48 @@ class PwmInEndpoint { return {}; } + // handle_request is PWM_IN's request-decode entry point — the piece + // this header previously had none of, mirroring rcp::i2c::I2cEndpoint:: + // handle_request's and rcp::adc::AdcEndpoint::handle_request's shape + // exactly (this repo's third Table 33 Row 2 endpoint type after I2C + // and ADC). 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 handle_read and be misread as an + // ordinary read, and a ConfigWrite value can never be silently + // accepted or silently dropped: + // - Plain (evt[2:0] == 000b): delegates straight to handle_read() + // with `out_value` unchanged — PWM_IN's existing response-only + // read model above already IS this row's correct "plain request" + // behavior (§13.7.6.3: "The interpretation of the evt-bits is + // described in the section 'The usage of evt bits in requests'", + // i.e. §13.5 Table 33). + // - Reserved (evt[2:0] in 001b-110b): returns + // endpoint::EndpointErrc::reserved_evt_row2 without touching + // `out_value` or any measured state — TC18 requires this be + // rejected with error code UNSUPPORTED_CMD. + // - ConfigWrite (evt[2:0] == 111b): §12.7.1's configuration-write + // shape targets the PWM_IN EP's own functional-config block + // (relative start address + configuration data), not a + // measurement read 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 and ADC's own handle_request comments defer for + // the identical reason); this returns + // PwmErrc::config_write_not_supported rather than crashing, + // silently accepting the request as a read, or silently doing + // nothing. + std::error_code handle_request(uint8_t evt_op, PwmValue& out_value) { + switch (endpoint::evt_row2_kind_of(evt_op)) { + case endpoint::EvtRow2Kind::Plain: + return handle_read(out_value); + case endpoint::EvtRow2Kind::Reserved: + return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); + case endpoint::EvtRow2Kind::ConfigWrite: + return make_error_code(PwmErrc::config_write_not_supported); + } + return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); // unreachable + } + endpoint::TriggerRegistry& triggers() noexcept { return triggers_; } private: diff --git a/tests/test_mock.cpp b/tests/test_mock.cpp index d7a7020..72bb369 100644 --- a/tests/test_mock.cpp +++ b/tests/test_mock.cpp @@ -12,6 +12,8 @@ // fusa:test REQ-MOCK-012 // fusa:test REQ-MOCK-013 // fusa:test REQ-MOCK-014 +// fusa:test REQ-MOCK-015 +// fusa:test REQ-MOCK-016 // Tests for rcp/mock.hpp — the in-process RC Server simulator (ROADMAP.md // milestone 56, "Test & Simulation Harness Rebuild", v2.12.0). See @@ -38,14 +40,14 @@ acf::AcfMessageInfo standard_request(avtp::ByteBusId bus_id, bool write, uint8_t // ── Construction / register map ────────────────────────────────────────────── -TEST_CASE("Server starts HW_UNCONFIGURED with a four-endpoint register map", +TEST_CASE("Server starts HW_UNCONFIGURED with a five-endpoint register map", "[mock][REQ-MOCK-001]") { mock::Server server; REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); - REQUIRE(server.registers().endpoint_count == 4); - REQUIRE(server.registers().generic_configs.size() == 4); - REQUIRE(server.registers().functional_configs.size() == 4); - REQUIRE(server.registers().ep_id_mapping.size() == 4); + 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().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); @@ -54,6 +56,8 @@ TEST_CASE("Server starts HW_UNCONFIGURED with a four-endpoint register map", REQUIRE(server.registers().ep_id_mapping[2].byte_bus_id == mock::kI2cByteBusId); REQUIRE(server.registers().ep_id_mapping[3].ep_id == mock::kAdcEndpointId); 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); } TEST_CASE("advance_to_rcp_configured drives the lifecycle straight to RCP_CONFIGURED", @@ -364,6 +368,115 @@ TEST_CASE("ADC request is rejected before RCP_CONFIGURED, same operational gatin REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); } +// ── PWM_IN ──────────────────────────────────────────────────────────────────── +// Table 30/33 Row 2 evt[2:0] validation, third endpoint type after I2C and +// ADC: PwmInEndpoint wired into dispatch() at byte_bus_id 5. + +TEST_CASE("PWM_IN plain request (evt[2:0]==000b) answers with the scripted measurement", + "[mock][REQ-MOCK-015]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + server.set_pwm_in_response({/*period=*/0x1234, /*active_duration=*/0x0056}); + + auto req = standard_request(mock::kPwmInByteBusId, /*write=*/false, /*evt_op=*/0); + acf::AcfMessageInfo resp; + std::vector resp_payload; + REQUIRE_FALSE(server.dispatch(0, req, {}, resp, resp_payload)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ReadResponse); + REQUIRE(resp_payload == std::vector{0x12, 0x34, 0x00, 0x56}); +} + +TEST_CASE("PWM_IN plain requests keep answering the same scripted measurement (not consumed)", + "[mock][REQ-MOCK-015]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + server.set_pwm_in_response({100, 50}); + + auto req = standard_request(mock::kPwmInByteBusId, /*write=*/false, /*evt_op=*/0); + acf::AcfMessageInfo resp; + std::vector resp_payload; + + REQUIRE_FALSE(server.dispatch(0, req, {}, resp, resp_payload)); + REQUIRE(resp_payload == std::vector{0x00, 0x64, 0x00, 0x32}); + + // A second dispatch against the same scripted value answers identically + // — unlike ADC's FIFO queue, PWM_IN's read model is not consume-once. + REQUIRE_FALSE(server.dispatch(0, req, {}, resp, resp_payload)); + REQUIRE(resp_payload == std::vector{0x00, 0x64, 0x00, 0x32}); +} + +TEST_CASE("PWM_IN plain request reports PwmInNoSignal before any measurement is scripted", + "[mock][REQ-MOCK-015]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + auto req = standard_request(mock::kPwmInByteBusId, /*write=*/false, /*evt_op=*/0); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {}, resp, resp_payload); + REQUIRE(ec == pwm::make_error_code(pwm::PwmErrc::no_signal)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); + REQUIRE(resp_payload == + std::vector{static_cast(acf::WireErrorCode::PwmInNoSignal)}); +} + +TEST_CASE("PWM_IN request with a reserved evt[2:0] (001b-110b) is rejected with wire error code " + "UNSUPPORTED_CMD and does not disturb the scripted measurement", + "[mock][REQ-MOCK-015]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + server.set_pwm_in_response({200, 75}); + + for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { + auto req = standard_request(mock::kPwmInByteBusId, /*write=*/false, evt_op); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {}, 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)}); + } + + // The scripted measurement must still be there — no reserved evt disturbed it. + auto req = standard_request(mock::kPwmInByteBusId, /*write=*/false, /*evt_op=*/0); + acf::AcfMessageInfo resp; + std::vector resp_payload; + REQUIRE_FALSE(server.dispatch(0, req, {}, resp, resp_payload)); + REQUIRE(resp_payload == std::vector{0x00, 0xC8, 0x00, 0x4B}); +} + +TEST_CASE("PWM_IN 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 read", + "[mock][REQ-MOCK-015]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + auto req = standard_request(mock::kPwmInByteBusId, /*write=*/false, /*evt_op=*/7); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {}, resp, resp_payload); + REQUIRE(ec == pwm::make_error_code(pwm::PwmErrc::config_write_not_supported)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); + REQUIRE(resp_payload == + std::vector{static_cast(acf::WireErrorCode::UnsupportedCmd)}); +} + +TEST_CASE("PWM_IN request is rejected before RCP_CONFIGURED, same operational gating as GPIO/SPI/I2C/ADC", + "[mock][REQ-MOCK-016]") { + mock::Server server; + REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); + + auto req = standard_request(mock::kPwmInByteBusId, /*write=*/false, /*evt_op=*/0); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {}, 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", diff --git a/tests/test_pwm.cpp b/tests/test_pwm.cpp index 91b19b6..e60a43c 100644 --- a/tests/test_pwm.cpp +++ b/tests/test_pwm.cpp @@ -5,6 +5,8 @@ // fusa:test REQ-PWM-005 // fusa:test REQ-PWM-006 // fusa:test REQ-PWM-007 +// fusa:test REQ-PWM-008 +// fusa:test REQ-PWM-009 // Tests for rcp/pwm.hpp — the PWM_OUT and PWM_IN endpoint types // (ROADMAP.md milestone 48, "Basic Endpoint Types II — I2C, UART, ADC, @@ -237,6 +239,79 @@ TEST_CASE("PwmInEndpoint::record_edge fires exactly the requested edge", "[pwm][ REQUIRE(drained[0] == pwm_in_signal_id(PwmInSignal::FallingEdge)); } +// ── Table 33 Row 2 evt[2:0] validation (handle_request) ───────────────────── +// Table 33 Row 2's shared evt[2:0] classification (Plain/Reserved/ +// ConfigWrite, exercised for all 8 evt_op values by +// tests/test_endpoint.cpp's own "evt_row2_kind_of classifies all 8 evt[2:0] +// values" case) applied to PWM_IN's own request-decode entry point, +// mirroring tests/test_i2c.cpp's and tests/test_adc.cpp's "Table 33 Row 2 +// evt[2:0] validation (handle_request)" sections exactly — see +// rcp/pwm.hpp's own PwmInEndpoint::handle_request comment. + +TEST_CASE("PwmInEndpoint::handle_request delegates a Plain (evt[2:0]==000b) request to " + "handle_read()", + "[pwm][REQ-PWM-008]") { + PwmInEndpoint ep; + ep.record_measurement({200, 75}); + + PwmValue out; + auto ec = ep.handle_request(/*evt_op=*/0, out); + REQUIRE_FALSE(ec); + REQUIRE(out.period == 200); + REQUIRE(out.active_duration == 75); +} + +TEST_CASE("PwmInEndpoint::handle_request Plain surfaces no_signal before any measurement, " + "same as handle_read()", + "[pwm][REQ-PWM-008]") { + PwmInEndpoint ep; + PwmValue out; + auto ec = ep.handle_request(/*evt_op=*/0, out); + REQUIRE(ec == make_error_code(PwmErrc::no_signal)); +} + +TEST_CASE("PwmInEndpoint::handle_request rejects every reserved evt[2:0] value (001b-110b) " + "without touching out_value", + "[pwm][REQ-PWM-008]") { + for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { + PwmInEndpoint ep; + ep.record_measurement({111, 22}); + + PwmValue out; + auto ec = ep.handle_request(evt_op, out); + REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); + REQUIRE(out.period == 0); // untouched — still default-constructed + REQUIRE(out.active_duration == 0); + } +} + +TEST_CASE("PwmInEndpoint::handle_request reports config_write_not_supported for evt[2:0]==111b " + "without touching out_value", + "[pwm][REQ-PWM-009]") { + PwmInEndpoint ep; + ep.record_measurement({111, 22}); + + PwmValue out; + auto ec = ep.handle_request(/*evt_op=*/7, out); + REQUIRE(ec == make_error_code(PwmErrc::config_write_not_supported)); + REQUIRE(out.period == 0); + REQUIRE(out.active_duration == 0); +} + +TEST_CASE("PwmInEndpoint::handle_request masks evt_op down to 3 bits before classifying", + "[pwm][REQ-PWM-008]") { + PwmInEndpoint ep; + ep.record_measurement({9, 4}); + + PwmValue out; + REQUIRE_FALSE(ep.handle_request(/*evt_op=*/0xF8, out)); // low 3 bits 000 -> Plain + REQUIRE(out.period == 9); + REQUIRE(out.active_duration == 4); + + auto ec = ep.handle_request(/*evt_op=*/0xF9, out); // low 3 bits 001 -> Reserved + REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); +} + // ── PwmErrc category sanity ─────────────────────────────────────────────────── TEST_CASE("PwmErrc reports a non-empty message in its own category", "[pwm][REQ-PWM-007]") { @@ -244,3 +319,10 @@ TEST_CASE("PwmErrc reports a non-empty message in its own category", "[pwm][REQ- REQUIRE(ec.category() == pwm_category()); REQUIRE_FALSE(ec.message().empty()); } + +TEST_CASE("PwmErrc::config_write_not_supported reports a non-empty message in its own category", + "[pwm][REQ-PWM-009]") { + auto ec = make_error_code(PwmErrc::config_write_not_supported); + REQUIRE(ec.category() == pwm_category()); + REQUIRE_FALSE(ec.message().empty()); +}