diff --git a/.fusa-reqs.json b/.fusa-reqs.json index c792d59..dfad576 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 three-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 three endpoints: GPIO at endpoint id/byte_bus_id 1, SPI at endpoint id/byte_bus_id 2, and I2C at endpoint id/byte_bus_id 3.", + "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.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -178,6 +178,24 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-MOCK-013", + "title": "ADC dispatch applies Table 33 Row 2 evt[2:0] classification before consuming a scripted sample", + "text": "rcp::mock::Server::dispatch, for a request addressed to the ADC endpoint while RcpConfigured, shall decode the request's evt[2:0] field via endpoint::evt_row2_kind_of and drive adc::AdcEndpoint::handle_request with it against a default-constructed adc::AdcAveragingConfig: a Plain (000b) request shall be answered as a ReadResponse carrying the 16-bit big-endian encoding (adc::encode_adc_value) of the next value set_adc_response last scripted, consumed in FIFO order without auto-refill (an exhausted scripted queue reports adc::AdcErrc::no_signal), a Reserved (001b-110b) evt shall be rejected with endpoint::EndpointErrc::reserved_evt_row2 (wire error code UNSUPPORTED_CMD) without consuming a scripted sample, and a ConfigWrite (111b) evt shall be rejected with adc::AdcErrc::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; 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-014", + "title": "Operational ADC requests are rejected before RCP_CONFIGURED, same gating as GPIO/SPI/I2C", + "text": "rcp::mock::Server::dispatch, for a request addressed to the ADC endpoint while the server's lifecycle state is not RcpConfigured, shall return regmap::RegMapErrc::request_rejected and an ErrorResponse without consuming a scripted sample or otherwise touching the endpoint's live state, the same operational-request gating REQ-MOCK-006/REQ-MOCK-012 already apply to GPIO/SPI/I2C.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, { "id": "REQ-UDP-001", "title": "Frame round-trips an NTSCF-framed ACF_ABB request", @@ -2847,6 +2865,37 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-ADC-007", + "title": "encode_adc_value encodes a single measurement as a 16-bit big-endian wire value", + "text": "rcp::adc::encode_adc_value shall encode its uint16_t argument as exactly rcp::adc::kAdcValueLen (2) bytes, big-endian, matching the \"16 bit ADC value\" field TC18's ADC response figures show.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.7.9.3 (\"16 bit ADC value 0\"/\"16 bit ADC value 1\" fields, Figure 34/Figure 35 - ADC response with 8 measurements), TC18.txt L5590-5628", + "tc18_master_id": "TC18-13.7.9.3-006" + }, + { + "id": "REQ-ADC-008", + "title": "AdcEndpoint::handle_request applies Table 33 Row 2 evt[2:0] classification before sampling", + "text": "rcp::adc::AdcEndpoint::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 request_reading() with cfg/take_sample/out_value unchanged; for EvtRow2Kind::Reserved, shall return endpoint::EndpointErrc::reserved_evt_row2 without invoking take_sample or otherwise touching out_value.", + "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-ADC-009", + "title": "AdcEndpoint::handle_request reports config-write (evt[2:0]==111b) as not yet supported", + "text": "rcp::adc::AdcEndpoint::handle_request, for EvtRow2Kind::ConfigWrite (evt[2:0]==111b), shall return AdcErrc::config_write_not_supported without invoking take_sample or otherwise touching out_value 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 already made. AdcErrc::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-PWM-001", "title": "PWM_OUT/PWM_IN shared period/active-duration payload shape", diff --git a/include/rcp/adc.hpp b/include/rcp/adc.hpp index a36ed2a..a0857a7 100644 --- a/include/rcp/adc.hpp +++ b/include/rcp/adc.hpp @@ -4,6 +4,9 @@ // fusa:req REQ-ADC-004 // fusa:req REQ-ADC-005 // fusa:req REQ-ADC-006 +// fusa:req REQ-ADC-007 +// fusa:req REQ-ADC-008 +// fusa:req REQ-ADC-009 // ADC endpoint (ep_type 0x09) — the three-level averaging model // (adc_sample_interval -> adc_avg_intervals_per_request -> @@ -46,19 +49,46 @@ // cannot produce a valid sample) without pretending to be, or to map to, // any numbered TC18 error code. // +// Table 30/33 Row 2 evt[2:0] validation (post-v2.4.0, second endpoint type +// after I2C): AdcEndpoint::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 +// ADC's request decode, following the exact shape rcp/i2c.hpp's +// I2cEndpoint::handle_request established as the pilot. ADC is in fact the +// row's own worked example (§13.7.9.3 Figure 34, "RC Client sends a standard +// read request", evt field all-zero) that resolved the row's evt=000b +// textual ambiguity in the first place — see rcp/endpoint.hpp's own "RESOLVED +// AMBIGUITY" comment on evt_row2_kind_of, which cites this exact section. +// Plain (evt[2:0]==000b) delegates to request_reading — the SelfTimed +// cadence pattern below, ADC's on-demand "standard read request" behavior, +// matching Figure 34 — not to request_reading_from_trigger_queue, whose +// ExternalTrigger cadence remains a separate entry point this milestone does +// not fold into handle_request. Reserved (001b-110b) is rejected with +// endpoint::EndpointErrc::reserved_evt_row2. ConfigWrite (evt[2:0]==111b, +// §12.7.1) is reported as AdcErrc::config_write_not_supported rather than +// crashing or silently accepted as a plain read — ADC has no EP_functional- +// config wiring in this codebase yet (same gap I2C's own handle_request +// comment calls out for I2C), so full §12.7.1 handling is out of scope here +// too. +// // 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 averaging -// combinator (arithmetic mean at both levels) and cadence-pattern API shape -// chosen in this file are this implementation's own, same as the equivalent -// disclaimers in rcp/avtp.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, -// rcp/gpio.hpp, and rcp/spi.hpp. +// combinator (arithmetic mean at both levels), cadence-pattern API shape, +// and single-value wire codec (encode_adc_value) chosen in this file are +// this implementation's own, same as the equivalent disclaimers in +// rcp/avtp.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, rcp/gpio.hpp, rcp/spi.hpp, +// and rcp/i2c.hpp. #pragma once +#include +#include + #include #include #include #include +#include #include namespace rcp { @@ -84,6 +114,13 @@ enum class AdcCadence : uint8_t { enum class AdcErrc : int { no_signal = 1, // internal condition: no valid sample before timeout/underrun — not a TC18 error code (see header comment; Table 27 has no ADC-specific no-signal entry) invalid_averaging_config = 2, // adc_avg_intervals_per_request or adc_combine_avg_values is 0 + // 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 AdcEndpoint::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. + config_write_not_supported = 3, }; inline const std::error_category& adc_category() noexcept { @@ -96,6 +133,8 @@ inline const std::error_category& adc_category() noexcept { "TC18's Table 27 defines no ADC-specific no-signal error code)"; case AdcErrc::invalid_averaging_config: return "rcp/adc: adc_avg_intervals_per_request/adc_combine_avg_values must be >= 1"; + case AdcErrc::config_write_not_supported: + return "rcp/adc: evt[2:0]=111b configuration-write requests are not yet implemented"; default: return "rcp/adc: unknown error"; } @@ -109,6 +148,24 @@ inline std::error_code make_error_code(AdcErrc e) noexcept { return {static_cast(e), adc_category()}; } +// ── Wire codec ──────────────────────────────────────────────────────────────── +// A single measurement value is a 16-bit big-endian "ADC value" field on the +// wire (§13.7.9.3's own field labels, Figure 34/Figure 35), reusing +// rcp/avtp.hpp's put_u16 rather than re-deriving byte order here, the same +// discipline rcp/pwm.hpp's encode_pwm_payload and rcp/gpio.hpp's +// encode_gpio_payload already follow. Only a single scripted-value encoding +// is provided — the multi-measurement (readsize > 2, Figure 35) response +// shape a real averaging/combine-count response can carry is out of scope +// for this milestone, same "data model, not full wire codec" scope split +// documented in rcp/regmap.hpp. +constexpr size_t kAdcValueLen = sizeof(uint16_t); + +inline std::vector encode_adc_value(uint16_t value) { + std::vector buf(kAdcValueLen); + avtp::detail::put_u16(buf.data(), value); + return buf; +} + // ── Level 1 / level 2 combinators ───────────────────────────────────────────── // Both levels of the averaging model use the same arithmetic-mean // combinator in this implementation — the extraction does not mandate a @@ -202,6 +259,50 @@ class AdcEndpoint { triggered_samples.begin() + static_cast(needed)); return compute_average(averaged_intervals, out_value); } + + // handle_request is ADC'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 second Table 33 Row 2 + // endpoint type after I2C). 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 request_reading 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 to request_reading (the + // SelfTimed cadence pattern above) with `cfg`/`take_sample`/ + // `out_value` unchanged — ADC's "standard read request" behavior, + // matching §13.7.9.3 Figure 34's own worked example (the same + // figure rcp/endpoint.hpp's evt_row2_kind_of cites to resolve Table + // 33's evt=000b ambiguity in the first place). This does not + // delegate to request_reading_from_trigger_queue — the + // ExternalTrigger cadence pattern remains its own separate entry + // point, not folded into handle_request by this milestone. + // - Reserved (evt[2:0] in 001b-110b): returns + // endpoint::EndpointErrc::reserved_evt_row2 without invoking + // `take_sample` or touching `out_value` — TC18 requires this be + // rejected with error code UNSUPPORTED_CMD. + // - ConfigWrite (evt[2:0] == 111b): §12.7.1's configuration-write + // shape targets the ADC EP's own functional-config block (relative + // start address + configuration data), not a sampling 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 own handle_request defers + // for the identical reason); this returns + // AdcErrc::config_write_not_supported rather than crashing, silently + // accepting the request as a read, or silently doing nothing. + template + std::error_code handle_request(uint8_t evt_op, const AdcAveragingConfig& cfg, SampleFn take_sample, + uint16_t& out_value) { + switch (endpoint::evt_row2_kind_of(evt_op)) { + case endpoint::EvtRow2Kind::Plain: + return request_reading(cfg, std::move(take_sample), out_value); + case endpoint::EvtRow2Kind::Reserved: + return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); + case endpoint::EvtRow2Kind::ConfigWrite: + return make_error_code(AdcErrc::config_write_not_supported); + } + return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); // unreachable + } }; } // namespace adc diff --git a/include/rcp/mock.hpp b/include/rcp/mock.hpp index 0286a0d..2b47921 100644 --- a/include/rcp/mock.hpp +++ b/include/rcp/mock.hpp @@ -10,6 +10,8 @@ // fusa:req REQ-MOCK-010 // fusa:req REQ-MOCK-011 // fusa:req REQ-MOCK-012 +// fusa:req REQ-MOCK-013 +// fusa:req REQ-MOCK-014 // In-process RC Server simulator — a small, representative OPEN Alliance // TC18 Remote Control Protocol Specification v0.5.1_RC server built @@ -35,9 +37,10 @@ // 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 three fully-built endpoint types — rcp::gpio::GpioEndpoint and -// rcp::spi::SpiEndpoint (both v2.3.0), plus rcp::i2c::I2cEndpoint (v2.4.0, -// wired in by the Table 30/33 Row 2 evt[2:0] validation pilot) — as its +// 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 // 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 @@ -58,17 +61,19 @@ // 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 endpoint -// numbering (GPIO at endpoint id / byte_bus_id 1, SPI at 2, I2C at 3), -// 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, and rcp/i2c.hpp. +// 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 +// 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. #pragma once #include +#include #include #include #include @@ -80,6 +85,8 @@ #include #include #include +#include +#include #include #include @@ -99,9 +106,11 @@ 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); // A discovery-shaped EP0 read only ever answers with the register map's // magic number below — see this header's own scope note above. @@ -132,6 +141,10 @@ inline acf::WireErrorCode wire_error_code_for(const std::error_code& ec) noexcep if (ec == make_error_code(i2c::I2cErrc::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(i2c::I2cErrc::nack)) return acf::WireErrorCode::EpError; + if (ec == make_error_code(adc::AdcErrc::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(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 return acf::WireErrorCode::UnsupportedCmd; } @@ -161,6 +174,7 @@ class Server final { gpio::GpioEndpoint& gpio() noexcept { return gpio_; } spi::SpiEndpoint& spi() noexcept { return spi_; } i2c::I2cEndpoint& i2c() noexcept { return i2c_; } + adc::AdcEndpoint& adc() noexcept { return adc_; } // set_spi_poci scripts the bytes a subsequent dispatch()/transfer() // call on `channel` reads back as POCI-in data. A real SPI peripheral's @@ -182,6 +196,22 @@ class Server final { i2c_acked_ = acked; } + // set_adc_response scripts the queue of 16-bit sample values a + // subsequent dispatch()-driven ADC plain (evt[2:0]==000b) request pulls + // from — the same "test scripts the sensor, this mock does not model + // actual hardware" pattern set_spi_poci/set_i2c_response above already + // establish. dispatch_adc() below always requests against a default- + // constructed AdcAveragingConfig (adc_avg_intervals_per_request==1, + // adc_combine_avg_values==1 — no averaging), so each dispatched plain + // ADC request consumes exactly one scripted sample, oldest first; + // scripted samples are consumed and NOT auto-refilled, so dispatching + // more plain requests than scripted samples reports AdcErrc::no_signal, + // the same underrun behavior AdcEndpoint::request_reading itself + // already implements when `take_sample` returns std::nullopt. + void set_adc_response(std::vector samples) { + adc_samples_.assign(samples.begin(), samples.end()); + } + // 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 @@ -218,6 +248,7 @@ class Server final { if (req.byte_bus_id == kGpioByteBusId) return dispatch_gpio(req, req_payload, out_resp, out_resp_payload); 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); return set_error_response(req, make_error_code(regmap::RegMapErrc::invalid_parameter), out_resp, out_resp_payload); @@ -238,13 +269,14 @@ class Server final { static regmap::RegisterMap make_initial_register_map() { regmap::RegisterMap regs; - regs.endpoint_count = 3; - regs.generic_configs.resize(3); - regs.functional_configs.resize(3); + regs.endpoint_count = 4; + regs.generic_configs.resize(4); + regs.functional_configs.resize(4); regs.ep_id_mapping = { {kGpioEndpointId, kGpioByteBusId}, {kSpiEndpointId, kSpiByteBusId}, {kI2cEndpointId, kI2cByteBusId}, + {kAdcEndpointId, kAdcByteBusId}, }; return regs; } @@ -343,15 +375,54 @@ class Server final { return {}; } + // ADC is request-driven sampling only (extraction §5.9) with no + // combinable write-request payload at all (adc.hpp's own header + // comment) — this dispatch path does not branch on req.op either, same + // rationale as dispatch_i2c/dispatch_spi above, just for a different + // reason (ADC has no write semantics implemented anywhere in this + // codebase, not that it is inherently full-duplex). Every request + // addressed here drives one AdcEndpoint::handle_request against a + // default-constructed AdcAveragingConfig (no averaging: one raw sample + // per request) and a take_sample callback that pulls the next scripted + // value set_adc_response() queued — Table 33 Row 2's 3-way + // Plain/Reserved/ConfigWrite classification (rcp::endpoint:: + // evt_row2_kind_of) is checked by handle_request itself before + // take_sample is ever invoked, so a Reserved or ConfigWrite evt can + // never consume a scripted sample. `payload` is unused: §13.7.9.3 + // states the ADC request itself carries no byte_msg_payload. + std::error_code dispatch_adc(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 take_sample = [this]() -> std::optional { + if (adc_samples_.empty()) return std::nullopt; + uint16_t v = adc_samples_.front(); + adc_samples_.pop_front(); + return v; + }; + uint16_t value = 0; + auto ec = adc_.handle_request(req.evt_op, adc::AdcAveragingConfig{}, take_sample, value); + if (ec) return set_error_response(req, ec, out_resp, out_resp_payload); + out_resp_payload = adc::encode_adc_value(value); + 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_; gpio::GpioEndpoint gpio_; spi::SpiEndpoint spi_; i2c::I2cEndpoint i2c_; + adc::AdcEndpoint adc_; std::array, spi::kMaxChannels> spi_poci_{}; std::vector i2c_response_{}; bool i2c_acked_ = true; + std::deque adc_samples_{}; }; } // namespace mock diff --git a/tests/test_adc.cpp b/tests/test_adc.cpp index 0da317c..d1e9bed 100644 --- a/tests/test_adc.cpp +++ b/tests/test_adc.cpp @@ -4,6 +4,9 @@ // fusa:test REQ-ADC-004 // fusa:test REQ-ADC-005 // fusa:test REQ-ADC-006 +// fusa:test REQ-ADC-007 +// fusa:test REQ-ADC-008 +// fusa:test REQ-ADC-009 // Tests for rcp/adc.hpp — the ADC endpoint type (ROADMAP.md milestone 48, // "Basic Endpoint Types II — I2C, UART, ADC, PWM_OUT, PWM_IN", v2.4.0). @@ -168,3 +171,97 @@ TEST_CASE("AdcErrc::no_signal's message does not claim an invented ADC_NO_SIGNAL auto ec = make_error_code(AdcErrc::no_signal); REQUIRE(ec.message().find("ADC_NO_SIGNAL") == std::string::npos); } + +// ── Wire codec ──────────────────────────────────────────────────────────────── + +TEST_CASE("encode_adc_value encodes a single measurement as 2-byte big-endian", + "[adc][REQ-ADC-007]") { + REQUIRE(encode_adc_value(0x0000) == std::vector{0x00, 0x00}); + REQUIRE(encode_adc_value(0xABCD) == std::vector{0xAB, 0xCD}); + REQUIRE(encode_adc_value(0xFFFF) == std::vector{0xFF, 0xFF}); +} + +// ── 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 ADC's own request-decode entry point, mirroring +// tests/test_i2c.cpp's "Table 33 Row 2 evt[2:0] validation (handle_request)" +// section exactly — see rcp/adc.hpp's own handle_request comment. + +TEST_CASE("AdcEndpoint::handle_request delegates a Plain (evt[2:0]==000b) request to " + "request_reading()", + "[adc][REQ-ADC-008]") { + AdcEndpoint ep; + AdcAveragingConfig cfg; + cfg.adc_avg_intervals_per_request = 2; + cfg.adc_combine_avg_values = 2; + + std::deque raw{10, 20, 30, 40}; + auto take_sample = [&raw]() -> std::optional { + if (raw.empty()) return std::nullopt; + uint16_t v = raw.front(); + raw.pop_front(); + return v; + }; + + uint16_t out_value = 0; + auto ec = ep.handle_request(/*evt_op=*/0, cfg, take_sample, out_value); + REQUIRE_FALSE(ec); + REQUIRE(out_value == 25); // same combination as request_reading's own direct-call test +} + +TEST_CASE("AdcEndpoint::handle_request rejects every reserved evt[2:0] value (001b-110b) " + "without invoking take_sample", + "[adc][REQ-ADC-008]") { + AdcAveragingConfig cfg; // default: 1x1, no averaging + for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { + AdcEndpoint ep; + bool take_sample_called = false; + auto take_sample = [&take_sample_called]() -> std::optional { + take_sample_called = true; + return uint16_t{1}; + }; + uint16_t out_value = 0; + auto ec = ep.handle_request(evt_op, cfg, take_sample, out_value); + REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); + REQUIRE_FALSE(take_sample_called); + } +} + +TEST_CASE("AdcEndpoint::handle_request reports config_write_not_supported for evt[2:0]==111b " + "without invoking take_sample", + "[adc][REQ-ADC-009]") { + AdcEndpoint ep; + AdcAveragingConfig cfg; + bool take_sample_called = false; + auto take_sample = [&take_sample_called]() -> std::optional { + take_sample_called = true; + return uint16_t{1}; + }; + uint16_t out_value = 0; + auto ec = ep.handle_request(/*evt_op=*/7, cfg, take_sample, out_value); + REQUIRE(ec == make_error_code(AdcErrc::config_write_not_supported)); + REQUIRE_FALSE(take_sample_called); +} + +TEST_CASE("AdcEndpoint::handle_request masks evt_op down to 3 bits before classifying", + "[adc][REQ-ADC-008]") { + AdcEndpoint ep; + AdcAveragingConfig cfg; + auto take_sample = []() -> std::optional { return uint16_t{7}; }; + + uint16_t out_value = 0; + REQUIRE_FALSE(ep.handle_request(/*evt_op=*/0xF8, cfg, take_sample, out_value)); // low 3 bits 000 -> Plain + REQUIRE(out_value == 7); + + auto ec = ep.handle_request(/*evt_op=*/0xF9, cfg, take_sample, out_value); // low 3 bits 001 -> Reserved + REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); +} + +TEST_CASE("AdcErrc::config_write_not_supported reports a non-empty message in its own category", + "[adc][REQ-ADC-009]") { + auto ec = make_error_code(AdcErrc::config_write_not_supported); + REQUIRE(ec.category() == adc_category()); + REQUIRE_FALSE(ec.message().empty()); +} diff --git a/tests/test_mock.cpp b/tests/test_mock.cpp index b4709af..d7a7020 100644 --- a/tests/test_mock.cpp +++ b/tests/test_mock.cpp @@ -10,6 +10,8 @@ // fusa:test REQ-MOCK-010 // fusa:test REQ-MOCK-011 // fusa:test REQ-MOCK-012 +// fusa:test REQ-MOCK-013 +// fusa:test REQ-MOCK-014 // Tests for rcp/mock.hpp — the in-process RC Server simulator (ROADMAP.md // milestone 56, "Test & Simulation Harness Rebuild", v2.12.0). See @@ -36,20 +38,22 @@ acf::AcfMessageInfo standard_request(avtp::ByteBusId bus_id, bool write, uint8_t // ── Construction / register map ────────────────────────────────────────────── -TEST_CASE("Server starts HW_UNCONFIGURED with a three-endpoint register map", +TEST_CASE("Server starts HW_UNCONFIGURED with a four-endpoint register map", "[mock][REQ-MOCK-001]") { mock::Server server; REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); - REQUIRE(server.registers().endpoint_count == 3); - REQUIRE(server.registers().generic_configs.size() == 3); - REQUIRE(server.registers().functional_configs.size() == 3); - REQUIRE(server.registers().ep_id_mapping.size() == 3); + 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().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); REQUIRE(server.registers().ep_id_mapping[1].byte_bus_id == mock::kSpiByteBusId); REQUIRE(server.registers().ep_id_mapping[2].ep_id == mock::kI2cEndpointId); 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); } TEST_CASE("advance_to_rcp_configured drives the lifecycle straight to RCP_CONFIGURED", @@ -263,6 +267,103 @@ TEST_CASE("I2C request is rejected before RCP_CONFIGURED, same operational gatin REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); } +// ── ADC ─────────────────────────────────────────────────────────────────────── +// Table 30/33 Row 2 evt[2:0] validation, second endpoint type after I2C: +// AdcEndpoint wired into dispatch() at byte_bus_id 4. + +TEST_CASE("ADC plain request (evt[2:0]==000b) answers with the scripted sample value", + "[mock][REQ-MOCK-013]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + server.set_adc_response({0xBEEF}); + + auto req = standard_request(mock::kAdcByteBusId, /*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{0xBE, 0xEF}); +} + +TEST_CASE("ADC plain requests consume scripted samples in FIFO order without auto-refill", + "[mock][REQ-MOCK-013]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + server.set_adc_response({0x0001, 0x0002}); + + auto req = standard_request(mock::kAdcByteBusId, /*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, 0x01}); + + REQUIRE_FALSE(server.dispatch(0, req, {}, resp, resp_payload)); + REQUIRE(resp_payload == std::vector{0x00, 0x02}); + + // Third dispatch: no scripted sample left -> AdcErrc::no_signal. + auto ec = server.dispatch(0, req, {}, resp, resp_payload); + REQUIRE(ec == adc::make_error_code(adc::AdcErrc::no_signal)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); +} + +TEST_CASE("ADC request with a reserved evt[2:0] (001b-110b) is rejected with wire error code " + "UNSUPPORTED_CMD and does not consume a scripted sample", + "[mock][REQ-MOCK-013]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + server.set_adc_response({0x1234}); + + for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { + auto req = standard_request(mock::kAdcByteBusId, /*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 sample must still be there — no reserved evt consumed it. + auto req = standard_request(mock::kAdcByteBusId, /*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{0x12, 0x34}); +} + +TEST_CASE("ADC 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-013]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + auto req = standard_request(mock::kAdcByteBusId, /*write=*/false, /*evt_op=*/7); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {}, resp, resp_payload); + REQUIRE(ec == adc::make_error_code(adc::AdcErrc::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("ADC request is rejected before RCP_CONFIGURED, same operational gating as GPIO/SPI/I2C", + "[mock][REQ-MOCK-014]") { + mock::Server server; + REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); + + auto req = standard_request(mock::kAdcByteBusId, /*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",