diff --git a/include/rcp/adc.hpp b/include/rcp/adc.hpp index a0857a7..9bcabe2 100644 --- a/include/rcp/adc.hpp +++ b/include/rcp/adc.hpp @@ -7,120 +7,492 @@ // fusa:req REQ-ADC-007 // fusa:req REQ-ADC-008 // fusa:req REQ-ADC-009 +// fusa:req REQ-ADC-010 +// fusa:req REQ-ADC-011 +// fusa:req REQ-ADC-012 +// fusa:req REQ-ADC-013 +// fusa:req REQ-ADC-014 +// fusa:req REQ-ADC-015 +// fusa:req REQ-ADC-016 +// fusa:req REQ-ADC-017 +// fusa:req REQ-ADC-018 +// fusa:req REQ-ADC-019 +// fusa:req REQ-ADC-020 +// fusa:req REQ-ADC-021 +// fusa:req REQ-ADC-022 +// fusa:req REQ-ADC-023 +// fusa:req REQ-ADC-024 +// fusa:req REQ-ADC-025 +// fusa:req REQ-ADC-026 +// fusa:req REQ-ADC-027 +// fusa:req REQ-ADC-028 +// fusa:req REQ-ADC-029 +// fusa:req REQ-ADC-030 +// fusa:req REQ-ADC-031 +// fusa:req REQ-ADC-032 +// fusa:req REQ-ADC-033 +// fusa:req REQ-ADC-034 +// fusa:req REQ-ADC-035 +// fusa:req REQ-ADC-036 +// fusa:req REQ-ADC-037 +// fusa:req REQ-ADC-038 +// fusa:req REQ-ADC-039 +// fusa:req REQ-ADC-040 +// fusa:req REQ-ADC-041 +// fusa:req REQ-ADC-042 +// fusa:req REQ-ADC-043 +// fusa:req REQ-ADC-044 +// fusa:req REQ-ADC-045 +// fusa:req REQ-ADC-046 +// fusa:req REQ-ADC-047 +// fusa:req REQ-ADC-048 +// fusa:req REQ-ADC-049 +// fusa:req REQ-ADC-050 +// fusa:req REQ-ADC-051 +// fusa:req REQ-ADC-052 +// fusa:req REQ-ADC-053 +// fusa:req REQ-ADC-054 +// fusa:req REQ-ADC-055 -// ADC endpoint (ep_type 0x09) — the three-level averaging model -// (adc_sample_interval -> adc_avg_intervals_per_request -> -// adc_combine_avg_values), request-driven sampling only (no free-running -// push), two self-triggering cadence patterns, and an internal no-signal -// condition analogous in *purpose*, but not in TC18 error-code identity, to -// PWM_IN's PWM_IN_NO_SIGNAL (extraction §5.9). +// ADC endpoint (ep_type 0x09) — the three-layer sampling model +// (adc_samples_per_avg_interval -> adc_avg_intervals_per_request -> +// adc_combine_avg_values), a real ACF-level wire codec, the EP_func +// functional-configuration register block (evt[2:0] == 111b), per-endpoint +// threshold/measurement-finished triggers (Table 53), and wall-clock +// inter-sample spacing validation (§13.7.9.1), request-driven sampling only +// (no free-running push) (extraction §5.9). // -// ROADMAP.md milestone 48, "Basic Endpoint Types II — I2C, UART, ADC, -// PWM_OUT, PWM_IN (v2.4.0)": ADC has no combinable write-request payload -// (it is read/request-driven only), so unlike rcp/gpio.hpp and -// rcp/pwm.hpp's PwmOutEndpoint it does not build on -// rcp/endpoint.hpp's WriteSemantics/apply_bitmask_write at all — its shared -// surface with the rest of this milestone is only the general -// request-dispatch shape (one entry point per request) and the no-signal -// error-path pattern rcp/pwm.hpp's PwmInEndpoint also implements. +// ROADMAP.md Phase 17 / cpp-RCP issue #129, Phase 3 ("Per-endpoint modules"): +// this header is re-derived from c-RCP's ep_adc.h/ep_adc.c — c-RCP's +// RC5-conformant reference implementation for this endpoint type — rather +// than incrementally patched, per the roadmap's own module-by-module +// rewrite plan. No text from the OPEN Alliance TC18 Remote Control Protocol +// Specification is reproduced here; field names and behavior below +// implement TC18's *behavior* as ported from c-RCP's own implementation of +// an internal structured extraction of the specification. // -// Sample/result width fix (issue #77, cpp-RCP-09): this header previously -// modeled ADC sample and result values as uint32_t throughout. Verified -// against the OPEN Alliance TC18 Remote Control Protocol Specification's -// ADC section (§13.7.9.1, "This endpoint type is for ADC's with a -// resolution of up to 16bits") and its response-frame figures (Figure 32, -// Figure 34), which are explicit about "16 bit ADC value" fields: ADC -// samples and results are 16-bit quantities, not 32-bit. compute_average -// and AdcEndpoint::request_reading/request_reading_from_trigger_queue below -// operate on uint16_t samples/results (widening only internally, to a -// 64-bit accumulator, to sum without overflow — the averaged *result* -// itself is always narrowed back to uint16_t, matching the wire width). +// CORRECTED (Phase 3 content-parity pass): this header's pre-Phase-3 +// content (ROADMAP.md milestone 48, v2.4.0, later relabeled the "Table 30/33 +// Row 2 evt[2:0] validation" pilot module) modeled adc_combine_avg_values as +// a SECOND arithmetic-mean reduction — AdcEndpoint::request_reading averaged +// `adc_avg_intervals_per_request` per-interval means down to one single +// out_value. c-RCP's own file header records that this exact mistake was +// already made and fixed once in c-RCP's own history: "An earlier revision +// of this module modelled adc_combine_avg_values as a four-way +// AVERAGE/MIN/MAX/LATEST mode enum collapsing every averaged value into one +// 2-octet response payload; that has no basis in the register table, which +// defines the field as the number of output values to be combined into one +// response, and it made every response exactly one value wide where a +// conforming peer expects N." This header's own pre-Phase-3 model was a +// different-shaped instance of the identical bug (silently averaging N +// output values into 1 instead of packing all N verbatim into a multi-value +// response) — re-verified against c-RCP's *current* ep_adc.c/.h rather than +// assumed accurate, exactly per this phase's own task instructions. Fixed +// below: `average_interval` (layer 1) and `collect_response_values` +// (layers 2/3, a packing operation, never a reduction) are now direct, +// signature-faithful ports of rcp_ep_adc_average_interval()/ +// rcp_ep_adc_collect_response_values(); `AdcEndpoint`'s old +// request_reading()/request_reading_from_trigger_queue()/handle_request() +// (which could only ever produce one out_value per call, and which +// conflated the field-naming: the old `AdcAveragingConfig:: +// adc_avg_intervals_per_request` was actually used as a *layer-1 +// samples-per-interval* count, c-RCP's `adc_samples_per_avg_interval`'s own +// role, not layer 2's real "intervals captured per measurement cycle" role) +// are replaced by AdcEndpoint::execute_measurement_cycle()/response_ready()/ +// collect_response(), which drive the corrected three-layer pipeline and +// produce a real N-value response array. The former AdcErrc:: +// invalid_averaging_config and AdcErrc::config_write_not_supported are +// removed: c-RCP validates neither adc_samples_per_avg_interval nor +// adc_avg_intervals_per_request (a 0 there simply yields kAdcNoSignal, the +// same fail-safe-over-rejection disposition average_interval() already uses +// for an empty sample set), and the evt[2:0]==111b configuration-write path +// this header previously stubbed out is now genuinely implemented via +// apply_reconfig() below (REQ-ADC-038/039), closing that gap rather than +// reporting it as out of scope. // -// No-signal error-identity fix (issue #77, cpp-RCP-09): this header -// previously rendered AdcErrc::no_signal's message as literally containing -// "ADC_NO_SIGNAL", implying a TC18-defined numeric error code by that name. -// Checked against the spec's Table 27 ("Error codes in responses", -// §12.9.6): there is no ADC-specific no-signal entry there at all — the -// only "*_NO_SIGNAL" entry in the whole table is PWM_IN_NO_SIGNAL (value 9), -// which belongs to the PWM_IN endpoint type, not ADC. AdcErrc::no_signal -// below is now documented and rendered as a plain internal library -// condition with no claimed spec error-code identity; it remains -// conceptually parallel to PWM_IN's no-signal handling (a request that -// cannot produce a valid sample) without pretending to be, or to map to, -// any numbered TC18 error code. +// No-signal sentinel: c-RCP reuses ep_pwm.h's RCP_EP_PWM_IN_NO_SIGNAL +// (0xFFFF) as ADC's own raw-sample/averaged-value timeout sentinel, so a +// "no valid measurement" value means the same numeric thing across both +// endpoint types sharing that milestone. cpp-RCP's own rcp/pwm.hpp models +// PWM_IN's no-signal condition as a std::error_code (PwmErrc::no_signal) +// rather than a reusable numeric constant, so this header defines its own +// `kAdcNoSignal` at the identical wire value (0xFFFF) instead of depending +// on a pwm.hpp constant that does not exist — same numeric identity as +// c-RCP's choice, own local definition per this codebase's own idiom. +// AdcErrc::no_signal (an actual error, distinct from the kAdcNoSignal +// sentinel value) no longer claims a TC18-defined error-code identity — +// Table 27 defines no ADC-specific no-signal code — matching this file's +// pre-Phase-3 finding (issue #77, cpp-RCP-09), which remains correct and is +// preserved below. // -// 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), 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. +// Sample/result width (issue #77, cpp-RCP-09, preserved): ADC samples and +// results are 16-bit quantities (§13.7.9.1, "up to 16bits"; Figure 32/34's +// "16 bit ADC value" fields) — RCP_EP_ADC_VALUE_LEN in c-RCP, kAdcValueLen +// here. #pragma once +#include #include #include +#include +#include +#include #include #include #include #include -#include #include namespace rcp { namespace adc { -// ── Averaging configuration ─────────────────────────────────────────────────── +// ── No-signal sentinel & response geometry ──────────────────────────────────── +// See the file header for kAdcNoSignal's derivation. A response's payload is +// exactly value_count * kAdcValueLen octets long -- N values, never one. + +constexpr uint16_t kAdcNoSignal = 0xFFFF; +constexpr size_t kAdcValueLen = sizeof(uint16_t); + +// The largest number of measurement values one response can carry, derived +// from acf.hpp's own ACF_ABB payload ceiling -- the same bound c-RCP derives +// from RCP_ACF_MAX_PAYLOAD for RCP_EP_ADC_MAX_VALUES. +constexpr size_t kAdcMaxResponseValues = acf::kAcfAbbMaxPayload / kAdcValueLen; + +// adc_combine_avg_values (Table 54) is an 8-bit register field, so no +// AdcEndpoint measurement cycle can ever need to hold more than 255 pending +// per-interval averages before a response can be assembled -- the bound +// AdcEndpoint's own fixed-capacity accumulator (below) is sized against, +// matching Phase 1/2's std::array-backed fixed-capacity convention (see +// rcp/request.hpp's detail::BoundedVector). +constexpr size_t kAdcMaxCombineValues = 255; + +// response_value_count returns the number of measurement values a response +// to a request carrying read_size is expected to contain: half the +// read_size, since each value occupies kAdcValueLen octets (extraction +// §5.9.3). Returns 0 for an odd read_size, which cannot describe a whole +// number of values. +inline size_t response_value_count(uint16_t read_size) noexcept { + if ((read_size % static_cast(kAdcValueLen)) != 0) return 0; + return static_cast(read_size) / kAdcValueLen; +} + +// ── Layer 1: adc_samples_per_avg_interval ───────────────────────────────────── + +// One raw ADC sample: value is kAdcNoSignal iff this particular sample did +// not complete within its timeout window; timestamp is the moment this +// sample was captured (or attempted) -- an attempted-but-timed-out sample is +// still a real capture attempt at a real moment (see +// validate_sample_spacing's own doc comment below). +struct AdcSample { + uint16_t value = 0; + uint64_t timestamp = 0; +}; + +// One averaging interval's result: value is the arithmetic mean (rounded +// down) of every non-kAdcNoSignal sample, or kAdcNoSignal itself iff every +// sample in this interval timed out (or the interval was empty); timestamp +// is the capture moment of the LAST sample that contributed to that mean -- +// or, when none did, of the last sample in the interval (0 iff the interval +// was empty). +struct AdcAvgValue { + uint16_t value = 0; + uint64_t timestamp = 0; +}; + +// average_interval computes one averaging interval's AdcAvgValue from raw +// samples -- layer 1 of the three-layer averaging model (see the file +// header). The capture moment reported is the end of the averaging window +// (the last sample that fed the mean), not its start (extraction §5.9.2) -- +// this function has no awareness of whether that spacing is actually +// uniform; see validate_sample_spacing below for the dedicated spacing +// check. +inline AdcAvgValue average_interval(const std::vector& samples) noexcept { + AdcAvgValue result; + if (samples.empty()) { + result.value = kAdcNoSignal; + result.timestamp = 0; + return result; + } + + uint64_t sum = 0; + size_t counted = 0; + for (const auto& s : samples) { + if (s.value == kAdcNoSignal) continue; + sum += s.value; + ++counted; + } + + // The capture moment of the LAST sample that actually fed the mean; when + // no sample was usable at all, the interval's last sample still marks + // the moment the window closed. + result.timestamp = samples.back().timestamp; + for (size_t i = samples.size(); i > 0; --i) { + if (samples[i - 1].value == kAdcNoSignal) continue; + result.timestamp = samples[i - 1].timestamp; + break; + } + + result.value = (counted == 0) ? kAdcNoSignal : static_cast(sum / counted); + return result; +} + +// ── REQ-ADC-033: inter-sample spacing, validated against a real caller- ────── +// supplied clock rate ───────────────────────────────────────────────────────── -struct AdcAveragingConfig { - uint32_t adc_sample_interval_us = 0; // level 1 pacing; not enforced by this header, see AdcCadence - uint16_t adc_avg_intervals_per_request = 1; // level 1 sample count per averaged-interval value - uint16_t adc_combine_avg_values = 1; // level 2 averaged-interval count per request result +enum class AdcSpacingResult : uint8_t { + Ok = 0, // every consecutive pair within tolerance, or too little + // information to check (fewer than 2 samples, + // base_clk_hz == 0, or base_clk_divider == 0) + Violation = 1, // at least one consecutive pair was not spaced within + // tolerance of the configured interval, OR two + // consecutive timestamps were not monotonically increasing }; -// ── Cadence pattern selector ────────────────────────────────────────────────── +// TC18 §13.7.9.1's own inter-sample-spacing rule: successive samples one +// adc_sample_interval apart, that interval expressed in multiples of +// ADC_CLK cycles, where ADC_CLK = adc_base_clk / adc_base_clk_divider. +// adc_base_clk itself is never modelled by this module (no real clock +// source) -- base_clk_hz is the caller's own real oscillator frequency in +// Hz, supplied directly, the same "this library never invents wall time or +// a clock rate itself" discipline rcp/request.hpp's own presentation-time +// admission uses for its caller-supplied gptp reference. +// +// expected_spacing_ns = sample_interval * base_clk_divider * 1e9 / +// base_clk_hz. tolerance_ns widens that expectation by +/- tolerance_ns on +// each side to absorb real capture jitter; 0 demands exact spacing. +// +// Fails open (AdcSpacingResult::Ok) when there is too little information to +// check at all (fewer than 2 samples, base_clk_hz == 0, or +// base_clk_divider == 0) -- the same disposition average_interval() uses +// for an empty sample set. Otherwise reports Violation on the first +// consecutive pair whose timestamps are not monotonically increasing, or +// whose spacing falls outside [expected - tolerance, expected + tolerance] +// -- does not continue checking past the first violation. +inline AdcSpacingResult validate_sample_spacing(const std::vector& samples, + uint8_t base_clk_divider, uint8_t sample_interval, + uint32_t base_clk_hz, uint64_t tolerance_ns) noexcept { + if (samples.size() < 2 || base_clk_hz == 0 || base_clk_divider == 0) { + return AdcSpacingResult::Ok; + } + + const uint64_t expected_ns = static_cast(sample_interval) * + static_cast(base_clk_divider) * 1000000000ULL / base_clk_hz; + + const uint64_t lo = (expected_ns > tolerance_ns) ? (expected_ns - tolerance_ns) : 0; + const uint64_t hi = (expected_ns <= UINT64_MAX - tolerance_ns) ? (expected_ns + tolerance_ns) + : UINT64_MAX; + + for (size_t i = 0; i + 1 < samples.size(); ++i) { + if (samples[i + 1].timestamp < samples[i].timestamp) return AdcSpacingResult::Violation; + const uint64_t actual_ns = samples[i + 1].timestamp - samples[i].timestamp; + if (actual_ns < lo || actual_ns > hi) return AdcSpacingResult::Violation; + } + + return AdcSpacingResult::Ok; +} + +// ── Layers 2/3: adc_avg_intervals_per_request + adc_combine_avg_values ─────── +// adc_combine_avg_values is a COUNT of averaged output values a response +// carries, not a mode selector -- this stage packs rather than reduces (see +// the file header's own correction note). + +// collect_response_values packs the first value_count layer-1 results into +// out_values, in capture order and verbatim (a kAdcNoSignal interval is +// carried through as kAdcNoSignal, never averaged away). Returns the number +// of values actually written, min(avg_values.size(), value_count). +inline size_t collect_response_values(const std::vector& avg_values, size_t value_count, + std::vector& out_values) { + const size_t n = std::min(avg_values.size(), value_count); + out_values.clear(); + out_values.reserve(n); + for (size_t i = 0; i < n; ++i) out_values.push_back(avg_values[i].value); + return n; +} -enum class AdcCadence : uint8_t { - SelfTimed = 0, // raw samples paced internally by adc_sample_interval (caller-supplied pacing) - ExternalTrigger = 1, // raw samples paced by an external trigger signal occurrence +// REQ-ADC-037: which of the three documented cadence cases applies for a +// given (adc_avg_intervals_per_request, adc_combine_avg_values) pair. Pure +// function of the two register values; this module still owns no scheduling +// state (see the file header) -- it only names the decision a caller must +// make. +enum class AdcCadenceCase : uint8_t { + Accumulate = 0, // combine > intervals: several request executions feed one response + OneToOne = 1, // combine == intervals: exactly one response per execution + FanOut = 2, // combine < intervals: one execution yields several responses }; +inline AdcCadenceCase cadence_case(uint16_t avg_intervals_per_request, uint8_t combine_avg_values) noexcept { + if (static_cast(combine_avg_values) > static_cast(avg_intervals_per_request)) + return AdcCadenceCase::Accumulate; + if (static_cast(combine_avg_values) < static_cast(avg_intervals_per_request)) + return AdcCadenceCase::FanOut; + return AdcCadenceCase::OneToOne; +} + +// REQ-ADC-053: true iff pending_value_count averaged values already captured +// are enough to assemble one response, i.e. pending_value_count >= +// combine_avg_values -- the one comparison underlying all three cadence +// cases. combine_avg_values == 0 always returns true (an empty response is +// trivially "ready", matching collect_response_values' own value_count == 0 +// handling). +inline bool cadence_response_ready(size_t pending_value_count, uint8_t combine_avg_values) noexcept { + return pending_value_count >= static_cast(combine_avg_values); +} + +// The last-sample-of-the-first-response-value capture-moment rule +// (extraction §5.9.2): returns avg_values[0].timestamp (0 iff avg_values is +// empty). +inline uint64_t capture_moment_timestamp(const std::vector& avg_values) noexcept { + if (avg_values.empty()) return 0; + return avg_values.front().timestamp; +} + +// ── Functional config ───────────────────────────────────────────────────────── +// Flattens regmap.h's shared functional-config "common" prefix +// (ep_enable/ep_clear_req_storage/ep_req_crc_enable/ep_response_ts_enable/ +// ep_suppress_response) directly into this struct's own bools, rather than +// composing rcp::regmap::EndpointFunctionalConfig as a typed member: unlike +// c-RCP's regmap.h, cpp-RCP's rcp/regmap.hpp deliberately leaves +// EndpointFunctionalConfig::data as an opaque byte blob (endpoint-type +// interpretation is out of regmap.hpp's own scope, per that header's file +// comment) -- render_registers()/apply_reconfig() below are this endpoint +// type's own full interpretation of that blob, matching gpio.hpp's existing +// encode/decode_gpio_functional_config precedent. +struct AdcFunctionalConfig { + bool ep_enable = false; + bool ep_clear_req_storage = false; + bool ep_req_crc_enable = false; + bool ep_response_ts_enable = false; + bool ep_suppress_response = false; + + uint16_t adc_samples_per_avg_interval = 0; + uint16_t adc_avg_intervals_per_request = 0; + uint8_t adc_combine_avg_values = 0; // COUNT of output values per response, not a mode selector + uint16_t ep_status = 0; // adc_ep_status, Table 54 + uint8_t base_clk_divider = 0; // adc_base_clk_divider + uint8_t sample_interval = 0; // adc_sample_interval + uint8_t resolution = 0; // adc_resolution, <=16 + uint16_t trigger_min = 0; // adc_trigger_min + uint16_t trigger_max = 0; // adc_trigger_max +}; + +// functional_cfg_writable is a thin, named wrapper over +// rcp::lifecycle::field_writable() with FieldKind::FunctionalW, reusing -- +// never duplicating -- that function's authorization logic (TC18 Table 24's +// W marker: functionally, not permanently, re-lockable once RcpConfigured). +inline bool functional_cfg_writable(lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + return lifecycle::field_writable(state, lifecycle::FieldKind::FunctionalW, writer); +} + +inline bool set_samples_per_avg_interval(AdcFunctionalConfig& cfg, uint16_t samples_per_interval, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.adc_samples_per_avg_interval = samples_per_interval; + return true; +} + +inline bool set_avg_intervals_per_request(AdcFunctionalConfig& cfg, uint16_t intervals_per_request, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.adc_avg_intervals_per_request = intervals_per_request; + return true; +} + +inline bool set_combine_avg_values(AdcFunctionalConfig& cfg, uint8_t combine_avg_values, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.adc_combine_avg_values = combine_avg_values; + return true; +} + +// ── Trigger outputs (Table 53), REQ-ADC-031/048-052 ─────────────────────────── +// Table 53: an ADC endpoint can generate five trigger events -- 0/1 fire +// when the averaged output value falls below/rises above adc_trigger_min, +// 2/3 fire when it falls below/rises above adc_trigger_max, and 4 fires when +// the endpoint finished a measurement interval. Triggers 0-3 are +// EDGE-triggered (a transition relative to the PREVIOUS averaged value, not +// a level comparison against the current one alone) -- distinct from +// trigger 4, a pure "did a measurement just finish" signal with no +// threshold or previous-value concept at all. + +constexpr uint8_t kAdcTriggerBelowMin = 0x01; // trigger 0 +constexpr uint8_t kAdcTriggerAboveMin = 0x02; // trigger 1 +constexpr uint8_t kAdcTriggerBelowMax = 0x04; // trigger 2 +constexpr uint8_t kAdcTriggerAboveMax = 0x08; // trigger 3 +constexpr uint8_t kAdcTriggerMeasurementFinished = 0x10; // trigger 4 + +// AdcTriggerState is this module's own small, caller-owned per-endpoint +// tracker holding the one piece of state edge detection needs -- the same +// caller-owned-data architecture rcp/pwm.hpp's PwmInEndpoint and +// rcp/endpoint.hpp's TriggerRegistry already establish for a per-endpoint +// tracker. +struct AdcTriggerState { + bool has_previous = false; // false until the first evaluate() call + uint16_t previous_value = 0; // meaningless while has_previous is false +}; + +// trigger_evaluate evaluates one newly acquired averaged output value +// against trigger_min/trigger_max and updates s's own tracked previous +// value for the next call. measurement_finished is a caller-supplied bool -- +// trigger 4 is not a threshold comparison at all, so this function cannot +// derive it from value alone. +// +// Triggers 0-3 fire only relative to a genuine previous value: +// BELOW_MIN/BELOW_MAX fire iff the previous value was AT OR ABOVE the +// threshold and value is strictly below it (a genuine downward crossing, +// not merely "currently below"); ABOVE_MIN/ABOVE_MAX symmetrically for an +// upward crossing. While s.has_previous is false, no edge exists to detect +// yet, so none of triggers 0-3 can fire on that call regardless of value -- +// only trigger 4, which needs no previous value at all. Returns 0 (no bits +// set) iff no trigger fires. +inline uint8_t trigger_evaluate(AdcTriggerState& s, uint16_t value, uint16_t trigger_min, + uint16_t trigger_max, bool measurement_finished) noexcept { + uint8_t fired = 0; + + if (s.has_previous) { + if (s.previous_value >= trigger_min && value < trigger_min) fired |= kAdcTriggerBelowMin; + if (s.previous_value <= trigger_min && value > trigger_min) fired |= kAdcTriggerAboveMin; + if (s.previous_value >= trigger_max && value < trigger_max) fired |= kAdcTriggerBelowMax; + if (s.previous_value <= trigger_max && value > trigger_max) fired |= kAdcTriggerAboveMax; + } + + if (measurement_finished) fired |= kAdcTriggerMeasurementFinished; + + s.previous_value = value; + s.has_previous = true; + return fired; +} + // ── Errors ──────────────────────────────────────────────────────────────────── 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, + no_signal = 1, // internal condition, not a TC18 error code (see file header) + short_frame = 2, + bad_msg_type = 3, + wrong_bus = 4, + wrong_op = 5, + bad_payload_len = 6, + // The response carries more measurement values than the caller's + // out_values buffer can hold -- see decode_response(). + too_many_values = 7, + // evt[2:0] is not 0b000, TC18 §13.5 Table 33's only legal value for a + // plain (non-configuration) request in ADC's endpoint-type row -- + // caller shall respond with error code UNSUPPORTED_CMD. + bad_evt = 8, + // Configuration write (evt[2:0]==111b) payload carries no address + // prefix, or an address prefix with no data octet after it. + reconfig_short = 9, + // Configuration write's start_address + data length exceeds + // kAdcEpFuncLen -- the whole write is ignored, per the specification's + // own rule. + reconfig_out_of_range = 10, + // AdcEndpoint's own fixed-capacity pending-value accumulator (bounded + // at kAdcMaxCombineValues, see the file header) is full -- this + // implementation's own defensive bound, not a TC18 error code. + pending_values_full = 11, + // AdcEndpoint::collect_response() was called before cadence_response_ + // ready() would report true. + response_not_ready = 12, }; inline const std::error_category& adc_category() noexcept { @@ -131,10 +503,22 @@ inline const std::error_category& adc_category() noexcept { case AdcErrc::no_signal: return "rcp/adc: no signal — no valid sample captured (an internal condition; " "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"; + case AdcErrc::short_frame: return "rcp/adc: frame too short"; + case AdcErrc::bad_msg_type: return "rcp/adc: unexpected ACF message type"; + case AdcErrc::wrong_bus: return "rcp/adc: wrong byte_bus_id"; + case AdcErrc::wrong_op: return "rcp/adc: wrong ACF op"; + case AdcErrc::bad_payload_len: return "rcp/adc: unexpected payload length"; + case AdcErrc::too_many_values: + return "rcp/adc: more measurement values than the caller can hold"; + case AdcErrc::bad_evt: return "rcp/adc: evt[2:0] is not 0b000"; + case AdcErrc::reconfig_short: + return "rcp/adc: ADC configuration write has no address and data"; + case AdcErrc::reconfig_out_of_range: + return "rcp/adc: ADC configuration write extends past the EP_func block"; + case AdcErrc::pending_values_full: + return "rcp/adc: no room for another pending averaged value"; + case AdcErrc::response_not_ready: + return "rcp/adc: not enough pending averaged values for a response yet"; default: return "rcp/adc: unknown error"; } @@ -148,161 +532,380 @@ 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; +// wire_error maps e to its numbered wire error code (acf::WireErrorCode), +// for a caller building an Error Response frame once a request has failed +// to decode. std::nullopt for every AdcErrc value with no numbered +// counterpart (local framing/routing outcomes a caller resolves before an +// ADC-specific Response frame would even be constructible). +inline std::optional wire_error(AdcErrc e) noexcept { + switch (e) { + case AdcErrc::bad_payload_len: return acf::WireErrorCode::InvalidParameter; + case AdcErrc::bad_evt: return acf::WireErrorCode::UnsupportedCmd; + default: return std::nullopt; + } } -// ── 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 -// different combination function per level, so reusing one keeps the two -// levels' behavior obviously consistent with each other. Samples and the -// averaged result are 16-bit (the spec's ADC resolution ceiling, §13.7.9.1); -// summation widens internally to avoid overflow but the result is narrowed -// back to uint16_t. - -inline std::error_code compute_average(const std::vector& samples, uint16_t& out_avg) noexcept { - if (samples.empty()) return make_error_code(AdcErrc::no_signal); - uint64_t sum = 0; - for (const uint16_t s : samples) sum += s; - out_avg = static_cast(sum / samples.size()); +// ── The EP_func register block (evt[2:0] == 111b) ──────────────────────────── +// Relative octet offsets, per Table 54. Every multi-octet register is +// big-endian. Offsets marked R are read-only: a configuration write +// covering them leaves them unchanged (see apply_reconfig()). + +constexpr uint16_t kAdcRegEpLen = 0x0000; // 8 bit, R +constexpr uint16_t kAdcRegReserved01 = 0x0001; // 8 bit, R +constexpr uint16_t kAdcRegEpEnableClr = 0x0002; // 8 bit, R/W +constexpr uint16_t kAdcRegEpOptions = 0x0003; // 8 bit, R/W +constexpr uint16_t kAdcRegBaseClk = 0x0004; // 16 bit, R +constexpr uint16_t kAdcRegEpStatus = 0x0006; // 16 bit, R/W +constexpr uint16_t kAdcRegBaseClkDivider = 0x0008; // 8 bit, R/W +constexpr uint16_t kAdcRegSampleInterval = 0x0009; // 8 bit, R/W +constexpr uint16_t kAdcRegAvgIntervals = 0x000A; // 8 bit, R/W +constexpr uint16_t kAdcRegSamplesPerAvg = 0x000B; // 8 bit, R/W +constexpr uint16_t kAdcRegCombineAvg = 0x000C; // 8 bit, R/W +constexpr uint16_t kAdcRegResolution = 0x000D; // 8 bit, R/W +constexpr uint16_t kAdcRegTriggerMin = 0x000E; // 16 bit, R/W +constexpr uint16_t kAdcRegTriggerMax = 0x0010; // 16 bit, R/W + +// The block's own length in octets -- one past the last assigned offset. +constexpr uint16_t kAdcEpFuncLen = 0x0012; +constexpr size_t kAdcReconfigAddrLen = 2; + +using AdcRegisterBlock = std::array; + +namespace detail { +constexpr uint8_t kAdcEnableClrBitEnable = 1u << 0; +constexpr uint8_t kAdcEnableClrBitClear = 1u << 4; +constexpr uint8_t kAdcOptionsBitReqCrc = 1u << 0; +constexpr uint8_t kAdcOptionsBitRespTs = 1u << 3; +constexpr uint8_t kAdcOptionsBitSuppress = 1u << 7; + +inline bool adc_reg_offset_read_only(uint16_t addr) noexcept { + return addr == kAdcRegEpLen || addr == kAdcRegReserved01 || addr == kAdcRegBaseClk || + addr == static_cast(kAdcRegBaseClk + 1); +} +} // namespace detail + +// render_registers serializes cfg's EP_func registers into out exactly as a +// configuration *read* of the whole block would report them -- the inverse +// of apply_reconfig()'s own parse step. adc_base_clk (read-only) always +// renders 0 -- no real clock source modelled (see the file header). +inline void render_registers(const AdcFunctionalConfig& cfg, AdcRegisterBlock& out) noexcept { + uint8_t enable_clr = 0; + uint8_t options = 0; + if (cfg.ep_enable) enable_clr |= detail::kAdcEnableClrBitEnable; + if (cfg.ep_clear_req_storage) enable_clr |= detail::kAdcEnableClrBitClear; + if (cfg.ep_req_crc_enable) options |= detail::kAdcOptionsBitReqCrc; + if (cfg.ep_response_ts_enable) options |= detail::kAdcOptionsBitRespTs; + if (cfg.ep_suppress_response) options |= detail::kAdcOptionsBitSuppress; + + out[kAdcRegEpLen] = static_cast(kAdcEpFuncLen); + out[kAdcRegReserved01] = 0; + out[kAdcRegEpEnableClr] = enable_clr; + out[kAdcRegEpOptions] = options; + avtp::detail::put_u16(&out[kAdcRegBaseClk], 0); + avtp::detail::put_u16(&out[kAdcRegEpStatus], cfg.ep_status); + out[kAdcRegBaseClkDivider] = cfg.base_clk_divider; + out[kAdcRegSampleInterval] = cfg.sample_interval; + // Truncated to the low octet -- these two fields are this module's own + // wider uint16_t than Table 54's 8-bit registers. + out[kAdcRegAvgIntervals] = static_cast(cfg.adc_avg_intervals_per_request & 0xFF); + out[kAdcRegSamplesPerAvg] = static_cast(cfg.adc_samples_per_avg_interval & 0xFF); + out[kAdcRegCombineAvg] = cfg.adc_combine_avg_values; + out[kAdcRegResolution] = cfg.resolution; + avtp::detail::put_u16(&out[kAdcRegTriggerMin], cfg.trigger_min); + avtp::detail::put_u16(&out[kAdcRegTriggerMax], cfg.trigger_max); +} + +namespace detail { +inline void adc_parse_registers(AdcFunctionalConfig& cfg, const AdcRegisterBlock& in) noexcept { + const uint8_t enable_clr = in[kAdcRegEpEnableClr]; + const uint8_t options = in[kAdcRegEpOptions]; + + cfg.ep_enable = (enable_clr & kAdcEnableClrBitEnable) != 0; + cfg.ep_clear_req_storage = (enable_clr & kAdcEnableClrBitClear) != 0; + cfg.ep_req_crc_enable = (options & kAdcOptionsBitReqCrc) != 0; + cfg.ep_response_ts_enable = (options & kAdcOptionsBitRespTs) != 0; + cfg.ep_suppress_response = (options & kAdcOptionsBitSuppress) != 0; + + cfg.ep_status = avtp::detail::get_u16(&in[kAdcRegEpStatus]); + cfg.base_clk_divider = in[kAdcRegBaseClkDivider]; + cfg.sample_interval = in[kAdcRegSampleInterval]; + cfg.adc_avg_intervals_per_request = in[kAdcRegAvgIntervals]; + cfg.adc_samples_per_avg_interval = in[kAdcRegSamplesPerAvg]; + cfg.adc_combine_avg_values = in[kAdcRegCombineAvg]; + cfg.resolution = in[kAdcRegResolution]; + cfg.trigger_min = avtp::detail::get_u16(&in[kAdcRegTriggerMin]); + cfg.trigger_max = avtp::detail::get_u16(&in[kAdcRegTriggerMax]); +} +} // namespace detail + +// apply_reconfig applies the configuration escape hatch (evt[2:0] == 111b): +// payload is a 16-bit big-endian relative start address followed by the +// configuration data octets to write from that address onward (§12.7.1). +// Returns AdcErrc::reconfig_short when payload_len is not at least +// kAdcReconfigAddrLen + 1, and AdcErrc::reconfig_out_of_range when the +// addressed span would extend past kAdcEpFuncLen; in both cases cfg is left +// entirely unchanged. Octets of the addressed span that land on a read-only +// register are left at their current values while the rest of the span is +// still applied. +inline std::error_code apply_reconfig(AdcFunctionalConfig& cfg, const uint8_t* payload, + size_t payload_len) { + if (payload_len <= kAdcReconfigAddrLen) return make_error_code(AdcErrc::reconfig_short); + + const uint16_t start_address = avtp::detail::get_u16(payload); + const size_t data_len = payload_len - kAdcReconfigAddrLen; + + if (static_cast(start_address) + data_len > static_cast(kAdcEpFuncLen)) + return make_error_code(AdcErrc::reconfig_out_of_range); + + AdcRegisterBlock block{}; + render_registers(cfg, block); + for (size_t i = 0; i < data_len; ++i) { + const auto addr = static_cast(start_address + i); + if (detail::adc_reg_offset_read_only(addr)) continue; + block[addr] = payload[kAdcReconfigAddrLen + i]; + } + detail::adc_parse_registers(cfg, block); + return {}; +} + +// ── Wire codec ───────────────────────────────────────────────────────────────── +// Ported directly from c-RCP's ep_adc.c wire functions, using rcp/acf.hpp's +// ACF_ABB/ACF_GBB codec rather than re-deriving frame layout here. + +// encode_read_request encodes an ACF_ABB read request addressed to +// byte_bus_id, with no payload -- how many measurement values it asks for +// is carried by read_size alone (extraction §5.9.3). +inline std::vector encode_read_request(avtp::ByteBusId byte_bus_id, uint16_t read_size, + uint8_t transaction_num) { + acf::AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.op = false; // read + info.read_size_or_segment_num = read_size; + info.transaction_num = transaction_num; + return acf::encode_acf_abb(info, {}); +} + +// decode_read_request decodes and validates an ACF-level ADC read request. +// Fails with AdcErrc::short_frame / bad_msg_type / wrong_bus / wrong_op / +// bad_evt (evt[2:0] != 0b000, caller shall respond UNSUPPORTED_CMD). On +// success, out_read_size/out_transaction_num are populated. +inline std::error_code decode_read_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + uint16_t& out_read_size, uint8_t& out_transaction_num) { + acf::AcfMessageInfo info; + std::vector payload; + const auto ec = acf::decode_acf_abb(b, len, info, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(AdcErrc::short_frame); + if (ec) return make_error_code(AdcErrc::bad_msg_type); + + if (info.byte_bus_id != expected_bus_id) return make_error_code(AdcErrc::wrong_bus); + if (info.op) return make_error_code(AdcErrc::wrong_op); // op=true means write + if (endpoint::evt_row2_kind_of(info.evt_op) != endpoint::EvtRow2Kind::Plain) + return make_error_code(AdcErrc::bad_evt); + + out_read_size = info.read_size_or_segment_num; + out_transaction_num = info.transaction_num; + return {}; +} + +// encode_response encodes an ADC response carrying values[0..values.size()) +// as a big-endian payload, echoing transaction_num and reporting +// 2*values.size() as the header's read_size. Encoded as ACF_ABB when timed +// is false; as ACF_GBB (mtv valid, message_timestamp = timestamp) when +// timed is true. Returns an empty vector if values is empty or exceeds +// kAdcMaxResponseValues. +inline std::vector encode_response(avtp::ByteBusId byte_bus_id, const std::vector& values, + uint8_t transaction_num, bool timed, uint64_t timestamp) { + if (values.empty() || values.size() > kAdcMaxResponseValues) return {}; + + std::vector payload(values.size() * kAdcValueLen); + for (size_t i = 0; i < values.size(); ++i) + avtp::detail::put_u16(&payload[i * kAdcValueLen], values[i]); + + acf::AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.op = false; // read + info.rsp = true; + info.read_size_or_segment_num = static_cast(payload.size()); + info.transaction_num = transaction_num; + + if (timed) { + info.mtv = true; + return acf::encode_acf_gbb(info, timestamp, payload); + } + return acf::encode_acf_abb(info, payload); +} + +// decode_response decodes an ADC response from either an ACF_ABB or +// ACF_GBB message (peeks the ACF message type itself, since a response's +// encoding depends on the responding endpoint's own timed/untimed choice). +// Fails with AdcErrc::short_frame / bad_msg_type / wrong_bus / +// bad_payload_len (payload absent, or not a whole number of kAdcValueLen- +// octet values) / too_many_values (more values than max_values). On +// success, out_values/out_transaction_num are populated; out_timed/ +// out_timestamp report whether the message was a valid-timestamp ACF_GBB, +// and that timestamp's value (0 when !out_timed). +inline std::error_code decode_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + size_t max_values, std::vector& out_values, + bool& out_timed, uint64_t& out_timestamp, + uint8_t& out_transaction_num) { + uint8_t msg_type = 0; + if (acf::peek_msg_type(b, len, msg_type)) return make_error_code(AdcErrc::short_frame); + + acf::AcfMessageInfo info; + std::vector payload; + avtp::ByteBusId bus_id = 0; + uint8_t txn = 0; + + if (msg_type == acf::kAcfMsgTypeGbb) { + uint64_t ts = 0; + const auto ec = acf::decode_acf_gbb(b, len, info, ts, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(AdcErrc::short_frame); + if (ec) return make_error_code(AdcErrc::bad_msg_type); + bus_id = info.byte_bus_id; + txn = info.transaction_num; + out_timed = info.mtv; + out_timestamp = out_timed ? ts : 0; + } else { + const auto ec = acf::decode_acf_abb(b, len, info, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(AdcErrc::short_frame); + if (ec) return make_error_code(AdcErrc::bad_msg_type); + bus_id = info.byte_bus_id; + txn = info.transaction_num; + out_timed = false; + out_timestamp = 0; + } + + if (bus_id != expected_bus_id) return make_error_code(AdcErrc::wrong_bus); + if (payload.empty() || (payload.size() % kAdcValueLen) != 0) + return make_error_code(AdcErrc::bad_payload_len); + + const size_t value_count = payload.size() / kAdcValueLen; + if (value_count > max_values) return make_error_code(AdcErrc::too_many_values); + + out_values.clear(); + out_values.reserve(value_count); + for (size_t i = 0; i < value_count; ++i) + out_values.push_back(avtp::detail::get_u16(&payload[i * kAdcValueLen])); + + out_transaction_num = txn; return {}; } +// encode_reconfig_request encodes an ACF_ABB configuration request +// (evt[2:0] == 111b) addressed to byte_bus_id: payload is start_address +// (16-bit big-endian) followed by data. Returns an empty vector if data is +// empty or the encoded payload would exceed acf::kAcfAbbMaxPayload. +inline std::vector encode_reconfig_request(avtp::ByteBusId byte_bus_id, uint16_t start_address, + const std::vector& data, + uint8_t transaction_num) { + if (data.empty()) return {}; + if (kAdcReconfigAddrLen + data.size() > acf::kAcfAbbMaxPayload) return {}; + + std::vector payload(kAdcReconfigAddrLen + data.size()); + avtp::detail::put_u16(payload.data(), start_address); + std::copy(data.begin(), data.end(), payload.begin() + static_cast(kAdcReconfigAddrLen)); + + acf::AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.op = true; // write: §12.7.1 Figure 18, the data of the byte_msg_payload + // from a write request is written into the EP_func block + info.evt_op = 0x7; // evt[2:0] = 111b + info.transaction_num = transaction_num; + return acf::encode_acf_abb(info, payload); +} + // ── AdcEndpoint ─────────────────────────────────────────────────────────────── -// Request-driven sampling only (extraction §5.9): a reading only happens as -// the direct result of one of the request_reading_* calls below — there is -// no free-running/self-pushed sample stream modeled anywhere in this -// header. +// Drives the corrected three-layer pipeline end to end: execute_measurement_ +// cycle() runs one layer-1 averaging interval and accumulates its result; +// response_ready()/collect_response() implement REQ-ADC-053's own single +// readiness rule and then pack+drain the pending accumulator into a real +// N-value response array, exactly mirroring the caller-orchestration loop +// c-RCP's own file header describes for all three cadence cases (this +// module still owns no scheduling/timer state of its own -- see the file +// header's "request-driven sampling only" section). The pending accumulator +// is a fixed-capacity, std::array-backed buffer bounded at +// kAdcMaxCombineValues (Table 54's own 8-bit adc_combine_avg_values width), +// matching Phase 1/2's fixed-capacity convention. +namespace detail { +class AdcPendingBuffer { +public: + bool push_back(AdcAvgValue v) noexcept { + if (size_ >= kAdcMaxCombineValues) return false; + data_[size_] = v; + ++size_; + return true; + } + size_t size() const noexcept { return size_; } + bool empty() const noexcept { return size_ == 0; } + const AdcAvgValue& operator[](size_t i) const noexcept { return data_[i]; } + + // pop_front_n removes the first n entries (clamped to size()), shifting + // the remainder down. + void pop_front_n(size_t n) noexcept { + n = std::min(n, size_); + std::move(data_.begin() + static_cast(n), data_.begin() + static_cast(size_), + data_.begin()); + size_ -= n; + } + +private: + std::array data_{}; + size_t size_ = 0; +}; +} // namespace detail + class AdcEndpoint { public: - // request_reading implements the SelfTimed cadence pattern: `take_sample` - // is invoked synchronously once per required raw sample (already paced - // by adc_sample_interval on the caller's side, e.g. by a wall-clock - // driven sampler) and must return std::nullopt to report a failed/ - // missing capture, which this function surfaces as AdcErrc::no_signal — - // this endpoint's analog of PWM_IN's no-signal handling on the request - // path (see header comment: not itself a TC18 error code). + // execute_measurement_cycle runs cfg.adc_samples_per_avg_interval raw + // samples through take_sample() (each returning an AdcSample -- a + // kAdcNoSignal value is itself a legitimate, timestamped capture + // attempt, not an exceptional error, matching average_interval()'s own + // handling), reduces them via average_interval(), and appends the + // result to this endpoint's pending accumulator. Reports + // AdcErrc::pending_values_full rather than growing without bound if the + // accumulator is already at its kAdcMaxCombineValues capacity. template - std::error_code request_reading(const AdcAveragingConfig& cfg, SampleFn take_sample, - uint16_t& out_value) { - if (cfg.adc_avg_intervals_per_request == 0 || cfg.adc_combine_avg_values == 0) - return make_error_code(AdcErrc::invalid_averaging_config); - - std::vector averaged_intervals; - averaged_intervals.reserve(cfg.adc_combine_avg_values); - for (uint16_t i = 0; i < cfg.adc_combine_avg_values; ++i) { - std::vector raw; - raw.reserve(cfg.adc_avg_intervals_per_request); - for (uint16_t j = 0; j < cfg.adc_avg_intervals_per_request; ++j) { - std::optional sample = take_sample(); - if (!sample.has_value()) return make_error_code(AdcErrc::no_signal); - raw.push_back(*sample); - } - uint16_t interval_avg = 0; - auto ec = compute_average(raw, interval_avg); - if (ec) return ec; - averaged_intervals.push_back(interval_avg); - } - return compute_average(averaged_intervals, out_value); + std::error_code execute_measurement_cycle(const AdcFunctionalConfig& cfg, SampleFn take_sample) { + std::vector samples; + samples.reserve(cfg.adc_samples_per_avg_interval); + for (uint16_t i = 0; i < cfg.adc_samples_per_avg_interval; ++i) samples.push_back(take_sample()); + + const AdcAvgValue avg = average_interval(samples); + if (!pending_.push_back(avg)) return make_error_code(AdcErrc::pending_values_full); + return {}; } - // request_reading_from_trigger_queue implements the ExternalTrigger - // cadence pattern: raw samples are not pulled on demand but have - // already been captured once per external trigger occurrence (e.g. - // rcp/pwm.hpp's PwmInEndpoint mid-pulse signal) and queued by the - // caller in `triggered_samples`, oldest first. This function consumes - // exactly enough entries to satisfy the averaging config, reporting - // AdcErrc::no_signal on either a std::nullopt entry (a trigger occurred - // with no valid capture) or a queue underrun (fewer trigger occurrences - // than required). - std::error_code request_reading_from_trigger_queue( - const AdcAveragingConfig& cfg, std::vector>& triggered_samples, - uint16_t& out_value) { - if (cfg.adc_avg_intervals_per_request == 0 || cfg.adc_combine_avg_values == 0) - return make_error_code(AdcErrc::invalid_averaging_config); - - const size_t needed = - static_cast(cfg.adc_avg_intervals_per_request) * cfg.adc_combine_avg_values; - if (triggered_samples.size() < needed) return make_error_code(AdcErrc::no_signal); - - std::vector averaged_intervals; - averaged_intervals.reserve(cfg.adc_combine_avg_values); - size_t cursor = 0; - for (uint16_t i = 0; i < cfg.adc_combine_avg_values; ++i) { - std::vector raw; - raw.reserve(cfg.adc_avg_intervals_per_request); - for (uint16_t j = 0; j < cfg.adc_avg_intervals_per_request; ++j, ++cursor) { - if (!triggered_samples[cursor].has_value()) return make_error_code(AdcErrc::no_signal); - raw.push_back(*triggered_samples[cursor]); - } - uint16_t interval_avg = 0; - auto ec = compute_average(raw, interval_avg); - if (ec) return ec; - averaged_intervals.push_back(interval_avg); - } - triggered_samples.erase(triggered_samples.begin(), - triggered_samples.begin() + static_cast(needed)); - return compute_average(averaged_intervals, out_value); + // response_ready reports whether enough pending averaged values have + // accumulated to assemble one response (REQ-ADC-053). + bool response_ready(const AdcFunctionalConfig& cfg) const noexcept { + return cadence_response_ready(pending_.size(), cfg.adc_combine_avg_values); } - // 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 + // collect_response pops cfg.adc_combine_avg_values pending averaged + // values (oldest first) into out_values verbatim, reports the response's + // own capture-moment timestamp, and removes exactly those entries from + // the pending accumulator. Fails with AdcErrc::response_not_ready + // (leaving the accumulator untouched) if response_ready() would report + // false. + std::error_code collect_response(const AdcFunctionalConfig& cfg, std::vector& out_values, + uint64_t& out_timestamp) { + if (!response_ready(cfg)) return make_error_code(AdcErrc::response_not_ready); + + const size_t n = cfg.adc_combine_avg_values; + std::vector front; + front.reserve(n); + for (size_t i = 0; i < n; ++i) front.push_back(pending_[i]); + + collect_response_values(front, n, out_values); + out_timestamp = capture_moment_timestamp(front); + pending_.pop_front_n(n); + return {}; } + + size_t pending_count() const noexcept { return pending_.size(); } + +private: + detail::AdcPendingBuffer pending_; }; } // namespace adc diff --git a/include/rcp/gpio.hpp b/include/rcp/gpio.hpp index b73d0cf..7d6457d 100644 --- a/include/rcp/gpio.hpp +++ b/include/rcp/gpio.hpp @@ -7,21 +7,78 @@ // fusa:req REQ-GPIO-007 // fusa:req REQ-GPIO-008 // fusa:req REQ-GPIO-009 +// fusa:req REQ-GPIO-010 +// fusa:req REQ-GPIO-011 +// fusa:req REQ-GPIO-012 +// fusa:req REQ-GPIO-013 +// fusa:req REQ-GPIO-014 +// fusa:req REQ-GPIO-015 +// fusa:req REQ-GPIO-016 +// fusa:req REQ-GPIO-017 +// fusa:req REQ-GPIO-018 +// fusa:req REQ-GPIO-019 +// fusa:req REQ-GPIO-020 +// fusa:req REQ-GPIO-021 +// fusa:req REQ-GPIO-022 +// fusa:req REQ-GPIO-023 +// fusa:req REQ-GPIO-024 +// fusa:req REQ-GPIO-025 +// fusa:req REQ-GPIO-026 +// fusa:req REQ-GPIO-027 +// fusa:req REQ-GPIO-028 +// fusa:req REQ-GPIO-029 +// fusa:req REQ-GPIO-030 +// fusa:req REQ-GPIO-031 +// fusa:req REQ-GPIO-032 +// fusa:req REQ-GPIO-033 +// fusa:req REQ-GPIO-034 +// fusa:req REQ-GPIO-035 +// fusa:req REQ-GPIO-036 +// fusa:req REQ-GPIO-037 +// fusa:req REQ-GPIO-038 +// fusa:req REQ-GPIO-039 +// fusa:req REQ-GPIO-040 +// fusa:req REQ-GPIO-041 +// fusa:req REQ-GPIO-042 +// fusa:req REQ-GPIO-043 +// fusa:req REQ-GPIO-044 +// fusa:req REQ-GPIO-045 +// fusa:req REQ-GPIO-046 -// GPIO endpoint (ep_type 0x02) — the OPEN Alliance TC18 Remote Control -// Protocol Specification v0.5.1_RC's simplest endpoint type: a 32-pin -// bitmask read/written with one of eight evt[2:0]-selected write semantics, -// plus per-pin change/rising/falling trigger signals (extraction §5.3, -// §4.5 Group C). +// GPIO endpoint (ep_type 0x02) — a 32-pin bitmask read/written with one of +// eight evt[2:0]-selected write semantics, per-pin change/rising/falling +// trigger signals, per-pin debounce filtering, response-timing +// classification, a real ACF-level wire codec, and the EP_func +// functional-configuration register block (evt[2:0] == 111b) (extraction +// §5.3, §4.5 Group C). // -// ROADMAP.md milestone 47, "Basic Endpoint Types I — GPIO & SPI (v2.3.0)": -// GPIO is the first concrete endpoint type built on rcp/endpoint.hpp's -// shared write-semantics decode, saturating-arithmetic helpers, and -// trigger-signal table (v2.3.0), and is also the vehicle that exercises the -// add/subtract saturation rule PWM_OUT reuses at v2.4.0. It rides on -// rcp/regmap.hpp's generic/functional endpoint config split (v2.1.0) for -// its functional config block and on rcp/avtp.hpp's big-endian field codec -// helpers (v2.0.0) for its 4-byte payload, without changing either header. +// ROADMAP.md Phase 17 / cpp-RCP issue #129, Phase 3 ("Per-endpoint +// modules"): this header is re-derived from c-RCP's ep_gpio.h/ep_gpio.c — +// c-RCP's RC5-conformant reference implementation for this endpoint type — +// rather than incrementally patched, per the roadmap's own module-by-module +// rewrite plan. No text from the OPEN Alliance TC18 Remote Control Protocol +// Specification is reproduced here; field names and behavior below +// implement TC18's *behavior* as ported from c-RCP's own implementation of +// an internal structured extraction of the specification. +// +// Content re-verified against c-RCP's *current* ep_gpio.h/.c (Phase 3 task +// instruction, given this repo's own earlier "Table 30 Row-2" pilot-module +// history for the sibling rcp/adc.hpp): this header's own pre-Phase-3 +// content (GPIO pin mask, GpioState, apply_gpio_write's input-pin write +// masking (issue #105, cpp-RCP-15), the 4-byte payload codec, and the +// per-pin trigger-signal TriggerRegistry wiring) was already correct and is +// preserved unchanged below — c-RCP's own rcp_ep_gpio_apply_masked_write() +// implements the identical input-pin masking rule this header's +// apply_gpio_write() already had. What was genuinely missing, ported below +// for the first time: the real ACF-level wire codec (encode/decode of read +// requests, write requests including the reserved evt[2:0]=100b rejection, +// and responses), the EP_func functional-configuration register block +// (render_registers()/apply_reconfig(), evt[2:0]==111b), per-pin debounce +// filtering (REQ-GPIO-035), response-timing classification +// (REQ-GPIO-036), and Table 43's own wire trigger-signal numbering +// (REQ-GPIO-034) — distinct from this header's own internal +// TriggerRegistry::SignalId encoding (gpio_signal_id below), which remains +// this codebase's own bookkeeping scheme, not a wire-visible value. // // Field names and behavior below implement TC18's *behavior* as described in // an internal structured extraction of the specification named above; no @@ -31,13 +88,16 @@ // disclaimers in rcp/avtp.hpp, rcp/regmap.hpp, and rcp/endpoint.hpp. #pragma once +#include #include #include +#include #include #include #include #include +#include #include #include @@ -49,7 +109,7 @@ namespace gpio { // pin, up to 32 pins (extraction §5.3). using PinMask = uint32_t; -constexpr uint8_t kMaxPins = 32; +constexpr uint8_t kMaxPins = 32; constexpr size_t kGpioPayloadLen = sizeof(PinMask); // ── GpioState ───────────────────────────────────────────────────────────────── @@ -68,6 +128,24 @@ struct GpioState { enum class GpioErrc : int { pin_index_out_of_range = 1, // a pin index >= kMaxPins was referenced + short_frame = 2, + bad_msg_type = 3, + wrong_bus = 4, + wrong_op = 5, + bad_payload_len = 6, + // TC18 §13.5 Table 33's GPIO/PWM_OUT row, evt[2:0]=100b: "reserved -- + // request shall be ignored and an err-response with error code = + // UNSUPPORTED_CMD shall be sent" -- the wire-decode half of that rule + // (apply_gpio_write already implements the "ignored" half by rejecting + // WriteSemantics::Reserved without mutating state). + reserved_evt = 7, + // Configuration write (evt[2:0]==111b) payload carries no address + // prefix, or an address prefix with no data octet after it. + reconfig_short = 8, + // Configuration write's start_address + data length exceeds + // kGpioEpFuncLen -- the whole write is ignored, per the specification's + // own rule. + reconfig_out_of_range = 9, }; inline const std::error_category& gpio_category() noexcept { @@ -76,6 +154,17 @@ inline const std::error_category& gpio_category() noexcept { std::string message(int ev) const override { switch (static_cast(ev)) { case GpioErrc::pin_index_out_of_range: return "rcp/gpio: pin index out of range"; + case GpioErrc::short_frame: return "rcp/gpio: frame too short"; + case GpioErrc::bad_msg_type: return "rcp/gpio: unexpected ACF message type"; + case GpioErrc::wrong_bus: return "rcp/gpio: wrong byte_bus_id"; + case GpioErrc::wrong_op: return "rcp/gpio: wrong ACF op"; + case GpioErrc::bad_payload_len: return "rcp/gpio: unexpected payload length"; + case GpioErrc::reserved_evt: + return "rcp/gpio: evt[2:0] is the reserved value 100b"; + case GpioErrc::reconfig_short: + return "rcp/gpio: GPIO configuration write has no address and data"; + case GpioErrc::reconfig_out_of_range: + return "rcp/gpio: GPIO configuration write extends past the EP_func block"; default: return "rcp/gpio: unknown error"; } } @@ -88,6 +177,18 @@ inline std::error_code make_error_code(GpioErrc e) noexcept { return {static_cast(e), gpio_category()}; } +// wire_error maps e to its numbered wire error code (acf::WireErrorCode), +// for a caller building an Error Response frame once a request has failed +// to decode. std::nullopt for every GpioErrc value with no numbered +// counterpart. +inline std::optional wire_error(GpioErrc e) noexcept { + switch (e) { + case GpioErrc::bad_payload_len: return acf::WireErrorCode::InvalidParameter; + case GpioErrc::reserved_evt: return acf::WireErrorCode::UnsupportedCmd; + default: return std::nullopt; + } +} + // ── Write-semantics application ────────────────────────────────────────────── // apply_gpio_write is GPIO's endpoint-specific completion of // endpoint::apply_bitmask_write: it handles the six generic combinators by @@ -96,25 +197,45 @@ inline std::error_code make_error_code(GpioErrc e) noexcept { // (replacing, not combining, `state.directions`) rather than a value // combined with the pin-level state (extraction §5.3, §4.5 Group C). // -// Input-pin write masking (issue #105, cpp-RCP-15): TC18 §13.7.4.3 states -// that a write request to a pin currently configured as an input is ignored -// for that pin — inputs are driven externally, and a write combinator's -// result for those bit positions must not be committed to state.values. -// This applied uniformly and without exception before this fix: every write -// semantics (including a bare Replace) overwrote every addressed bit -// regardless of state.directions, so a request could silently corrupt an -// input pin's externally-driven value. The masking below commits the -// combinator's result only for bits where directions == output (1), -// preserving state.values unchanged wherever directions == input (0). +// Subtract operand order (REQ-GPIO-011, found during the Phase 3 +// content-parity pass): TC18's evt[2:0]=110b row (shared normatively by +// GPIO and PWM_OUT) defines this operation as "byte_msg_payload minus +// current interface status" — request MINUS current, not the reverse. +// rcp::endpoint::apply_bitmask_write's own Subtract branch instead computes +// current MINUS operand (saturating_subtract(current, operand)) — the +// opposite order, verified wrong against c-RCP's own +// rcp_ep_gpio_apply_write()'s RCP_EP_GPIO_WRITE_SUB case ("(current > +// request) ? 0u : (request - current)"), c-RCP being this project's +// RC5-conformant reference. Rather than change that shared helper's +// Subtract behavior for every caller (which would also silently change +// rcp/pwm.hpp's PWM_OUT semantics — a different endpoint type, out of this +// phase's own scope, not yet re-verified against its own current c-RCP +// counterpart), this function special-cases Subtract locally, calling +// endpoint::saturating_subtract directly with the operand order this row +// actually specifies. +// +// Input-pin write masking (issue #105, cpp-RCP-15; c-RCP's +// rcp_ep_gpio_apply_masked_write() implements the identical rule): TC18 +// §13.7.4.3 states that a write request to a pin currently configured as an +// input is ignored for that pin — inputs are driven externally, and a write +// combinator's result for those bit positions must not be committed to +// state.values. The masking below commits the combinator's result only for +// bits where directions == output (1), preserving state.values unchanged +// wherever directions == input (0). inline std::error_code apply_gpio_write(endpoint::WriteSemantics op, GpioState& state, PinMask operand) noexcept { if (op == endpoint::WriteSemantics::Reconfigure) { state.directions = operand; return {}; } + uint32_t out = 0; - auto ec = endpoint::apply_bitmask_write(op, state.values, operand, out); - if (ec) return ec; + if (op == endpoint::WriteSemantics::Subtract) { + out = endpoint::saturating_subtract(operand, state.values); // request minus current + } else { + auto ec = endpoint::apply_bitmask_write(op, state.values, operand, out); + if (ec) return ec; + } state.values = (out & state.directions) | (state.values & ~state.directions); return {}; } @@ -145,7 +266,9 @@ inline std::error_code decode_gpio_payload(const uint8_t* buf, size_t len, PinMa // built on rcp/endpoint.hpp's generic TriggerRegistry. gpio_signal_id packs // (pin, edge) into one TriggerRegistry::SignalId so all three of a pin's // signals — and every other pin's — share one registry instance per GPIO -// endpoint. +// endpoint. This is this codebase's own internal bookkeeping scheme, kept +// deliberately distinct from trigger_signal_number() below (Table 43's own +// wire-visible signal numbering, REQ-GPIO-034). enum class GpioEdge : uint8_t { Change = 0, Rising = 1, Falling = 2 }; @@ -176,13 +299,490 @@ evaluate_gpio_triggers(endpoint::TriggerRegistry& triggers, PinMask old_values, return fired; } -// ── Functional config block wiring ──────────────────────────────────────────── -// Interprets regmap::EndpointFunctionalConfig::data (left as an opaque byte -// blob by v2.1.0's generic/functional split) for a GPIO endpoint: the -// pin-direction mask followed by one enabled-edge bitmask byte per pin -// (bits 0-2 corresponding to GpioEdge::Change/Rising/Falling). This is the -// pattern later endpoint types are expected to follow for their own -// functional config content (extraction §6 item 5). +// ── Per-pin trigger mode (functional config) & Table 43 wire numbering ────── +// GpioTrigger names the three asynchronous-event trigger modes a pin's +// functional config may select, plus None — ordinals deliberately equal to +// Table 43's own per-pin offset (None=0, AnyChange=1, Rising=2, Falling=3, +// matching c-RCP's rcp_ep_gpio_trigger_t exactly), so trigger_signal_number +// below is exactly 3*pin_index + trigger with no per-case arithmetic. + +enum class GpioTrigger : uint8_t { None = 0, AnyChange = 1, Rising = 2, Falling = 3 }; + +// trigger_fires: true iff a level transition from prev_level to new_level +// satisfies trigger — never for None; for AnyChange iff prev_level != +// new_level; for Rising iff prev_level is false and new_level is true; for +// Falling iff prev_level is true and new_level is false. +inline bool trigger_fires(GpioTrigger trigger, bool prev_level, bool new_level) noexcept { + switch (trigger) { + case GpioTrigger::AnyChange: return prev_level != new_level; + case GpioTrigger::Rising: return !prev_level && new_level; + case GpioTrigger::Falling: return prev_level && !new_level; + case GpioTrigger::None: + default: return false; + } +} + +// REQ-GPIO-034: TC18 §13.7.4.1 Table 43 (RC5)'s own trigger signal +// numbering — signal 0 is "GPIO EP request execution done" (a whole- +// endpoint trigger, not modeled by this per-pin function), and for each pin +// IOn: signal 3n+1 is AnyChange, 3n+2 is Rising, 3n+3 is Falling, running up +// to signal 96 for IO31's own Falling entry. Returns the signal number iff +// pin_index < kMaxPins and trigger is one of AnyChange/Rising/Falling +// (never GpioTrigger::None, which names no trigger event and therefore no +// Table 43 signal number); std::nullopt otherwise. +inline std::optional trigger_signal_number(uint8_t pin_index, GpioTrigger trigger) noexcept { + if (pin_index >= kMaxPins) return std::nullopt; + switch (trigger) { + case GpioTrigger::AnyChange: + case GpioTrigger::Rising: + case GpioTrigger::Falling: + return static_cast(3u * pin_index + static_cast(trigger)); + case GpioTrigger::None: + default: + return std::nullopt; + } +} + +// ── Debounce filtering (REQ-GPIO-035/044) ───────────────────────────────────── +// GpioDebounceState is one pin's own debounce-filter state — the +// caller-owned tracker every other stateful primitive in this codebase uses +// (matches AdcTriggerState's own "one small struct per thing being tracked" +// convention). + +struct GpioDebounceState { + bool has_settled = false; // false until the first sample settles + bool settled_value = false; // the pin's current, debounced output value + bool has_candidate = false; // whether a same-value run is in progress + bool candidate_value = false; // the value being counted, if has_candidate + uint8_t consecutive_count = 0; // how many consecutive samples of + // candidate_value have been seen so far +}; + +inline void debounce_state_init(GpioDebounceState& s) noexcept { s = GpioDebounceState{}; } + +// TC18 §13.7.4.2 Table 44's own gpio_debounce_IOn rule: "0: no debounce; +// n>0: n consecutive samples of the same value need to be sampled before +// the output value is changed." This function is the pure decision the +// rule describes for one newly sampled raw pin value; the actual periodic +// sampling cadence (gpio_base_clk/gpio_clk_divider) remains a caller-owned +// timer this module never itself runs, matching every other endpoint +// type's own "never owns a timer, thread, or hardware" scope boundary. +// +// n == 0 (no debounce): raw_value becomes the settled value immediately, +// every call. n > 0: raw_value must be observed n consecutive times before +// it becomes the new settled value; a raw_value that differs from the value +// currently being counted resets the count to 1 for the new value — a +// single differing sample discards any partial run, it does not merely +// pause it. +// +// Returns s's settled value AFTER this call — false before the very first +// debounce window ever completes (there is no settled value yet; this +// deliberately does not leak the raw, unfiltered sample). *out_changed, +// when non-null, is set to whether this call's settled-value return +// differs from the settled value before this call — always false on the +// very first call. +inline bool debounce_sample(GpioDebounceState& s, bool raw_value, uint8_t n, bool* out_changed) noexcept { + const bool prev_settled = s.has_settled && s.settled_value; + + if (n == 0) { + s.has_candidate = false; + const bool changed = s.has_settled && (prev_settled != raw_value); + s.has_settled = true; + s.settled_value = raw_value; + if (out_changed) *out_changed = changed; + return s.settled_value; + } + + if (!s.has_candidate || s.candidate_value != raw_value) { + s.has_candidate = true; + s.candidate_value = raw_value; + s.consecutive_count = 1; + } else if (s.consecutive_count < 0xFF) { + ++s.consecutive_count; + } + + bool changed = false; + if (s.consecutive_count >= n && (!s.has_settled || s.settled_value != s.candidate_value)) { + changed = s.has_settled; // first-ever settle isn't a "change" + s.has_settled = true; + s.settled_value = s.candidate_value; + } + + if (out_changed) *out_changed = changed; + return s.has_settled ? s.settled_value : false; +} + +// ── Response timing (REQ-GPIO-036) ──────────────────────────────────────────── + +enum class GpioResponseTiming : uint8_t { + Immediate = 0, // pure read (no payload): respond immediately on execution + AfterDebounce = 1, // payload-bearing read, or any write: change the pin drive + // first, then wait the configured debounce time before + // responding +}; + +// TC18 §13.7.4.3: "A read request without a byte_msg_payload (pure read) +// generates a response immediately upon execution. A read request with a +// byte_msg_payload as well as a write request first change the drive of +// the pins, then wait for the debounce time before creating a response." +// `is_write` is acf::AcfMessageInfo::op's own read/write convention (false +// = read, true = write) — a caller that has already decoded the ACF header +// already has both values in hand. For is_write, payload_len is irrelevant +// to the outcome (a write always debounces) but is still accepted to keep +// the call site uniform. +inline GpioResponseTiming response_timing(bool is_write, size_t payload_len) noexcept { + if (is_write) return GpioResponseTiming::AfterDebounce; + return (payload_len == 0) ? GpioResponseTiming::Immediate : GpioResponseTiming::AfterDebounce; +} + +// ── Functional config ───────────────────────────────────────────────────────── +// Flattens regmap.h's shared functional-config "common" prefix directly +// into this struct's own bools, same rationale as rcp/adc.hpp's +// AdcFunctionalConfig (cpp-RCP's rcp/regmap.hpp leaves +// EndpointFunctionalConfig::data as an opaque byte blob; render_registers()/ +// apply_reconfig() below are this endpoint type's own full interpretation +// of that blob). + +struct GpioPinCfg { + // This implementation's own pin-direction bit convention (bit0 = + // output when set), matching GpioState::directions' own convention + // above — cpp-RCP's rcp/regmap.hpp defines no RCP_REGMAP_PIN_PROP_* + // bitmask of its own (unlike c-RCP's regmap.h), so this endpoint type + // owns the encoding, per this file's own "concrete ... bit convention + // ... this implementation's own" disclaimer. + uint8_t pin_property = 0; + GpioTrigger trigger = GpioTrigger::None; +}; + +constexpr uint8_t kPinPropOutput = 0x01; + +struct GpioFunctionalConfig { + bool ep_enable = false; + bool ep_clear_req_storage = false; + bool ep_req_crc_enable = false; + bool ep_response_ts_enable = false; + bool ep_suppress_response = false; + + std::array pins{}; + uint16_t ep_status = 0; // gpio_ep_status + uint8_t clk_divider = 0; // gpio_clk_divider + std::array debounce{}; // gpio_debounce_IO0..IO31 +}; + +// functional_cfg_writable is a thin, named wrapper over +// rcp::lifecycle::field_writable() with FieldKind::FunctionalW, reusing -- +// never duplicating -- that function's authorization logic. +inline bool functional_cfg_writable(lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + return lifecycle::field_writable(state, lifecycle::FieldKind::FunctionalW, writer); +} + +// set_pin_property sets cfg.pins[pin_index].pin_property iff pin_index is +// valid and functional_cfg_writable() authorizes the write; returns whether +// the write was applied. cfg is left entirely unchanged when it returns +// false. +inline bool set_pin_property(GpioFunctionalConfig& cfg, uint8_t pin_index, uint8_t pin_property, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (pin_index >= kMaxPins) return false; + if (!functional_cfg_writable(state, writer)) return false; + cfg.pins[pin_index].pin_property = pin_property; + return true; +} + +// Same authorization/validity rule as set_pin_property, for +// cfg.pins[pin_index].trigger. +inline bool set_pin_trigger(GpioFunctionalConfig& cfg, uint8_t pin_index, GpioTrigger trigger, + lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + if (pin_index >= kMaxPins) return false; + if (!functional_cfg_writable(state, writer)) return false; + cfg.pins[pin_index].trigger = trigger; + return true; +} + +// ── The EP_func register block (evt[2:0] == 111b) ──────────────────────────── +// Relative addresses within this endpoint's own EP_func block. Known +// editorial defect in the source table, resolved the same way c-RCP's own +// ep_gpio.h resolves it: the table's own explicit, non-elided rows +// establish gpio_debounce_IO0 at 0x0009 and gpio_debounce_IO1 at 0x000A — +// one octet per register, in pin order — but the table's own summary label +// for the elided range's last entry ("0x0024 gpio_debounce_IO31") is +// arithmetically inconsistent with that pattern (0x0009 + 31 = 0x0028, not +// 0x0024). This module places gpio_debounce_IO31 at the +// arithmetically-consistent 0x0028 (kGpioEpFuncLen = 0x0029, 41 octets +// total). + +constexpr uint16_t kGpioRegEpLen = 0x0000; // 8 bit, R +constexpr uint16_t kGpioRegIoMax = 0x0001; // 8 bit, R +constexpr uint16_t kGpioRegEpEnableClr = 0x0002; // 8 bit, R/W +constexpr uint16_t kGpioRegEpOptions = 0x0003; // 8 bit, R/W +constexpr uint16_t kGpioRegBaseClk = 0x0004; // 16 bit, R +constexpr uint16_t kGpioRegEpStatus = 0x0006; // 16 bit, R/W +constexpr uint16_t kGpioRegClkDivider = 0x0008; // 8 bit, R/W +constexpr uint16_t kGpioRegDebounceIo0 = 0x0009; // 8 bit, R/W; IO(n) at 0x0009+n + +constexpr uint16_t kGpioEpFuncLen = 0x0029; +constexpr size_t kGpioReconfigAddrLen = 2; + +using GpioRegisterBlock = std::array; + +namespace detail { +constexpr uint8_t kGpioEnableClrBitEnable = 1u << 0; +constexpr uint8_t kGpioEnableClrBitClear = 1u << 4; +constexpr uint8_t kGpioOptionsBitReqCrc = 1u << 0; +constexpr uint8_t kGpioOptionsBitRespTs = 1u << 3; +constexpr uint8_t kGpioOptionsBitSuppress = 1u << 7; + +inline bool gpio_reg_offset_read_only(uint16_t addr) noexcept { + return addr == kGpioRegEpLen || addr == kGpioRegIoMax || addr == kGpioRegBaseClk || + addr == static_cast(kGpioRegBaseClk + 1); +} +} // namespace detail + +// render_registers serializes cfg's EP_func registers into out exactly as a +// configuration *read* of the whole block would report them — the inverse +// of apply_reconfig()'s own parse step. +inline void render_registers(const GpioFunctionalConfig& cfg, GpioRegisterBlock& out) noexcept { + uint8_t enable_clr = 0; + uint8_t options = 0; + if (cfg.ep_enable) enable_clr |= detail::kGpioEnableClrBitEnable; + if (cfg.ep_clear_req_storage) enable_clr |= detail::kGpioEnableClrBitClear; + if (cfg.ep_req_crc_enable) options |= detail::kGpioOptionsBitReqCrc; + if (cfg.ep_response_ts_enable) options |= detail::kGpioOptionsBitRespTs; + if (cfg.ep_suppress_response) options |= detail::kGpioOptionsBitSuppress; + + out[kGpioRegEpLen] = static_cast(kGpioEpFuncLen); + out[kGpioRegIoMax] = kMaxPins; + out[kGpioRegEpEnableClr] = enable_clr; + out[kGpioRegEpOptions] = options; + avtp::detail::put_u16(&out[kGpioRegBaseClk], 0); // read-only, no GPIO clock source modelled + avtp::detail::put_u16(&out[kGpioRegEpStatus], cfg.ep_status); + out[kGpioRegClkDivider] = cfg.clk_divider; + for (uint8_t i = 0; i < kMaxPins; ++i) out[kGpioRegDebounceIo0 + i] = cfg.debounce[i]; +} + +namespace detail { +inline void gpio_parse_registers(GpioFunctionalConfig& cfg, const GpioRegisterBlock& in) noexcept { + const uint8_t enable_clr = in[kGpioRegEpEnableClr]; + const uint8_t options = in[kGpioRegEpOptions]; + + cfg.ep_enable = (enable_clr & kGpioEnableClrBitEnable) != 0; + cfg.ep_clear_req_storage = (enable_clr & kGpioEnableClrBitClear) != 0; + cfg.ep_req_crc_enable = (options & kGpioOptionsBitReqCrc) != 0; + cfg.ep_response_ts_enable = (options & kGpioOptionsBitRespTs) != 0; + cfg.ep_suppress_response = (options & kGpioOptionsBitSuppress) != 0; + + cfg.ep_status = avtp::detail::get_u16(&in[kGpioRegEpStatus]); + cfg.clk_divider = in[kGpioRegClkDivider]; + for (uint8_t i = 0; i < kMaxPins; ++i) cfg.debounce[i] = in[kGpioRegDebounceIo0 + i]; +} +} // namespace detail + +// apply_reconfig applies the real configuration escape hatch (evt[2:0] == +// 111b): payload is a 16-bit big-endian relative start address followed by +// the configuration data octets to write from that address onward +// (extraction §3.7.1, TC18 §12.7.1 Figure 18). Returns +// GpioErrc::reconfig_short when payload_len is not at least +// kGpioReconfigAddrLen + 1, and GpioErrc::reconfig_out_of_range when the +// addressed span would extend past kGpioEpFuncLen; in both cases cfg is +// left entirely unchanged. Octets of the addressed span that land on a +// read-only register (EP_LEN, IO_MAX, base_clk) are left at their current +// values while the rest of the span is still applied. +inline std::error_code apply_reconfig(GpioFunctionalConfig& cfg, const uint8_t* payload, + size_t payload_len) { + if (payload_len <= kGpioReconfigAddrLen) return make_error_code(GpioErrc::reconfig_short); + + const uint16_t start_address = avtp::detail::get_u16(payload); + const size_t data_len = payload_len - kGpioReconfigAddrLen; + + if (static_cast(start_address) + data_len > static_cast(kGpioEpFuncLen)) + return make_error_code(GpioErrc::reconfig_out_of_range); + + GpioRegisterBlock block{}; + render_registers(cfg, block); + for (size_t i = 0; i < data_len; ++i) { + const auto addr = static_cast(start_address + i); + if (detail::gpio_reg_offset_read_only(addr)) continue; + block[addr] = payload[kGpioReconfigAddrLen + i]; + } + detail::gpio_parse_registers(cfg, block); + return {}; +} + +// ── Wire codec ───────────────────────────────────────────────────────────────── +// Ported directly from c-RCP's ep_gpio.c wire functions, using rcp/acf.hpp's +// ACF_ABB/ACF_GBB codec rather than re-deriving frame layout here. + +// encode_read_request encodes an ACF_ABB read request addressed to +// byte_bus_id, with no payload. +inline std::vector encode_read_request(avtp::ByteBusId byte_bus_id, uint8_t transaction_num) { + acf::AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.op = false; // read + info.transaction_num = transaction_num; + return acf::encode_acf_abb(info, {}); +} + +// decode_read_request decodes and validates an ACF-level GPIO read request. +// Fails with GpioErrc::short_frame / bad_msg_type / wrong_bus / wrong_op. +// On success, out_transaction_num is populated. +inline std::error_code decode_read_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + uint8_t& out_transaction_num) { + acf::AcfMessageInfo info; + std::vector payload; + const auto ec = acf::decode_acf_abb(b, len, info, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(GpioErrc::short_frame); + if (ec) return make_error_code(GpioErrc::bad_msg_type); + + if (info.byte_bus_id != expected_bus_id) return make_error_code(GpioErrc::wrong_bus); + if (info.op) return make_error_code(GpioErrc::wrong_op); // op=true means write + + out_transaction_num = info.transaction_num; + return {}; +} + +// encode_write_request encodes an ACF_ABB write request addressed to +// byte_bus_id: evt's low three bits carry evt (masked to 3 bits), and the +// payload is bitmask as kGpioPayloadLen big-endian octets. +inline std::vector encode_write_request(avtp::ByteBusId byte_bus_id, PinMask bitmask, + endpoint::WriteSemantics evt, uint8_t transaction_num) { + acf::AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.op = true; // write + info.evt_op = static_cast(static_cast(evt) & 0x7); + info.transaction_num = transaction_num; + return acf::encode_acf_abb(info, encode_gpio_payload(bitmask)); +} + +// decode_write_request decodes and validates a GPIO write request. Same +// ACF-level failure modes as decode_read_request (short frame / bad msg +// type / wrong bus), except GpioErrc::wrong_op is returned when op is not +// write, and GpioErrc::bad_payload_len when the payload is not exactly +// kGpioPayloadLen octets. +// +// REQ-GPIO-012/045: GpioErrc::reserved_evt is returned when evt[2:0] == +// WriteSemantics::Reserved (100b), Table 33's own GPIO/PWM_OUT row's +// reserved value — neither out_bitmask, out_evt, nor out_transaction_num is +// populated in that case; a caller builds the required err-response via +// wire_error(), which maps this errc to acf::WireErrorCode::UnsupportedCmd. +// +// On success, out_bitmask, out_evt, and out_transaction_num are populated. +inline std::error_code decode_write_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + PinMask& out_bitmask, endpoint::WriteSemantics& out_evt, + uint8_t& out_transaction_num) { + acf::AcfMessageInfo info; + std::vector payload; + const auto ec = acf::decode_acf_abb(b, len, info, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(GpioErrc::short_frame); + if (ec) return make_error_code(GpioErrc::bad_msg_type); + + if (info.byte_bus_id != expected_bus_id) return make_error_code(GpioErrc::wrong_bus); + if (!info.op) return make_error_code(GpioErrc::wrong_op); // op=false means read + if (payload.size() != kGpioPayloadLen) return make_error_code(GpioErrc::bad_payload_len); + + const auto evt = endpoint::write_semantics_of(info.evt_op); + if (evt == endpoint::WriteSemantics::Reserved) return make_error_code(GpioErrc::reserved_evt); + + out_bitmask = avtp::detail::get_u32(payload.data()); + out_evt = evt; + out_transaction_num = info.transaction_num; + return {}; +} + +// encode_reconfig_request encodes an ACF_ABB configuration request +// (evt[2:0] == 111b) addressed to byte_bus_id: payload is start_address +// (16-bit big-endian) followed by data. Returns an empty vector if data is +// empty or the encoded payload would exceed acf::kAcfAbbMaxPayload. +inline std::vector encode_reconfig_request(avtp::ByteBusId byte_bus_id, uint16_t start_address, + const std::vector& data, + uint8_t transaction_num) { + if (data.empty()) return {}; + if (kGpioReconfigAddrLen + data.size() > acf::kAcfAbbMaxPayload) return {}; + + std::vector payload(kGpioReconfigAddrLen + data.size()); + avtp::detail::put_u16(payload.data(), start_address); + std::copy(data.begin(), data.end(), payload.begin() + static_cast(kGpioReconfigAddrLen)); + + acf::AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.op = true; // write + info.evt_op = static_cast(endpoint::WriteSemantics::Reconfigure); + info.transaction_num = transaction_num; + return acf::encode_acf_abb(info, payload); +} + +// encode_response encodes a GPIO response carrying bitmask as its +// kGpioPayloadLen big-endian payload, echoing transaction_num. Encoded as +// ACF_ABB when timed is false; as ACF_GBB (mtv valid, message_timestamp = +// timestamp) when timed is true. +inline std::vector encode_response(avtp::ByteBusId byte_bus_id, PinMask bitmask, + uint8_t transaction_num, bool timed, uint64_t timestamp) { + const auto payload = encode_gpio_payload(bitmask); + + acf::AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.op = false; // read + info.rsp = true; + info.transaction_num = transaction_num; + + if (timed) { + info.mtv = true; + return acf::encode_acf_gbb(info, timestamp, payload); + } + return acf::encode_acf_abb(info, payload); +} + +// decode_response decodes a GPIO response from either an ACF_ABB or +// ACF_GBB message (peeks the ACF message type itself). Fails with +// GpioErrc::short_frame / wrong_bus / bad_payload_len (payload present but +// not exactly kGpioPayloadLen octets). On success, out_bitmask/ +// out_transaction_num are populated; out_timed/out_timestamp report +// whether the message was a valid-timestamp ACF_GBB, and that timestamp's +// value (0 when !out_timed). +inline std::error_code decode_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + PinMask& out_bitmask, bool& out_timed, uint64_t& out_timestamp, + uint8_t& out_transaction_num) { + uint8_t msg_type = 0; + if (acf::peek_msg_type(b, len, msg_type)) return make_error_code(GpioErrc::short_frame); + + acf::AcfMessageInfo info; + std::vector payload; + avtp::ByteBusId bus_id = 0; + uint8_t txn = 0; + + if (msg_type == acf::kAcfMsgTypeGbb) { + uint64_t ts = 0; + const auto ec = acf::decode_acf_gbb(b, len, info, ts, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(GpioErrc::short_frame); + if (ec) return make_error_code(GpioErrc::bad_msg_type); + bus_id = info.byte_bus_id; + txn = info.transaction_num; + out_timed = info.mtv; + out_timestamp = out_timed ? ts : 0; + } else { + const auto ec = acf::decode_acf_abb(b, len, info, payload); + if (ec == avtp::make_error_code(avtp::AvtpErrc::short_buffer)) return make_error_code(GpioErrc::short_frame); + if (ec) return make_error_code(GpioErrc::bad_msg_type); + bus_id = info.byte_bus_id; + txn = info.transaction_num; + out_timed = false; + out_timestamp = 0; + } + + if (bus_id != expected_bus_id) return make_error_code(GpioErrc::wrong_bus); + if (payload.size() != kGpioPayloadLen) return make_error_code(GpioErrc::bad_payload_len); + + out_bitmask = avtp::detail::get_u32(payload.data()); + out_transaction_num = txn; + return {}; +} + +// ── Functional config block wiring (pre-Phase-3 opaque-blob helpers) ──────── +// Kept unchanged for existing callers: interprets +// regmap::EndpointFunctionalConfig::data as a plain (directions, per-pin +// enabled-edge-mask) pair — a simpler, independent encoding from +// GpioFunctionalConfig/render_registers/apply_reconfig above, which now +// model the *real* TC18 EP_func register block (Table 44) instead. This +// pair predates that real register block and is retained as a lighter +// caller-side convenience where the full register block is unneeded. constexpr size_t kGpioFunctionalConfigLen = kGpioPayloadLen + kMaxPins; @@ -211,9 +811,7 @@ inline std::error_code decode_gpio_functional_config(const regmap::EndpointFunct // ── GpioEndpoint ────────────────────────────────────────────────────────────── // Ties GpioState, apply_gpio_write, and the trigger-signal evaluation above // into the single request-dispatch entry point a caller (e.g. an RC Server's -// request loop) would invoke per incoming GPIO write — the reusable -// endpoint-registration/dispatch object this milestone establishes the shape -// of for SPI (rcp::spi::SpiEndpoint) and later endpoint types to follow. +// request loop) would invoke per incoming GPIO write. class GpioEndpoint { public: // handle_write applies one decoded write request (`op` from diff --git a/include/rcp/mock.hpp b/include/rcp/mock.hpp index bd636bb..91810fa 100644 --- a/include/rcp/mock.hpp +++ b/include/rcp/mock.hpp @@ -186,8 +186,9 @@ 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::reconfig_short) || + ec == make_error_code(adc::AdcErrc::reconfig_out_of_range)) + return acf::WireErrorCode::InvalidParameter; // evt[2:0]==111b config-write payload malformed/out of range (Phase 3, adc.hpp's own apply_reconfig) 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)) @@ -556,33 +557,77 @@ class Server final { // 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*/, + // codebase, not that it is inherently full-duplex). + // + // Table 33 Row 2's 3-way Plain/Reserved/ConfigWrite classification + // (rcp::endpoint::evt_row2_kind_of) is checked directly here (Phase 3: + // adc::AdcEndpoint no longer has its own combined handle_request, since + // c-RCP's own ep_adc.h/.c have no such function either — see adc.hpp's + // own file header): Reserved is rejected without consuming a scripted + // sample or touching adc_cfg_; ConfigWrite applies `payload` as a real + // §12.7.1 addressed register write against this server's own + // adc_cfg_ (adc::apply_reconfig, genuinely implemented as of Phase 3, + // unlike this mock's still-open Table 30/33 dispatch wiring for other + // endpoint types' own reconfig paths — full response-shape/register- + // read-back wiring for every endpoint type remains Phase 4's scope, see + // ROADMAP.md); Plain drives one AdcEndpoint::execute_measurement_cycle + // (samples_per_avg_interval=1, combine_avg_values=1: no averaging, one + // raw sample per request) against a take_sample callback that pulls the + // next scripted value set_adc_response() queued. An empty scripted + // queue reports a kAdcNoSignal-valued sample, which average_interval() + // carries through as the response's own value on the wire (adc.hpp's + // own corrected behavior) — this mock chooses to additionally surface + // that as an AdcErrc::no_signal *error* response instead, preserving + // this dispatch path's own pre-existing "queue underrun is an error" + // contract for set_adc_response()'s own callers/tests. + 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; + + switch (endpoint::evt_row2_kind_of(req.evt_op)) { + case endpoint::EvtRow2Kind::Reserved: + return set_error_response(req, endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2), + out_resp, out_resp_payload); + case endpoint::EvtRow2Kind::ConfigWrite: { + auto ec = adc::apply_reconfig(adc_cfg_, payload.data(), payload.size()); + if (ec) return set_error_response(req, ec, out_resp, out_resp_payload); + out_resp = acf::make_response(req, acf::ResponseKind::WriteResponse); + return {}; + } + case endpoint::EvtRow2Kind::Plain: + break; + } + + adc::AdcFunctionalConfig cfg; + cfg.adc_samples_per_avg_interval = 1; + cfg.adc_combine_avg_values = 1; + + auto take_sample = [this]() -> adc::AdcSample { + if (adc_samples_.empty()) return adc::AdcSample{adc::kAdcNoSignal, 0}; uint16_t v = adc_samples_.front(); adc_samples_.pop_front(); - return v; + return adc::AdcSample{v, 0}; }; - uint16_t value = 0; - auto ec = adc_.handle_request(req.evt_op, adc::AdcAveragingConfig{}, take_sample, value); + + auto ec = adc_.execute_measurement_cycle(cfg, take_sample); + if (ec) return set_error_response(req, ec, out_resp, out_resp_payload); + + std::vector values; + uint64_t timestamp = 0; + ec = adc_.collect_response(cfg, values, timestamp); if (ec) return set_error_response(req, ec, out_resp, out_resp_payload); - out_resp_payload = adc::encode_adc_value(value); + + if (values.front() == adc::kAdcNoSignal) { + return set_error_response(req, adc::make_error_code(adc::AdcErrc::no_signal), out_resp, + out_resp_payload); + } + + out_resp_payload.resize(adc::kAdcValueLen); + avtp::detail::put_u16(out_resp_payload.data(), values.front()); out_resp = acf::make_response(req, req.evt_ack ? acf::ResponseKind::Acknowledge : acf::ResponseKind::ReadResponse); return {}; @@ -864,6 +909,7 @@ class Server final { spi::SpiEndpoint spi_; i2c::I2cEndpoint i2c_; adc::AdcEndpoint adc_; + adc::AdcFunctionalConfig adc_cfg_; // backs dispatch_adc's evt[2:0]==111b configuration-write path pwm::PwmInEndpoint pwm_in_; lin::LinEndpoint lin_; can::CanEndpoint can_; diff --git a/tests/test_adc.cpp b/tests/test_adc.cpp index d1e9bed..2e44cf5 100644 --- a/tests/test_adc.cpp +++ b/tests/test_adc.cpp @@ -7,261 +7,724 @@ // 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). +// fusa:test REQ-ADC-010 +// fusa:test REQ-ADC-011 +// fusa:test REQ-ADC-012 +// fusa:test REQ-ADC-013 +// fusa:test REQ-ADC-014 +// fusa:test REQ-ADC-015 +// fusa:test REQ-ADC-016 +// fusa:test REQ-ADC-017 +// fusa:test REQ-ADC-018 +// fusa:test REQ-ADC-019 +// fusa:test REQ-ADC-020 +// fusa:test REQ-ADC-021 +// fusa:test REQ-ADC-022 +// fusa:test REQ-ADC-023 +// fusa:test REQ-ADC-024 +// fusa:test REQ-ADC-025 +// fusa:test REQ-ADC-026 +// fusa:test REQ-ADC-027 +// fusa:test REQ-ADC-028 +// fusa:test REQ-ADC-029 +// fusa:test REQ-ADC-030 +// fusa:test REQ-ADC-031 +// fusa:test REQ-ADC-033 +// fusa:test REQ-ADC-037 +// fusa:test REQ-ADC-038 +// fusa:test REQ-ADC-039 +// fusa:test REQ-ADC-040 +// fusa:test REQ-ADC-042 +// fusa:test REQ-ADC-043 +// fusa:test REQ-ADC-044 +// fusa:test REQ-ADC-045 +// fusa:test REQ-ADC-046 +// fusa:test REQ-ADC-047 +// fusa:test REQ-ADC-048 +// fusa:test REQ-ADC-049 +// fusa:test REQ-ADC-050 +// fusa:test REQ-ADC-051 +// fusa:test REQ-ADC-052 +// fusa:test REQ-ADC-053 +// fusa:test REQ-ADC-054 +// fusa:test REQ-ADC-055 + +// Tests for rcp/adc.hpp — the ADC endpoint type, re-derived from c-RCP's +// test_ep_adc.c (Phase 3, cpp-RCP issue #129). #include #include -#include - using namespace rcp::adc; +using rcp::endpoint::EndpointErrc; +using rcp::lifecycle::FieldKind; +using rcp::lifecycle::ServerState; +using rcp::lifecycle::WriterCtx; -// ── Three-level averaging model ────────────────────────────────────────────── +// ── Layer 1: average_interval ───────────────────────────────────────────────── -TEST_CASE("compute_average computes an arithmetic mean", "[adc][REQ-ADC-001]") { - uint16_t out = 0; - REQUIRE_FALSE(compute_average({10, 20, 30}, out)); - REQUIRE(out == 20); +TEST_CASE("average_interval computes the arithmetic mean of raw samples", "[adc][REQ-ADC-002]") { + std::vector samples{{10, 100}, {20, 200}, {30, 300}}; + auto avg = average_interval(samples); + REQUIRE(avg.value == 20); } -TEST_CASE("compute_average reports no_signal on an empty sample set", "[adc][REQ-ADC-001]") { - uint16_t out = 0; - REQUIRE(compute_average({}, out) == make_error_code(AdcErrc::no_signal)); +TEST_CASE("average_interval returns kAdcNoSignal with a zero timestamp when sample_count is 0", + "[adc][REQ-ADC-003]") { + auto avg = average_interval({}); + REQUIRE(avg.value == kAdcNoSignal); + REQUIRE(avg.timestamp == 0); } -// ADC's resolution ceiling is 16 bits (§13.7.9.1) — compute_average must -// average values right up to that ceiling without overflowing internally -// (it widens to a 64-bit accumulator before narrowing the result back to -// uint16_t), unlike the previous uint32_t-everywhere model this replaces. -TEST_CASE("compute_average handles samples at the 16-bit ceiling without overflow", - "[adc][REQ-ADC-001]") { - uint16_t out = 0; - REQUIRE_FALSE(compute_average({0xFFFF, 0xFFFF, 0xFFFF}, out)); - REQUIRE(out == 0xFFFF); +TEST_CASE("average_interval excludes kAdcNoSignal samples from its arithmetic mean", "[adc][REQ-ADC-004]") { + std::vector samples{{10, 1}, {kAdcNoSignal, 2}, {30, 3}}; + auto avg = average_interval(samples); + REQUIRE(avg.value == 20); // mean of 10 and 30 only } -TEST_CASE("request_reading combines level-1 and level-2 averaging correctly", "[adc][REQ-ADC-002]") { - AdcEndpoint ep; - AdcAveragingConfig cfg; - cfg.adc_avg_intervals_per_request = 2; // level 1: average 2 raw samples per interval - cfg.adc_combine_avg_values = 2; // level 2: average 2 intervals - - // Raw samples, consumed in order: interval 1 = {10,20} -> avg 15; - // interval 2 = {30,40} -> avg 35; combined = avg(15,35) = 25. - 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; - }; +TEST_CASE("average_interval reports the timestamp of the last sample that fed the mean", + "[adc][REQ-ADC-005]") { + std::vector samples{{10, 100}, {20, 200}, {kAdcNoSignal, 300}}; + auto avg = average_interval(samples); + REQUIRE(avg.timestamp == 200); // last USED sample, not the interval's literal last sample +} - uint16_t out_value = 0; - auto ec = ep.request_reading(cfg, take_sample, out_value); - REQUIRE_FALSE(ec); - REQUIRE(out_value == 25); +TEST_CASE("average_interval reports the last sample's timestamp when every sample timed out", + "[adc][REQ-ADC-041]") { + std::vector samples{{kAdcNoSignal, 100}, {kAdcNoSignal, 200}}; + auto avg = average_interval(samples); + REQUIRE(avg.value == kAdcNoSignal); + REQUIRE(avg.timestamp == 200); // the interval's own last sample still marks when it closed } -TEST_CASE("request_reading rejects a zero-valued averaging config field", "[adc][REQ-ADC-002]") { - AdcEndpoint ep; - AdcAveragingConfig cfg; - cfg.adc_avg_intervals_per_request = 0; - auto take_sample = []() -> std::optional { return uint16_t{1}; }; +TEST_CASE("average_interval averages samples at the 16-bit ceiling without overflow", "[adc][REQ-ADC-002]") { + std::vector samples{{0xFFFE, 1}, {0xFFFE, 2}, {0xFFFE, 3}}; + auto avg = average_interval(samples); + REQUIRE(avg.value == 0xFFFE); +} + +// ── REQ-ADC-033: inter-sample spacing ───────────────────────────────────────── + +TEST_CASE("validate_sample_spacing distinguishes even from ragged spacing", "[adc][REQ-ADC-033]") { + // base_clk_divider=5, sample_interval=200, base_clk_hz=1e9 -> expected + // spacing exactly 1000ns. + std::vector even{{0, 0}, {0, 1000}, {0, 2000}}; + std::vector ragged{{0, 0}, {0, 5}, {0, 2000}}; - uint16_t out_value = 0; - auto ec = ep.request_reading(cfg, take_sample, out_value); - REQUIRE(ec == make_error_code(AdcErrc::invalid_averaging_config)); + REQUIRE(validate_sample_spacing(even, 5, 200, 1000000000u, 0) == AdcSpacingResult::Ok); + REQUIRE(validate_sample_spacing(ragged, 5, 200, 1000000000u, 0) == AdcSpacingResult::Violation); } -// ── Request-driven sampling only / no-signal timeout path ─────────────────── +TEST_CASE("validate_sample_spacing respects a nonzero tolerance", "[adc][REQ-ADC-033]") { + std::vector s{{0, 0}, {0, 1050}}; // 50ns off the expected 1000ns -TEST_CASE("request_reading reports no_signal when take_sample underruns", "[adc][REQ-ADC-003]") { - AdcEndpoint ep; - AdcAveragingConfig cfg; - cfg.adc_avg_intervals_per_request = 3; - cfg.adc_combine_avg_values = 1; + REQUIRE(validate_sample_spacing(s, 5, 200, 1000000000u, 0) == AdcSpacingResult::Violation); + REQUIRE(validate_sample_spacing(s, 5, 200, 1000000000u, 50) == AdcSpacingResult::Ok); + REQUIRE(validate_sample_spacing(s, 5, 200, 1000000000u, 49) == AdcSpacingResult::Violation); +} - auto take_sample = []() -> std::optional { return std::nullopt; }; // no signal captured +TEST_CASE("validate_sample_spacing fails open without a real clock rate", "[adc][REQ-ADC-033]") { + std::vector s{{0, 0}, {0, 5000}}; + REQUIRE(validate_sample_spacing(s, 1, 1, 0, 0) == AdcSpacingResult::Ok); // base_clk_hz == 0 + REQUIRE(validate_sample_spacing(s, 0, 1, 1000000000u, 0) == AdcSpacingResult::Ok); // divider == 0 + REQUIRE(validate_sample_spacing({s[0]}, 1, 1, 1000000000u, 0) == AdcSpacingResult::Ok); // < 2 samples + REQUIRE(validate_sample_spacing({}, 1, 1, 1000000000u, 0) == AdcSpacingResult::Ok); +} - uint16_t out_value = 0; - auto ec = ep.request_reading(cfg, take_sample, out_value); - REQUIRE(ec == make_error_code(AdcErrc::no_signal)); +TEST_CASE("validate_sample_spacing rejects non-monotonic timestamps", "[adc][REQ-ADC-033]") { + std::vector s{{0, 1000}, {0, 500}}; // went backwards + REQUIRE(validate_sample_spacing(s, 5, 200, 1000000000u, 0) == AdcSpacingResult::Violation); + // Must be caught even with a huge tolerance -- not an accident of the + // (i+1)-minus-i subtraction underflowing. + REQUIRE(validate_sample_spacing(s, 5, 200, 1000000000u, UINT64_MAX - 1000) == + AdcSpacingResult::Violation); } -TEST_CASE("request_reading only invokes take_sample exactly the required number of times", - "[adc][REQ-ADC-003]") { - AdcEndpoint ep; - AdcAveragingConfig cfg; - cfg.adc_avg_intervals_per_request = 2; - cfg.adc_combine_avg_values = 3; +// ── Layers 2/3: collect_response_values / cadence ───────────────────────────── - int calls = 0; - auto take_sample = [&calls]() -> std::optional { - ++calls; - return uint16_t{42}; - }; +TEST_CASE("collect_response_values packs averaged values in capture order, verbatim", + "[adc][REQ-ADC-006][REQ-ADC-009]") { + std::vector avg{{10, 1}, {kAdcNoSignal, 2}, {30, 3}}; + std::vector out; + auto n = collect_response_values(avg, 3, out); + REQUIRE(n == 3); + REQUIRE(out == std::vector{10, kAdcNoSignal, 30}); +} - uint16_t out_value = 0; - REQUIRE_FALSE(ep.request_reading(cfg, take_sample, out_value)); - REQUIRE(calls == 6); // 2 * 3, no free-running/extra sampling - REQUIRE(out_value == 42); +TEST_CASE("collect_response_values reports a short count without touching unwritten entries", + "[adc][REQ-ADC-008]") { + std::vector avg{{10, 1}, {20, 2}}; + std::vector out; + auto n = collect_response_values(avg, 5, out); + REQUIRE(n == 2); + REQUIRE(out.size() == 2); } -// ── Two self-triggering cadence patterns ───────────────────────────────────── +TEST_CASE("collect_response_values returns 0 when either count is 0", "[adc][REQ-ADC-010]") { + std::vector out; + REQUIRE(collect_response_values({}, 3, out) == 0); + REQUIRE(collect_response_values({{10, 1}}, 0, out) == 0); +} -TEST_CASE("request_reading_from_trigger_queue implements the ExternalTrigger cadence pattern", - "[adc][REQ-ADC-004]") { - AdcEndpoint ep; - AdcAveragingConfig cfg; - cfg.adc_avg_intervals_per_request = 2; - cfg.adc_combine_avg_values = 2; +TEST_CASE("response_value_count returns half a request's read_size", "[adc][REQ-ADC-001]") { + REQUIRE(response_value_count(0) == 0); + REQUIRE(response_value_count(2) == 1); + REQUIRE(response_value_count(16) == 8); + REQUIRE(response_value_count(3) == 0); // odd -> no whole number of values +} + +TEST_CASE("cadence_case classifies ACCUMULATE/ONE_TO_ONE/FAN_OUT", "[adc][REQ-ADC-037]") { + REQUIRE(cadence_case(2, 4) == AdcCadenceCase::Accumulate); // combine > intervals + REQUIRE(cadence_case(4, 4) == AdcCadenceCase::OneToOne); // combine == intervals + REQUIRE(cadence_case(4, 2) == AdcCadenceCase::FanOut); // combine < intervals +} + +TEST_CASE("cadence_response_ready compares pending count against combine_avg_values", + "[adc][REQ-ADC-053]") { + REQUIRE(cadence_response_ready(3, 3)); + REQUIRE_FALSE(cadence_response_ready(2, 3)); + REQUIRE(cadence_response_ready(0, 0)); // zero combine is trivially ready +} + +TEST_CASE("capture_moment_timestamp returns the first response value's timestamp", + "[adc][REQ-ADC-012]") { + std::vector avg{{10, 111}, {20, 222}}; + REQUIRE(capture_moment_timestamp(avg) == 111); +} + +TEST_CASE("capture_moment_timestamp returns 0 when avg_count is 0", "[adc][REQ-ADC-013]") { + REQUIRE(capture_moment_timestamp({}) == 0); +} + +// ── Functional config ────────────────────────────────────────────────────────── + +TEST_CASE("AdcFunctionalConfig default-constructs zeroed", "[adc][REQ-ADC-014]") { + AdcFunctionalConfig cfg; + REQUIRE_FALSE(cfg.ep_enable); + REQUIRE(cfg.adc_samples_per_avg_interval == 0); + REQUIRE(cfg.adc_avg_intervals_per_request == 0); + REQUIRE(cfg.adc_combine_avg_values == 0); + REQUIRE(cfg.trigger_min == 0); + REQUIRE(cfg.trigger_max == 0); +} + +TEST_CASE("functional_cfg_writable is unwritable while HwUnconfigured", "[adc][REQ-ADC-015]") { + WriterCtx writer; + writer.via_root_client_ep0 = true; + REQUIRE_FALSE(functional_cfg_writable(ServerState::HwUnconfigured, writer)); +} + +TEST_CASE("functional_cfg_writable requires authorization or a discovery stream while HwConfigured", + "[adc][REQ-ADC-016]") { + WriterCtx authorized; + authorized.via_root_client_ep0 = true; + REQUIRE(functional_cfg_writable(ServerState::HwConfigured, authorized)); + + WriterCtx discovery; + discovery.via_discovery_stream = true; + REQUIRE(functional_cfg_writable(ServerState::HwConfigured, discovery)); + + REQUIRE_FALSE(functional_cfg_writable(ServerState::HwConfigured, WriterCtx{})); +} + +TEST_CASE("functional_cfg_writable requires authorization once RcpConfigured", "[adc][REQ-ADC-017]") { + WriterCtx discovery; + discovery.via_discovery_stream = true; + REQUIRE_FALSE(functional_cfg_writable(ServerState::RcpConfigured, discovery)); // no longer suffices alone + + WriterCtx owning; + owning.via_owning_stream = true; + REQUIRE(functional_cfg_writable(ServerState::RcpConfigured, owning)); +} + +TEST_CASE("set_samples_per_avg_interval rejects an unauthorized write without mutating cfg", + "[adc][REQ-ADC-018]") { + AdcFunctionalConfig cfg; + REQUIRE_FALSE(set_samples_per_avg_interval(cfg, 42, ServerState::HwUnconfigured, WriterCtx{})); + REQUIRE(cfg.adc_samples_per_avg_interval == 0); +} + +TEST_CASE("set_samples_per_avg_interval applies the write when authorized", "[adc][REQ-ADC-019]") { + AdcFunctionalConfig cfg; + WriterCtx writer; + writer.via_root_client_ep0 = true; + REQUIRE(set_samples_per_avg_interval(cfg, 42, ServerState::HwConfigured, writer)); + REQUIRE(cfg.adc_samples_per_avg_interval == 42); +} + +TEST_CASE("set_avg_intervals_per_request rejects unauthorized / applies when authorized", + "[adc][REQ-ADC-020][REQ-ADC-021]") { + AdcFunctionalConfig cfg; + REQUIRE_FALSE(set_avg_intervals_per_request(cfg, 7, ServerState::HwUnconfigured, WriterCtx{})); + REQUIRE(cfg.adc_avg_intervals_per_request == 0); + + WriterCtx writer; + writer.via_owning_stream = true; + REQUIRE(set_avg_intervals_per_request(cfg, 7, ServerState::RcpConfigured, writer)); + REQUIRE(cfg.adc_avg_intervals_per_request == 7); +} + +TEST_CASE("set_combine_avg_values rejects unauthorized / applies when authorized", + "[adc][REQ-ADC-022][REQ-ADC-023]") { + AdcFunctionalConfig cfg; + REQUIRE_FALSE(set_combine_avg_values(cfg, 9, ServerState::HwUnconfigured, WriterCtx{})); + REQUIRE(cfg.adc_combine_avg_values == 0); + + WriterCtx writer; + writer.via_root_client_ep0 = true; + REQUIRE(set_combine_avg_values(cfg, 9, ServerState::RcpConfigured, writer)); + REQUIRE(cfg.adc_combine_avg_values == 9); +} + +// ── Trigger outputs (Table 53) ───────────────────────────────────────────────── + +TEST_CASE("AdcTriggerState starts with no previous value tracked", "[adc][REQ-ADC-031]") { + AdcTriggerState s; + REQUIRE_FALSE(s.has_previous); +} + +TEST_CASE("trigger_evaluate never fires an edge trigger on the first call", "[adc][REQ-ADC-031]") { + AdcTriggerState s; + auto fired = trigger_evaluate(s, 5, 10, 100, false); + REQUIRE(fired == 0); + REQUIRE(s.has_previous); +} + +TEST_CASE("trigger_evaluate BELOW_MIN fires on a downward crossing of trigger_min", + "[adc][REQ-ADC-048]") { + AdcTriggerState s; + trigger_evaluate(s, 20, 10, 100, false); // establish previous >= min + auto fired = trigger_evaluate(s, 5, 10, 100, false); // crosses below min + REQUIRE((fired & kAdcTriggerBelowMin) != 0); +} - std::vector> queue{uint16_t{10}, uint16_t{20}, uint16_t{30}, uint16_t{40}, uint16_t{999}}; // one extra entry left over +TEST_CASE("trigger_evaluate ABOVE_MIN fires on an upward crossing of trigger_min", + "[adc][REQ-ADC-049]") { + AdcTriggerState s; + trigger_evaluate(s, 5, 10, 100, false); + auto fired = trigger_evaluate(s, 20, 10, 100, false); + REQUIRE((fired & kAdcTriggerAboveMin) != 0); +} + +TEST_CASE("trigger_evaluate BELOW_MAX/ABOVE_MAX cross trigger_max", "[adc][REQ-ADC-050][REQ-ADC-051]") { + AdcTriggerState s; + trigger_evaluate(s, 200, 10, 100, false); // above max + auto down = trigger_evaluate(s, 50, 10, 100, false); // crosses below max + REQUIRE((down & kAdcTriggerBelowMax) != 0); + + AdcTriggerState s2; + trigger_evaluate(s2, 50, 10, 100, false); + auto up = trigger_evaluate(s2, 200, 10, 100, false); + REQUIRE((up & kAdcTriggerAboveMax) != 0); +} + +TEST_CASE("trigger_evaluate's MEASUREMENT_FINISHED composes independently with the other four", + "[adc][REQ-ADC-052]") { + AdcTriggerState s; + trigger_evaluate(s, 20, 10, 100, false); + auto fired = trigger_evaluate(s, 5, 10, 100, /*measurement_finished=*/true); + REQUIRE((fired & kAdcTriggerBelowMin) != 0); + REQUIRE((fired & kAdcTriggerMeasurementFinished) != 0); + + AdcTriggerState s2; + auto first_call_finished_only = trigger_evaluate(s2, 5, 10, 100, true); + REQUIRE(first_call_finished_only == kAdcTriggerMeasurementFinished); // no previous -> no edge triggers +} + +// ── The EP_func register block ──────────────────────────────────────────────── + +TEST_CASE("render_registers matches Table 54's own offsets", "[adc][REQ-ADC-035][REQ-ADC-036][REQ-ADC-038][REQ-ADC-040]") { + AdcFunctionalConfig cfg; + cfg.ep_enable = true; + cfg.ep_response_ts_enable = true; + cfg.ep_status = 0xBEEF; + cfg.base_clk_divider = 5; + cfg.sample_interval = 200; + cfg.adc_avg_intervals_per_request = 4; + cfg.adc_samples_per_avg_interval = 8; + cfg.adc_combine_avg_values = 2; + cfg.resolution = 12; + cfg.trigger_min = 100; + cfg.trigger_max = 4000; + + AdcRegisterBlock block{}; + render_registers(cfg, block); + + REQUIRE(block[kAdcRegEpLen] == kAdcEpFuncLen); + REQUIRE((block[kAdcRegEpEnableClr] & 0x01) != 0); // ep_enable + REQUIRE((block[kAdcRegEpOptions] & 0x08) != 0); // ep_response_ts_enable + REQUIRE(rcp::avtp::detail::get_u16(&block[kAdcRegBaseClk]) == 0); // no real clock modelled + REQUIRE(rcp::avtp::detail::get_u16(&block[kAdcRegEpStatus]) == 0xBEEF); + REQUIRE(block[kAdcRegBaseClkDivider] == 5); + REQUIRE(block[kAdcRegSampleInterval] == 200); + REQUIRE(block[kAdcRegAvgIntervals] == 4); + REQUIRE(block[kAdcRegSamplesPerAvg] == 8); + REQUIRE(block[kAdcRegCombineAvg] == 2); + REQUIRE(block[kAdcRegResolution] == 12); + REQUIRE(rcp::avtp::detail::get_u16(&block[kAdcRegTriggerMin]) == 100); + REQUIRE(rcp::avtp::detail::get_u16(&block[kAdcRegTriggerMax]) == 4000); +} - uint16_t out_value = 0; - auto ec = ep.request_reading_from_trigger_queue(cfg, queue, out_value); +TEST_CASE("render_registers truncates the wide avg_intervals/samples_per_avg fields to their low octet", + "[adc][REQ-ADC-035]") { + AdcFunctionalConfig cfg; + cfg.adc_avg_intervals_per_request = 0x0142; // > 255 + cfg.adc_samples_per_avg_interval = 0x0201; + + AdcRegisterBlock block{}; + render_registers(cfg, block); + REQUIRE(block[kAdcRegAvgIntervals] == 0x42); + REQUIRE(block[kAdcRegSamplesPerAvg] == 0x01); +} + +TEST_CASE("apply_reconfig writes a multi-register span", "[adc][REQ-ADC-039]") { + AdcFunctionalConfig cfg; + // Patch from kAdcRegBaseClkDivider through kAdcRegAvgIntervals inclusive. + std::vector payload; + payload.push_back(static_cast(kAdcRegBaseClkDivider >> 8)); + payload.push_back(static_cast(kAdcRegBaseClkDivider & 0xFF)); + payload.push_back(9); // base_clk_divider + payload.push_back(77); // sample_interval + payload.push_back(3); // avg_intervals (low octet) + + auto ec = apply_reconfig(cfg, payload.data(), payload.size()); REQUIRE_FALSE(ec); - REQUIRE(out_value == 25); // same combination as the SelfTimed test above - REQUIRE(queue.size() == 1); // exactly the consumed entries were removed - REQUIRE(*queue[0] == 999u); + REQUIRE(cfg.base_clk_divider == 9); + REQUIRE(cfg.sample_interval == 77); + REQUIRE(cfg.adc_avg_intervals_per_request == 3); } -TEST_CASE("request_reading_from_trigger_queue reports no_signal on queue underrun", - "[adc][REQ-ADC-004]") { - AdcEndpoint ep; - AdcAveragingConfig cfg; - cfg.adc_avg_intervals_per_request = 2; - cfg.adc_combine_avg_values = 2; +TEST_CASE("apply_reconfig writes resolution and trigger thresholds", "[adc][REQ-ADC-040]") { + AdcFunctionalConfig cfg; + std::vector payload; + payload.push_back(static_cast(kAdcRegResolution >> 8)); + payload.push_back(static_cast(kAdcRegResolution & 0xFF)); + payload.push_back(16); // resolution + payload.push_back(0x01); // trigger_min hi + payload.push_back(0x00); // trigger_min lo + payload.push_back(0x02); // trigger_max hi + payload.push_back(0x00); // trigger_max lo + + REQUIRE_FALSE(apply_reconfig(cfg, payload.data(), payload.size())); + REQUIRE(cfg.resolution == 16); + REQUIRE(cfg.trigger_min == 0x0100); + REQUIRE(cfg.trigger_max == 0x0200); +} - std::vector> queue{uint16_t{1}, uint16_t{2}}; // fewer than the 4 needed - uint16_t out_value = 0; - auto ec = ep.request_reading_from_trigger_queue(cfg, queue, out_value); - REQUIRE(ec == make_error_code(AdcErrc::no_signal)); +TEST_CASE("apply_reconfig ignores read-only registers within an otherwise-applied span", + "[adc][REQ-ADC-039]") { + AdcFunctionalConfig cfg; + cfg.ep_status = 0x1234; + + // Span from EP_LEN (0x0000, read-only) through EP_OPTIONS (0x0003). + std::vector payload{0x00, 0x00, /*EP_LEN write*/ 0xAA, /*reserved write*/ 0xBB, + /*EP_ENABLE_CLR*/ 0x01, /*EP_OPTIONS*/ 0x08}; + REQUIRE_FALSE(apply_reconfig(cfg, payload.data(), payload.size())); + + AdcRegisterBlock block{}; + render_registers(cfg, block); + REQUIRE(block[kAdcRegEpLen] == kAdcEpFuncLen); // untouched by the write + REQUIRE(block[kAdcRegReserved01] == 0); // untouched + REQUIRE(cfg.ep_enable); // R/W octet DID apply + REQUIRE(cfg.ep_response_ts_enable); // options bit 3 (0x08) DID apply } -TEST_CASE("request_reading_from_trigger_queue reports no_signal on a missing-capture entry", - "[adc][REQ-ADC-005]") { - AdcEndpoint ep; - AdcAveragingConfig cfg; - cfg.adc_avg_intervals_per_request = 2; - cfg.adc_combine_avg_values = 1; +TEST_CASE("apply_reconfig rejects a write extending past EP_LEN", "[adc][REQ-ADC-039]") { + AdcFunctionalConfig cfg; + std::vector payload{0x00, static_cast(kAdcEpFuncLen - 1), 0xAA, 0xBB}; + auto ec = apply_reconfig(cfg, payload.data(), payload.size()); + REQUIRE(ec == make_error_code(AdcErrc::reconfig_out_of_range)); +} + +TEST_CASE("apply_reconfig rejects a payload with no data octet", "[adc][REQ-ADC-039]") { + AdcFunctionalConfig cfg; + std::vector payload{0x00, 0x00}; // address only, no data + auto ec = apply_reconfig(cfg, payload.data(), payload.size()); + REQUIRE(ec == make_error_code(AdcErrc::reconfig_short)); +} + +TEST_CASE("encode_reconfig_request round-trips through apply_reconfig", "[adc][REQ-ADC-054]") { + std::vector data{9, 77}; + auto frame = encode_reconfig_request(0x10, kAdcRegBaseClkDivider, data, 5); + REQUIRE_FALSE(frame.empty()); + + // Manually decode the frame's payload (the reconfig-request encoder + // builds an ordinary ACF_ABB write with evt[2:0]=111b). + rcp::acf::AcfMessageInfo info; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), info, payload)); + REQUIRE(info.op); + REQUIRE(info.evt_op == 0x7); + + AdcFunctionalConfig cfg; + REQUIRE_FALSE(apply_reconfig(cfg, payload.data(), payload.size())); + REQUIRE(cfg.base_clk_divider == 9); + REQUIRE(cfg.sample_interval == 77); +} - std::vector> queue{uint16_t{10}, std::nullopt}; // trigger occurred, no valid capture - uint16_t out_value = 0; - auto ec = ep.request_reading_from_trigger_queue(cfg, queue, out_value); - REQUIRE(ec == make_error_code(AdcErrc::no_signal)); +TEST_CASE("encode_reconfig_request rejects empty data", "[adc][REQ-ADC-054]") { + REQUIRE(encode_reconfig_request(0x10, 0, {}, 1).empty()); } // ── AdcErrc category sanity ──────────────────────────────────────────────────── -TEST_CASE("AdcErrc reports a non-empty message in its own category", "[adc][REQ-ADC-006]") { - auto ec = make_error_code(AdcErrc::no_signal); - REQUIRE(ec.category() == adc_category()); - REQUIRE_FALSE(ec.message().empty()); +TEST_CASE("AdcErrc reports a non-empty, category-correct message for every value", + "[adc][REQ-ADC-024][REQ-ADC-055]") { + for (int v = 1; v <= 12; ++v) { + auto ec = make_error_code(static_cast(v)); + REQUIRE(ec.category() == adc_category()); + REQUIRE_FALSE(ec.message().empty()); + } } -// AdcErrc::no_signal must not claim a TC18-defined error-code identity it -// does not have (issue #77): Table 27 defines no ADC-specific no-signal -// code, so the message must not contain an invented "ADC_NO_SIGNAL" -// identifier. TEST_CASE("AdcErrc::no_signal's message does not claim an invented ADC_NO_SIGNAL spec identifier", "[adc][REQ-ADC-006]") { auto ec = make_error_code(AdcErrc::no_signal); REQUIRE(ec.message().find("ADC_NO_SIGNAL") == std::string::npos); } -// ── Wire codec ──────────────────────────────────────────────────────────────── +TEST_CASE("wire_error maps bad_payload_len/bad_evt to their numbered wire codes", "[adc][REQ-ADC-024]") { + REQUIRE(wire_error(AdcErrc::bad_payload_len) == rcp::acf::WireErrorCode::InvalidParameter); + REQUIRE(wire_error(AdcErrc::bad_evt) == rcp::acf::WireErrorCode::UnsupportedCmd); + REQUIRE_FALSE(wire_error(AdcErrc::short_frame).has_value()); +} + +// ── Wire codec: read request ────────────────────────────────────────────────── -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}); +TEST_CASE("ADC read request encode/decode round-trips, carrying read_size", "[adc][REQ-ADC-025]") { + auto frame = encode_read_request(0x20, 16, 7); + uint16_t read_size = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_read_request(frame.data(), frame.size(), 0x20, read_size, txn)); + REQUIRE(read_size == 16); + REQUIRE(txn == 7); } -// ── 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("encode_read_request's frame carries no payload", "[adc][REQ-ADC-043]") { + auto frame = encode_read_request(0x20, 16, 7); + REQUIRE(frame.size() == rcp::acf::kAcfCommonHeaderLen); +} -TEST_CASE("AdcEndpoint::handle_request delegates a Plain (evt[2:0]==000b) request to " - "request_reading()", - "[adc][REQ-ADC-008]") { +TEST_CASE("decode_read_request rejects a misaddressed byte_bus_id", "[adc][REQ-ADC-044]") { + auto frame = encode_read_request(0x20, 2, 1); + uint16_t read_size = 0; + uint8_t txn = 0; + REQUIRE(decode_read_request(frame.data(), frame.size(), 0x21, read_size, txn) == + make_error_code(AdcErrc::wrong_bus)); +} + +// A dedicated write request encoder doesn't exist for ADC (it has no write +// request shape besides the reconfiguration escape hatch) -- a read request +// with op forced to write is built directly to exercise this rejection. +TEST_CASE("decode_read_request rejects a write-op frame", "[adc][REQ-ADC-045]") { + rcp::acf::AcfMessageInfo info; + info.byte_bus_id = 0x20; + info.op = true; // write + auto frame = rcp::acf::encode_acf_abb(info, {}); + uint16_t read_size = 0; + uint8_t txn = 0; + REQUIRE(decode_read_request(frame.data(), frame.size(), 0x20, read_size, txn) == + make_error_code(AdcErrc::wrong_op)); +} + +TEST_CASE("decode_read_request rejects a nonzero evt[2:0]", "[adc][REQ-ADC-046]") { + rcp::acf::AcfMessageInfo info; + info.byte_bus_id = 0x20; + info.op = false; + info.evt_op = 0x3; // reserved + auto frame = rcp::acf::encode_acf_abb(info, {}); + uint16_t read_size = 0; + uint8_t txn = 0; + REQUIRE(decode_read_request(frame.data(), frame.size(), 0x20, read_size, txn) == + make_error_code(AdcErrc::bad_evt)); +} + +TEST_CASE("decode_read_request rejects a frame shorter than the ACF_ABB fixed header", + "[adc][REQ-ADC-026]") { + // byte0's top 7 bits must carry acf_msg_type == kAcfMsgTypeAbb (0x0E) -- + // otherwise decode_acf_abb reports bad_acf_msg_type before it even gets + // to check the buffer's length against the fixed header size. + std::vector short_frame{0x1C, 0x00, 0x00}; + uint16_t read_size = 0; + uint8_t txn = 0; + REQUIRE(decode_read_request(short_frame.data(), short_frame.size(), 0x20, read_size, txn) == + make_error_code(AdcErrc::short_frame)); +} + +// ── Wire codec: response ────────────────────────────────────────────────────── + +TEST_CASE("ADC response encode/decode round-trips every measurement value when untimed", + "[adc][REQ-ADC-027]") { + std::vector values{1, 2, 3, 4, 5, 6, 7, 8}; + auto frame = encode_response(0x20, values, 9, /*timed=*/false, 0); + + std::vector out; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_response(frame.data(), frame.size(), 0x20, 16, out, timed, ts, txn)); + REQUIRE(out == values); + REQUIRE_FALSE(timed); + REQUIRE(ts == 0); + REQUIRE(txn == 9); +} + +TEST_CASE("ADC response encode/decode round-trips every measurement value when timed", + "[adc][REQ-ADC-028]") { + std::vector values{100, 200}; + auto frame = encode_response(0x20, values, 3, /*timed=*/true, 999999); + + std::vector out; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_response(frame.data(), frame.size(), 0x20, 16, out, timed, ts, txn)); + REQUIRE(out == values); + REQUIRE(timed); + REQUIRE(ts == 999999); +} + +TEST_CASE("ADC response encode/decode round-trips kAdcNoSignal verbatim in its own value slot", + "[adc][REQ-ADC-030]") { + std::vector values{10, kAdcNoSignal, 30}; + auto frame = encode_response(0x20, values, 1, false, 0); + + std::vector out; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_response(frame.data(), frame.size(), 0x20, 16, out, timed, ts, txn)); + REQUIRE(out == values); +} + +TEST_CASE("decode_response rejects a bad-payload-length frame", "[adc][REQ-ADC-029]") { + rcp::acf::AcfMessageInfo info; + info.byte_bus_id = 0x20; + info.op = false; + info.rsp = true; + auto frame = rcp::acf::encode_acf_abb(info, std::vector{0x01}); // odd length + std::vector out; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE(decode_response(frame.data(), frame.size(), 0x20, 16, out, timed, ts, txn) == + make_error_code(AdcErrc::bad_payload_len)); +} + +TEST_CASE("decode_response rejects a frame holding more values than the caller can hold", + "[adc][REQ-ADC-047]") { + std::vector values{1, 2, 3, 4}; + auto frame = encode_response(0x20, values, 1, false, 0); + std::vector out; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE(decode_response(frame.data(), frame.size(), 0x20, /*max_values=*/2, out, timed, ts, txn) == + make_error_code(AdcErrc::too_many_values)); +} + +TEST_CASE("encode_response returns an empty vector for an invalid value_count", "[adc][REQ-ADC-042]") { + REQUIRE(encode_response(0x20, {}, 1, false, 0).empty()); + std::vector too_many(kAdcMaxResponseValues + 1, 1); + REQUIRE(encode_response(0x20, too_many, 1, false, 0).empty()); +} + +TEST_CASE("encode_response carries value_count measurement values and reports 2*value_count as read_size", + "[adc][REQ-ADC-011]") { + std::vector values{1, 2, 3}; + auto frame = encode_response(0x20, values, 1, false, 0); + rcp::acf::AcfMessageInfo info; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), info, payload)); + REQUIRE(info.read_size_or_segment_num == values.size() * kAdcValueLen); + REQUIRE(payload.size() == values.size() * kAdcValueLen); +} + +// ── AdcEndpoint: corrected three-layer pipeline end to end ──────────────────── + +TEST_CASE("AdcEndpoint accumulates averaged interval values and reports response readiness", + "[adc][REQ-ADC-053]") { AdcEndpoint ep; - AdcAveragingConfig cfg; - cfg.adc_avg_intervals_per_request = 2; + AdcFunctionalConfig cfg; + cfg.adc_samples_per_avg_interval = 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; + std::vector raw{10, 20, 30, 40}; + size_t cursor = 0; + auto take_sample = [&]() -> AdcSample { + AdcSample s{raw[cursor], cursor}; + ++cursor; + return s; }; - 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 + REQUIRE_FALSE(ep.execute_measurement_cycle(cfg, take_sample)); // interval 1: avg(10,20)=15 + REQUIRE_FALSE(ep.response_ready(cfg)); + REQUIRE_FALSE(ep.execute_measurement_cycle(cfg, take_sample)); // interval 2: avg(30,40)=35 + REQUIRE(ep.response_ready(cfg)); + + std::vector out_values; + uint64_t out_ts = 0; + REQUIRE_FALSE(ep.collect_response(cfg, out_values, out_ts)); + REQUIRE(out_values == std::vector{15, 35}); // packed verbatim, NOT re-averaged + REQUIRE(ep.pending_count() == 0); } -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::collect_response fails when not enough values are pending", + "[adc][REQ-ADC-053]") { + AdcEndpoint ep; + AdcFunctionalConfig cfg; + cfg.adc_combine_avg_values = 3; + std::vector out_values; + uint64_t out_ts = 0; + REQUIRE(ep.collect_response(cfg, out_values, out_ts) == make_error_code(AdcErrc::response_not_ready)); } -TEST_CASE("AdcEndpoint::handle_request reports config_write_not_supported for evt[2:0]==111b " - "without invoking take_sample", - "[adc][REQ-ADC-009]") { +TEST_CASE("AdcEndpoint::execute_measurement_cycle only invokes take_sample exactly " + "samples_per_avg_interval times", + "[adc][REQ-ADC-002]") { 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}; + AdcFunctionalConfig cfg; + cfg.adc_samples_per_avg_interval = 5; + int calls = 0; + auto take_sample = [&calls]() -> AdcSample { + ++calls; + return AdcSample{42, static_cast(calls)}; }; - 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); + REQUIRE_FALSE(ep.execute_measurement_cycle(cfg, take_sample)); + REQUIRE(calls == 5); } -TEST_CASE("AdcEndpoint::handle_request masks evt_op down to 3 bits before classifying", - "[adc][REQ-ADC-008]") { +TEST_CASE("AdcEndpoint::execute_measurement_cycle reports pending_values_full at capacity", + "[adc][REQ-ADC-053]") { 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)); + AdcFunctionalConfig cfg; // samples_per_avg_interval == 0 -> every cycle is instantly kAdcNoSignal + auto take_sample = []() -> AdcSample { return AdcSample{1, 1}; }; + for (size_t i = 0; i < kAdcMaxCombineValues; ++i) { + REQUIRE_FALSE(ep.execute_measurement_cycle(cfg, take_sample)); + } + REQUIRE(ep.execute_measurement_cycle(cfg, take_sample) == make_error_code(AdcErrc::pending_values_full)); } -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()); +TEST_CASE("AdcEndpoint's FAN_OUT case: one execution's worth of values serve multiple responses", + "[adc][REQ-ADC-037][REQ-ADC-053]") { + AdcEndpoint ep; + AdcFunctionalConfig cfg; + cfg.adc_samples_per_avg_interval = 1; + cfg.adc_combine_avg_values = 1; // combine < intervals executed below -> FAN_OUT relationship + REQUIRE(cadence_case(4, 1) == AdcCadenceCase::FanOut); + + int v = 0; + auto take_sample = [&v]() -> AdcSample { ++v; return AdcSample{static_cast(v), static_cast(v)}; }; + + for (int i = 0; i < 4; ++i) REQUIRE_FALSE(ep.execute_measurement_cycle(cfg, take_sample)); + REQUIRE(ep.pending_count() == 4); + + for (int i = 0; i < 4; ++i) { + REQUIRE(ep.response_ready(cfg)); + std::vector out_values; + uint64_t out_ts = 0; + REQUIRE_FALSE(ep.collect_response(cfg, out_values, out_ts)); + REQUIRE(out_values.size() == 1); + } + REQUIRE(ep.pending_count() == 0); } diff --git a/tests/test_gpio.cpp b/tests/test_gpio.cpp index 30990a9..344be2b 100644 --- a/tests/test_gpio.cpp +++ b/tests/test_gpio.cpp @@ -7,15 +7,54 @@ // fusa:test REQ-GPIO-007 // fusa:test REQ-GPIO-008 // fusa:test REQ-GPIO-009 - -// Tests for rcp/gpio.hpp — the GPIO endpoint type (ROADMAP.md milestone 47, -// "Basic Endpoint Types I — GPIO & SPI", v2.3.0). +// fusa:test REQ-GPIO-010 +// fusa:test REQ-GPIO-011 +// fusa:test REQ-GPIO-012 +// fusa:test REQ-GPIO-013 +// fusa:test REQ-GPIO-014 +// fusa:test REQ-GPIO-015 +// fusa:test REQ-GPIO-016 +// fusa:test REQ-GPIO-017 +// fusa:test REQ-GPIO-018 +// fusa:test REQ-GPIO-019 +// fusa:test REQ-GPIO-020 +// fusa:test REQ-GPIO-021 +// fusa:test REQ-GPIO-022 +// fusa:test REQ-GPIO-023 +// fusa:test REQ-GPIO-024 +// fusa:test REQ-GPIO-025 +// fusa:test REQ-GPIO-026 +// fusa:test REQ-GPIO-027 +// fusa:test REQ-GPIO-028 +// fusa:test REQ-GPIO-029 +// fusa:test REQ-GPIO-030 +// fusa:test REQ-GPIO-031 +// fusa:test REQ-GPIO-032 +// fusa:test REQ-GPIO-033 +// fusa:test REQ-GPIO-034 +// fusa:test REQ-GPIO-035 +// fusa:test REQ-GPIO-036 +// fusa:test REQ-GPIO-037 +// fusa:test REQ-GPIO-038 +// fusa:test REQ-GPIO-039 +// fusa:test REQ-GPIO-040 +// fusa:test REQ-GPIO-041 +// fusa:test REQ-GPIO-042 +// fusa:test REQ-GPIO-043 +// fusa:test REQ-GPIO-044 +// fusa:test REQ-GPIO-045 +// fusa:test REQ-GPIO-046 + +// Tests for rcp/gpio.hpp — the GPIO endpoint type, re-derived from c-RCP's +// test_ep_gpio.c (Phase 3, cpp-RCP issue #129). #include #include using namespace rcp::gpio; using rcp::endpoint::WriteSemantics; +using rcp::lifecycle::ServerState; +using rcp::lifecycle::WriterCtx; // ── Payload shape ───────────────────────────────────────────────────────────── @@ -43,8 +82,6 @@ TEST_CASE("decode_gpio_payload rejects a short buffer", "[gpio][REQ-GPIO-001]") TEST_CASE("decode_gpio_payload rejects an over-long buffer (spec requires exactly 4 bytes)", "[gpio][REQ-GPIO-001]") { - // §13.7.4: "A request not having exactly four bytes is rejected" — - // cpp-RCP-05-fresh. Trailing bytes must not be silently ignored. std::vector long_buf{0x01, 0x02, 0x03, 0x04, 0x05}; PinMask out = 0; auto ec = decode_gpio_payload(long_buf.data(), long_buf.size(), out); @@ -53,7 +90,7 @@ TEST_CASE("decode_gpio_payload rejects an over-long buffer (spec requires exactl // ── Write semantics: the 6 generic combinators ─────────────────────────────── -TEST_CASE("apply_gpio_write applies Replace/Or/And/Xor to state.values", "[gpio][REQ-GPIO-002]") { +TEST_CASE("apply_gpio_write applies Replace/Or/And/Xor to state.values", "[gpio][REQ-GPIO-006][REQ-GPIO-007][REQ-GPIO-008][REQ-GPIO-009]") { GpioState state; state.directions = 0xFFFFFFFF; // every pin output, so masking is a no-op here state.values = 0x0000FFFF; @@ -73,9 +110,9 @@ TEST_CASE("apply_gpio_write applies Replace/Or/And/Xor to state.values", "[gpio] // ── Write semantics: saturating Add/Subtract shared with PWM_OUT ──────────── -TEST_CASE("apply_gpio_write applies saturating Add/Subtract to state.values", "[gpio][REQ-GPIO-003]") { +TEST_CASE("apply_gpio_write applies saturating Add/Subtract to state.values", "[gpio][REQ-GPIO-010][REQ-GPIO-011]") { GpioState state; - state.directions = 0xFFFFFFFF; // every pin output, so masking is a no-op here + state.directions = 0xFFFFFFFF; state.values = 0xFFFFFFF0; REQUIRE_FALSE(apply_gpio_write(WriteSemantics::Add, state, 0x20)); @@ -83,12 +120,16 @@ TEST_CASE("apply_gpio_write applies saturating Add/Subtract to state.values", "[ state.values = 5; REQUIRE_FALSE(apply_gpio_write(WriteSemantics::Subtract, state, 10)); - REQUIRE(state.values == 0); // saturates at 0, does not wrap + REQUIRE(state.values == 5); // request(10) - current(5) = 5, per the "request minus current" rule + + state.values = 20; + REQUIRE_FALSE(apply_gpio_write(WriteSemantics::Subtract, state, 5)); + REQUIRE(state.values == 0); // request(5) < current(20) -> saturates at 0 } // ── Write semantics: Reserved rejected, Reconfigure targets directions ────── -TEST_CASE("apply_gpio_write rejects Reserved and leaves state untouched", "[gpio][REQ-GPIO-004]") { +TEST_CASE("apply_gpio_write rejects Reserved and leaves state untouched", "[gpio][REQ-GPIO-012]") { GpioState state; state.values = 0x12345678; auto ec = apply_gpio_write(WriteSemantics::Reserved, state, 0xFFFFFFFF); @@ -97,62 +138,55 @@ TEST_CASE("apply_gpio_write rejects Reserved and leaves state untouched", "[gpio } TEST_CASE("apply_gpio_write's Reconfigure replaces state.directions, not state.values", - "[gpio][REQ-GPIO-004]") { + "[gpio][REQ-GPIO-013]") { GpioState state; state.values = 0xAAAAAAAA; state.directions = 0; REQUIRE_FALSE(apply_gpio_write(WriteSemantics::Reconfigure, state, 0x0000FFFF)); REQUIRE(state.directions == 0x0000FFFF); - REQUIRE(state.values == 0xAAAAAAAA); // unaffected by a Reconfigure write + REQUIRE(state.values == 0xAAAAAAAA); } // ── Write masking against input-configured pins (TC18 §13.7.4.3) ──────────── TEST_CASE("apply_gpio_write's Replace does not modify input-configured pins", - "[gpio][REQ-GPIO-009]") { + "[gpio][REQ-GPIO-037]") { GpioState state; state.directions = 0x0000000F; // pins 0-3 output, pins 4-31 input state.values = 0xABCD0005; // pin 4 (input) currently reads 1 REQUIRE_FALSE(apply_gpio_write(WriteSemantics::Replace, state, 0xFFFFFFF0)); - // Output pins 0-3 take the request's bits (0x0); every input pin (4-31) - // keeps its prior value (0xABCD0000) untouched, including bit 4. REQUIRE(state.values == 0xABCD0000); } -TEST_CASE("apply_gpio_write's Or/And/Xor only affect output-configured pins", - "[gpio][REQ-GPIO-009]") { +TEST_CASE("apply_gpio_write's Or/And only affect output-configured pins", "[gpio][REQ-GPIO-037]") { GpioState state; state.directions = 0x000000FF; // pins 0-7 output, rest input - state.values = 0x00000F0F; // output byte = 0x0F, input bits = 0x000000_0 + state.values = 0x00000F0F; REQUIRE_FALSE(apply_gpio_write(WriteSemantics::Or, state, 0xFFFFFFF0)); - // Combinator computes 0x0FFF (0x0F0F | 0xFFF0) but only the low 8 bits - // (output) may actually change; bits 8-31 (input) stay exactly as before. REQUIRE(state.values == 0x00000FFF); - state.values = 0x0000FFFF; // output byte = 0xFF, input bits = 0x0000FF00 + state.values = 0x0000FFFF; REQUIRE_FALSE(apply_gpio_write(WriteSemantics::And, state, 0x00000000)); - REQUIRE(state.values == 0x0000FF00); // output byte actually cleared, input untouched + REQUIRE(state.values == 0x0000FF00); } TEST_CASE("apply_gpio_write's Add/Subtract saturation still respects input masking", - "[gpio][REQ-GPIO-009]") { + "[gpio][REQ-GPIO-037]") { GpioState state; state.directions = 0x0000FFFF; // low 16 bits output, high 16 bits input state.values = 0xBEEF0000; REQUIRE_FALSE(apply_gpio_write(WriteSemantics::Add, state, 0xFFFFFFFF)); - // The 32-bit saturating add itself saturates to 0xFFFFFFFF, but masking - // still confines the committed change to the output half. REQUIRE(state.values == 0xBEEFFFFF); } -// ── Per-pin change/rising/falling trigger signals ──────────────────────────── +// ── Per-pin change/rising/falling trigger signals (internal bookkeeping) ──── TEST_CASE("evaluate_gpio_triggers fires Change+Rising for a 0->1 transition when both are armed", - "[gpio][REQ-GPIO-005]") { + "[gpio][REQ-GPIO-014][REQ-GPIO-015]") { rcp::endpoint::TriggerRegistry triggers; triggers.enable(gpio_signal_id(3, GpioEdge::Change)); triggers.enable(gpio_signal_id(3, GpioEdge::Rising)); @@ -166,7 +200,7 @@ TEST_CASE("evaluate_gpio_triggers fires Change+Rising for a 0->1 transition when REQUIRE(drained[1] == gpio_signal_id(3, GpioEdge::Rising)); } -TEST_CASE("evaluate_gpio_triggers fires Change+Falling for a 1->0 transition", "[gpio][REQ-GPIO-005]") { +TEST_CASE("evaluate_gpio_triggers fires Change+Falling for a 1->0 transition", "[gpio][REQ-GPIO-017]") { rcp::endpoint::TriggerRegistry triggers; triggers.enable(gpio_signal_id(7, GpioEdge::Change)); triggers.enable(gpio_signal_id(7, GpioEdge::Falling)); @@ -179,7 +213,7 @@ TEST_CASE("evaluate_gpio_triggers fires Change+Falling for a 1->0 transition", " REQUIRE(drained[1] == gpio_signal_id(7, GpioEdge::Falling)); } -TEST_CASE("evaluate_gpio_triggers fires nothing for a pin whose bit is unchanged", "[gpio][REQ-GPIO-005]") { +TEST_CASE("evaluate_gpio_triggers fires nothing for a pin whose bit is unchanged", "[gpio][REQ-GPIO-014]") { rcp::endpoint::TriggerRegistry triggers; triggers.enable(gpio_signal_id(0, GpioEdge::Change)); @@ -187,19 +221,452 @@ TEST_CASE("evaluate_gpio_triggers fires nothing for a pin whose bit is unchanged REQUIRE_FALSE(triggers.has_pending()); } -TEST_CASE("evaluate_gpio_triggers only reports signals that were actually armed", "[gpio][REQ-GPIO-005]") { - rcp::endpoint::TriggerRegistry triggers; // nothing enabled +TEST_CASE("evaluate_gpio_triggers only reports signals that were actually armed", "[gpio][REQ-GPIO-014]") { + rcp::endpoint::TriggerRegistry triggers; auto fired = evaluate_gpio_triggers(triggers, /*old=*/0, /*new=*/1); REQUIRE(fired.empty()); } -// ── Functional config block wiring ──────────────────────────────────────────── +// ── GpioTrigger / trigger_fires / Table 43 wire signal numbering ──────────── + +TEST_CASE("trigger_fires never fires for None", "[gpio][REQ-GPIO-014]") { + REQUIRE_FALSE(trigger_fires(GpioTrigger::None, false, true)); + REQUIRE_FALSE(trigger_fires(GpioTrigger::None, true, false)); +} + +TEST_CASE("trigger_fires implements AnyChange/Rising/Falling", "[gpio][REQ-GPIO-015][REQ-GPIO-016][REQ-GPIO-017]") { + REQUIRE(trigger_fires(GpioTrigger::AnyChange, false, true)); + REQUIRE(trigger_fires(GpioTrigger::AnyChange, true, false)); + REQUIRE_FALSE(trigger_fires(GpioTrigger::AnyChange, true, true)); + + REQUIRE(trigger_fires(GpioTrigger::Rising, false, true)); + REQUIRE_FALSE(trigger_fires(GpioTrigger::Rising, true, false)); + + REQUIRE(trigger_fires(GpioTrigger::Falling, true, false)); + REQUIRE_FALSE(trigger_fires(GpioTrigger::Falling, false, true)); +} + +TEST_CASE("trigger_signal_number implements Table 43's 3n+{1,2,3} pattern", "[gpio][REQ-GPIO-034]") { + REQUIRE(trigger_signal_number(0, GpioTrigger::AnyChange) == 1); + REQUIRE(trigger_signal_number(0, GpioTrigger::Rising) == 2); + REQUIRE(trigger_signal_number(0, GpioTrigger::Falling) == 3); + REQUIRE(trigger_signal_number(31, GpioTrigger::Falling) == 96); // 3*31+3 +} + +TEST_CASE("trigger_signal_number returns nullopt for None or an out-of-range pin", "[gpio][REQ-GPIO-034]") { + REQUIRE_FALSE(trigger_signal_number(0, GpioTrigger::None).has_value()); + REQUIRE_FALSE(trigger_signal_number(32, GpioTrigger::AnyChange).has_value()); +} + +// ── Debounce filtering (REQ-GPIO-035/044) ───────────────────────────────────── + +TEST_CASE("GpioDebounceState default-constructs / debounce_state_init zeroes", "[gpio][REQ-GPIO-044]") { + GpioDebounceState s; + s.consecutive_count = 5; + debounce_state_init(s); + REQUIRE_FALSE(s.has_settled); + REQUIRE_FALSE(s.has_candidate); + REQUIRE(s.consecutive_count == 0); +} + +TEST_CASE("debounce_sample with n=0 settles every sample immediately", "[gpio][REQ-GPIO-035]") { + GpioDebounceState s; + bool changed = false; + REQUIRE(debounce_sample(s, true, 0, &changed) == true); + REQUIRE_FALSE(changed); // first-ever settle isn't reported as a change + REQUIRE(debounce_sample(s, false, 0, &changed) == false); + REQUIRE(changed); +} + +TEST_CASE("debounce_sample returns false before the first settle completes", "[gpio][REQ-GPIO-035]") { + GpioDebounceState s; + bool changed = true; + REQUIRE(debounce_sample(s, true, 3, &changed) == false); // only 1 of 3 samples seen + REQUIRE_FALSE(changed); + REQUIRE(debounce_sample(s, true, 3, &changed) == false); // 2 of 3 +} + +TEST_CASE("debounce_sample settles after n consecutive identical samples", "[gpio][REQ-GPIO-035]") { + GpioDebounceState s; + debounce_sample(s, true, 3, nullptr); + debounce_sample(s, true, 3, nullptr); + bool changed = true; + REQUIRE(debounce_sample(s, true, 3, &changed) == true); // 3rd consecutive sample settles + REQUIRE_FALSE(changed); // this is the first-ever settle, not reported as a "change" +} + +TEST_CASE("debounce_sample's first-ever settle is not reported as a change", "[gpio][REQ-GPIO-035]") { + GpioDebounceState s; + debounce_sample(s, true, 2, nullptr); + bool changed = true; + REQUIRE(debounce_sample(s, true, 2, &changed) == true); + REQUIRE_FALSE(changed); +} + +TEST_CASE("debounce_sample discards a partial run on a differing sample", "[gpio][REQ-GPIO-035]") { + GpioDebounceState s; + debounce_sample(s, true, 3, nullptr); + debounce_sample(s, true, 3, nullptr); // 2 consecutive trues + debounce_sample(s, false, 3, nullptr); // differing sample resets the run + bool changed = false; + REQUIRE(debounce_sample(s, true, 3, &changed) == false); // only 1 consecutive true again + REQUIRE_FALSE(changed); +} + +TEST_CASE("debounce_sample reports no change when the settled value repeats", "[gpio][REQ-GPIO-035]") { + GpioDebounceState s; + debounce_sample(s, true, 1, nullptr); // settles true + bool changed = true; + REQUIRE(debounce_sample(s, true, 1, &changed) == true); + REQUIRE_FALSE(changed); +} + +TEST_CASE("debounce_sample reports a change when re-settling to the opposite value", + "[gpio][REQ-GPIO-035]") { + GpioDebounceState s; + debounce_sample(s, true, 1, nullptr); // settles true + bool changed = false; + REQUIRE(debounce_sample(s, false, 1, &changed) == false); + REQUIRE(changed); +} + +// ── Response timing (REQ-GPIO-036) ──────────────────────────────────────────── + +TEST_CASE("response_timing: a pure read (no payload) is immediate", "[gpio][REQ-GPIO-036]") { + REQUIRE(response_timing(/*is_write=*/false, /*payload_len=*/0) == GpioResponseTiming::Immediate); +} + +TEST_CASE("response_timing: a payload-bearing read is after debounce", "[gpio][REQ-GPIO-036]") { + REQUIRE(response_timing(false, 4) == GpioResponseTiming::AfterDebounce); +} + +TEST_CASE("response_timing: a write is always after debounce", "[gpio][REQ-GPIO-036]") { + REQUIRE(response_timing(true, 0) == GpioResponseTiming::AfterDebounce); + REQUIRE(response_timing(true, 4) == GpioResponseTiming::AfterDebounce); +} + +// ── Functional config ────────────────────────────────────────────────────────── + +TEST_CASE("GpioFunctionalConfig default-constructs zeroed", "[gpio][REQ-GPIO-018]") { + GpioFunctionalConfig cfg; + REQUIRE_FALSE(cfg.ep_enable); + REQUIRE(cfg.pins[0].pin_property == 0); + REQUIRE(cfg.pins[0].trigger == GpioTrigger::None); + REQUIRE(cfg.ep_status == 0); + REQUIRE(cfg.clk_divider == 0); + REQUIRE(cfg.debounce[0] == 0); +} + +TEST_CASE("functional_cfg_writable is unwritable while HwUnconfigured", "[gpio][REQ-GPIO-019]") { + WriterCtx writer; + writer.via_root_client_ep0 = true; + REQUIRE_FALSE(functional_cfg_writable(ServerState::HwUnconfigured, writer)); +} + +TEST_CASE("functional_cfg_writable requires an authorized writer while HwConfigured", + "[gpio][REQ-GPIO-020]") { + WriterCtx authorized; + authorized.via_root_client_ep0 = true; + REQUIRE(functional_cfg_writable(ServerState::HwConfigured, authorized)); + REQUIRE_FALSE(functional_cfg_writable(ServerState::HwConfigured, WriterCtx{})); +} + +TEST_CASE("functional_cfg_writable requires authorization once RcpConfigured", "[gpio][REQ-GPIO-021]") { + WriterCtx owning; + owning.via_owning_stream = true; + REQUIRE(functional_cfg_writable(ServerState::RcpConfigured, owning)); + + WriterCtx discovery; + discovery.via_discovery_stream = true; + REQUIRE_FALSE(functional_cfg_writable(ServerState::RcpConfigured, discovery)); +} + +TEST_CASE("set_pin_property rejects an invalid pin index or an unauthorized write without mutating cfg", + "[gpio][REQ-GPIO-022]") { + GpioFunctionalConfig cfg; + WriterCtx writer; + writer.via_root_client_ep0 = true; + REQUIRE_FALSE(set_pin_property(cfg, kMaxPins, kPinPropOutput, ServerState::HwConfigured, writer)); + REQUIRE_FALSE(set_pin_property(cfg, 0, kPinPropOutput, ServerState::HwUnconfigured, writer)); + REQUIRE(cfg.pins[0].pin_property == 0); +} + +TEST_CASE("set_pin_property applies the write when authorized", "[gpio][REQ-GPIO-023]") { + GpioFunctionalConfig cfg; + WriterCtx writer; + writer.via_root_client_ep0 = true; + REQUIRE(set_pin_property(cfg, 5, kPinPropOutput, ServerState::HwConfigured, writer)); + REQUIRE(cfg.pins[5].pin_property == kPinPropOutput); +} + +TEST_CASE("set_pin_trigger rejects an invalid pin index or an unauthorized write without mutating cfg", + "[gpio][REQ-GPIO-024]") { + GpioFunctionalConfig cfg; + WriterCtx writer; + writer.via_root_client_ep0 = true; + REQUIRE_FALSE(set_pin_trigger(cfg, kMaxPins, GpioTrigger::Rising, ServerState::HwConfigured, writer)); + REQUIRE_FALSE(set_pin_trigger(cfg, 0, GpioTrigger::Rising, ServerState::HwUnconfigured, writer)); + REQUIRE(cfg.pins[0].trigger == GpioTrigger::None); +} + +TEST_CASE("set_pin_trigger applies the write when authorized", "[gpio][REQ-GPIO-025]") { + GpioFunctionalConfig cfg; + WriterCtx writer; + writer.via_owning_stream = true; + REQUIRE(set_pin_trigger(cfg, 3, GpioTrigger::Falling, ServerState::RcpConfigured, writer)); + REQUIRE(cfg.pins[3].trigger == GpioTrigger::Falling); +} + +// ── The EP_func register block ──────────────────────────────────────────────── + +TEST_CASE("render_registers matches Table 44's own offsets", "[gpio][REQ-GPIO-038]") { + GpioFunctionalConfig cfg; + cfg.ep_enable = true; + cfg.ep_status = 0xCAFE; + cfg.clk_divider = 7; + cfg.debounce[0] = 3; + cfg.debounce[31] = 9; + + GpioRegisterBlock block{}; + render_registers(cfg, block); + + REQUIRE(block[kGpioRegEpLen] == kGpioEpFuncLen); + REQUIRE(block[kGpioRegIoMax] == kMaxPins); + REQUIRE((block[kGpioRegEpEnableClr] & 0x01) != 0); + REQUIRE(rcp::avtp::detail::get_u16(&block[kGpioRegBaseClk]) == 0); + REQUIRE(rcp::avtp::detail::get_u16(&block[kGpioRegEpStatus]) == 0xCAFE); + REQUIRE(block[kGpioRegClkDivider] == 7); + REQUIRE(block[kGpioRegDebounceIo0 + 0] == 3); + REQUIRE(block[kGpioRegDebounceIo0 + 31] == 9); + REQUIRE(kGpioEpFuncLen == 0x0029); // gpio_debounce_IO31 at the arithmetically-consistent 0x0028 +} + +TEST_CASE("apply_reconfig writes clk_divider", "[gpio][REQ-GPIO-013]") { + GpioFunctionalConfig cfg; + std::vector payload{0x00, static_cast(kGpioRegClkDivider), 42}; + REQUIRE_FALSE(apply_reconfig(cfg, payload.data(), payload.size())); + REQUIRE(cfg.clk_divider == 42); +} + +TEST_CASE("apply_reconfig writes a multi-register debounce span", "[gpio][REQ-GPIO-013]") { + GpioFunctionalConfig cfg; + std::vector payload{0x00, static_cast(kGpioRegDebounceIo0), 1, 2, 3}; + REQUIRE_FALSE(apply_reconfig(cfg, payload.data(), payload.size())); + REQUIRE(cfg.debounce[0] == 1); + REQUIRE(cfg.debounce[1] == 2); + REQUIRE(cfg.debounce[2] == 3); +} + +TEST_CASE("apply_reconfig ignores read-only registers within an otherwise-applied span", + "[gpio][REQ-GPIO-042]") { + GpioFunctionalConfig cfg; + std::vector payload{0x00, 0x00, /*EP_LEN*/ 0xAA, /*IO_MAX*/ 0xBB, + /*EP_ENABLE_CLR*/ 0x01, /*EP_OPTIONS*/ 0x08}; + REQUIRE_FALSE(apply_reconfig(cfg, payload.data(), payload.size())); + + GpioRegisterBlock block{}; + render_registers(cfg, block); + REQUIRE(block[kGpioRegEpLen] == kGpioEpFuncLen); + REQUIRE(block[kGpioRegIoMax] == kMaxPins); + REQUIRE(cfg.ep_enable); + REQUIRE(cfg.ep_response_ts_enable); // options bit 3 (0x08) DID apply +} + +TEST_CASE("apply_reconfig ignores the base_clk octets individually", "[gpio][REQ-GPIO-042]") { + GpioFunctionalConfig cfg; + std::vector payload{0x00, static_cast(kGpioRegBaseClk), 0xFF, 0xFF}; + REQUIRE_FALSE(apply_reconfig(cfg, payload.data(), payload.size())); + GpioRegisterBlock block{}; + render_registers(cfg, block); + REQUIRE(rcp::avtp::detail::get_u16(&block[kGpioRegBaseClk]) == 0); +} + +TEST_CASE("apply_reconfig rejects a write extending past EP_LEN", "[gpio][REQ-GPIO-041]") { + GpioFunctionalConfig cfg; + std::vector payload{0x00, static_cast(kGpioEpFuncLen - 1), 0xAA, 0xBB}; + REQUIRE(apply_reconfig(cfg, payload.data(), payload.size()) == + make_error_code(GpioErrc::reconfig_out_of_range)); +} + +TEST_CASE("apply_reconfig rejects a payload without address+data", "[gpio][REQ-GPIO-040]") { + GpioFunctionalConfig cfg; + std::vector payload{0x00, 0x00}; + REQUIRE(apply_reconfig(cfg, payload.data(), payload.size()) == + make_error_code(GpioErrc::reconfig_short)); +} + +TEST_CASE("encode_reconfig_request round-trips through apply_reconfig", "[gpio][REQ-GPIO-043]") { + std::vector data{42}; + auto frame = encode_reconfig_request(0x10, kGpioRegClkDivider, data, 5); + REQUIRE_FALSE(frame.empty()); + + rcp::acf::AcfMessageInfo info; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), info, payload)); + REQUIRE(info.op); + REQUIRE(info.evt_op == static_cast(WriteSemantics::Reconfigure)); + + GpioFunctionalConfig cfg; + REQUIRE_FALSE(apply_reconfig(cfg, payload.data(), payload.size())); + REQUIRE(cfg.clk_divider == 42); +} + +TEST_CASE("encode_reconfig_request rejects empty data", "[gpio][REQ-GPIO-043]") { + REQUIRE(encode_reconfig_request(0x10, 0, {}, 1).empty()); +} + +// ── GpioErrc category sanity ────────────────────────────────────────────────── + +TEST_CASE("GpioErrc reports a non-empty, category-correct message for every value", + "[gpio][REQ-GPIO-001][REQ-GPIO-039]") { + for (int v = 1; v <= 9; ++v) { + auto ec = make_error_code(static_cast(v)); + REQUIRE(ec.category() == gpio_category()); + REQUIRE_FALSE(ec.message().empty()); + } +} + +TEST_CASE("wire_error maps bad_payload_len/reserved_evt to their numbered wire codes", + "[gpio][REQ-GPIO-033][REQ-GPIO-046]") { + REQUIRE(wire_error(GpioErrc::bad_payload_len) == rcp::acf::WireErrorCode::InvalidParameter); + REQUIRE(wire_error(GpioErrc::reserved_evt) == rcp::acf::WireErrorCode::UnsupportedCmd); + REQUIRE_FALSE(wire_error(GpioErrc::short_frame).has_value()); +} + +// ── Wire codec: read request ────────────────────────────────────────────────── + +TEST_CASE("GPIO read request encode/decode round-trips", "[gpio][REQ-GPIO-026]") { + auto frame = encode_read_request(0x20, 7); + uint8_t txn = 0; + REQUIRE_FALSE(decode_read_request(frame.data(), frame.size(), 0x20, txn)); + REQUIRE(txn == 7); + REQUIRE(frame.size() == rcp::acf::kAcfCommonHeaderLen); // no payload +} + +TEST_CASE("decode_read_request rejects a malformed or misaddressed frame", "[gpio][REQ-GPIO-027]") { + auto frame = encode_read_request(0x20, 1); + uint8_t txn = 0; + REQUIRE(decode_read_request(frame.data(), frame.size(), 0x21, txn) == + make_error_code(GpioErrc::wrong_bus)); + + // byte0's top 7 bits must carry acf_msg_type == kAcfMsgTypeAbb (0x0E) -- + // otherwise decode_acf_abb reports bad_acf_msg_type before it even gets + // to check the buffer's length against the fixed header size. + std::vector short_frame{0x1C, 0x01}; + REQUIRE(decode_read_request(short_frame.data(), short_frame.size(), 0x20, txn) == + make_error_code(GpioErrc::short_frame)); + + auto write_frame = encode_write_request(0x20, 0, WriteSemantics::Replace, 1); + REQUIRE(decode_read_request(write_frame.data(), write_frame.size(), 0x20, txn) == + make_error_code(GpioErrc::wrong_op)); +} + +// ── Wire codec: write request ───────────────────────────────────────────────── + +TEST_CASE("GPIO write request encode/decode round-trips, including evt[2:0]", "[gpio][REQ-GPIO-028]") { + auto frame = encode_write_request(0x20, 0xDEADBEEF, WriteSemantics::Or, 3); + PinMask bitmask = 0; + WriteSemantics evt{}; + uint8_t txn = 0; + REQUIRE_FALSE(decode_write_request(frame.data(), frame.size(), 0x20, bitmask, evt, txn)); + REQUIRE(bitmask == 0xDEADBEEF); + REQUIRE(evt == WriteSemantics::Or); + REQUIRE(txn == 3); +} + +TEST_CASE("decode_write_request rejects a malformed or misaddressed frame", "[gpio][REQ-GPIO-029]") { + auto frame = encode_write_request(0x20, 0, WriteSemantics::Replace, 1); + PinMask bitmask = 0; + WriteSemantics evt{}; + uint8_t txn = 0; + REQUIRE(decode_write_request(frame.data(), frame.size(), 0x21, bitmask, evt, txn) == + make_error_code(GpioErrc::wrong_bus)); + + auto read_frame = encode_read_request(0x20, 1); + REQUIRE(decode_write_request(read_frame.data(), read_frame.size(), 0x20, bitmask, evt, txn) == + make_error_code(GpioErrc::wrong_op)); + + rcp::acf::AcfMessageInfo bad_len_info; + bad_len_info.byte_bus_id = 0x20; + bad_len_info.op = true; + auto bad_len_frame = rcp::acf::encode_acf_abb(bad_len_info, std::vector{0x01}); + REQUIRE(decode_write_request(bad_len_frame.data(), bad_len_frame.size(), 0x20, bitmask, evt, txn) == + make_error_code(GpioErrc::bad_payload_len)); +} + +TEST_CASE("decode_write_request rejects the reserved evt[2:0]=100b value", "[gpio][REQ-GPIO-012][REQ-GPIO-045]") { + auto frame = encode_write_request(0x20, 0xFF, WriteSemantics::Reserved, 1); + PinMask bitmask = 0xAAAAAAAA; + WriteSemantics evt = WriteSemantics::Or; + uint8_t txn = 0xFF; + auto ec = decode_write_request(frame.data(), frame.size(), 0x20, bitmask, evt, txn); + REQUIRE(ec == make_error_code(GpioErrc::reserved_evt)); +} + +TEST_CASE("encode_write_request masks evt to its low 3 bits", "[gpio][REQ-GPIO-028]") { + // Add (5) is within range; verify the round trip carries it faithfully. + auto frame = encode_write_request(0x20, 1, WriteSemantics::Add, 1); + PinMask bitmask = 0; + WriteSemantics evt{}; + uint8_t txn = 0; + REQUIRE_FALSE(decode_write_request(frame.data(), frame.size(), 0x20, bitmask, evt, txn)); + REQUIRE(evt == WriteSemantics::Add); +} + +// ── Wire codec: response ────────────────────────────────────────────────────── + +TEST_CASE("GPIO response encode/decode round-trips when untimed", "[gpio][REQ-GPIO-030]") { + auto frame = encode_response(0x20, 0x12345678, 9, /*timed=*/false, 0); + PinMask bitmask = 0; + bool timed = true; + uint64_t ts = 1; + uint8_t txn = 0; + REQUIRE_FALSE(decode_response(frame.data(), frame.size(), 0x20, bitmask, timed, ts, txn)); + REQUIRE(bitmask == 0x12345678); + REQUIRE_FALSE(timed); + REQUIRE(ts == 0); + REQUIRE(txn == 9); +} + +TEST_CASE("GPIO response encode/decode round-trips when timed", "[gpio][REQ-GPIO-031]") { + auto frame = encode_response(0x20, 0xAABBCCDD, 2, /*timed=*/true, 55555); + PinMask bitmask = 0; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_response(frame.data(), frame.size(), 0x20, bitmask, timed, ts, txn)); + REQUIRE(bitmask == 0xAABBCCDD); + REQUIRE(timed); + REQUIRE(ts == 55555); +} + +TEST_CASE("decode_response rejects a malformed or misaddressed frame", "[gpio][REQ-GPIO-032]") { + auto frame = encode_response(0x20, 0, 1, false, 0); + PinMask bitmask = 0; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE(decode_response(frame.data(), frame.size(), 0x21, bitmask, timed, ts, txn) == + make_error_code(GpioErrc::wrong_bus)); + + std::vector short_frame{0x00}; + REQUIRE(decode_response(short_frame.data(), 0, 0x20, bitmask, timed, ts, txn) == + make_error_code(GpioErrc::short_frame)); + + rcp::acf::AcfMessageInfo bad_len_info; + bad_len_info.byte_bus_id = 0x20; + auto bad_len_frame = rcp::acf::encode_acf_abb(bad_len_info, std::vector{0x01, 0x02}); + REQUIRE(decode_response(bad_len_frame.data(), bad_len_frame.size(), 0x20, bitmask, timed, ts, txn) == + make_error_code(GpioErrc::bad_payload_len)); +} + +// ── Functional config block wiring (pre-Phase-3 opaque-blob helpers) ──────── TEST_CASE("encode/decode_gpio_functional_config round-trips through the opaque regmap blob", "[gpio][REQ-GPIO-006]") { std::array edges{}; - edges[0] = 0b001; // Change - edges[5] = 0b110; // Rising + Falling + edges[0] = 0b001; + edges[5] = 0b110; auto cfg = encode_gpio_functional_config(0xF0F0F0F0, edges); REQUIRE(cfg.data.size() == kGpioFunctionalConfigLen); @@ -214,8 +681,7 @@ TEST_CASE("encode/decode_gpio_functional_config round-trips through the opaque r TEST_CASE("decode_gpio_functional_config rejects an undersized blob", "[gpio][REQ-GPIO-006]") { rcp::regmap::EndpointFunctionalConfig cfg; - cfg.data = {0x01, 0x02}; // far short of kGpioFunctionalConfigLen - + cfg.data = {0x01, 0x02}; PinMask out_directions = 0; std::array out_edges{}; auto ec = decode_gpio_functional_config(cfg, out_directions, out_edges); @@ -230,8 +696,6 @@ TEST_CASE("GpioEndpoint::handle_write updates state and fires triggers in one ca ep.triggers().enable(gpio_signal_id(0, GpioEdge::Change)); ep.triggers().enable(gpio_signal_id(0, GpioEdge::Rising)); - // Pin 0 must be configured as output before a write to it can take - // effect (REQ-GPIO-009) — GpioState defaults every pin to input. PinMask out_value = 0; REQUIRE_FALSE(ep.handle_write(WriteSemantics::Reconfigure, /*operand=*/0x1, out_value)); @@ -245,18 +709,10 @@ TEST_CASE("GpioEndpoint::handle_write updates state and fires triggers in one ca } TEST_CASE("GpioEndpoint::handle_write propagates a Reserved-semantics error without changing state", - "[gpio][REQ-GPIO-008]") { + "[gpio][REQ-GPIO-012]") { GpioEndpoint ep; PinMask out_value = 0; auto ec = ep.handle_write(WriteSemantics::Reserved, 0xFF, out_value); REQUIRE(ec); REQUIRE(ep.read() == 0); } - -// ── GpioErrc category sanity ────────────────────────────────────────────────── - -TEST_CASE("GpioErrc reports a non-empty message in its own category", "[gpio][REQ-GPIO-008]") { - auto ec = make_error_code(GpioErrc::pin_index_out_of_range); - REQUIRE(ec.category() == gpio_category()); - REQUIRE_FALSE(ec.message().empty()); -} diff --git a/tests/test_mock.cpp b/tests/test_mock.cpp index d1fe0f4..bbf257c 100644 --- a/tests/test_mock.cpp +++ b/tests/test_mock.cpp @@ -359,20 +359,42 @@ TEST_CASE("ADC request with a reserved evt[2:0] (001b-110b) is rejected with wir 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", +TEST_CASE("ADC request with evt[2:0]==111b (config-write) and a malformed payload is rejected with " + "wire error code INVALID_PARAMETER rather than crashing or being treated as a plain read", "[mock][REQ-MOCK-013]") { + // Phase 3: adc.hpp's evt[2:0]==111b configuration-write path + // (adc::apply_reconfig) is now genuinely implemented, so this mock's + // own dispatch_adc() routes ConfigWrite requests there instead of + // rejecting them outright — see dispatch_adc's own comment. A payload + // with no address+data octet is still rejected, now with + // AdcErrc::reconfig_short / INVALID_PARAMETER rather than the old + // (pre-Phase-3) "not implemented at all" UNSUPPORTED_CMD. mock::Server server; REQUIRE_FALSE(server.advance_to_rcp_configured()); - auto req = standard_request(mock::kAdcByteBusId, /*write=*/false, /*evt_op=*/7); + auto req = standard_request(mock::kAdcByteBusId, /*write=*/true, /*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(ec == adc::make_error_code(adc::AdcErrc::reconfig_short)); REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); REQUIRE(resp_payload == - std::vector{static_cast(acf::WireErrorCode::UnsupportedCmd)}); + std::vector{static_cast(acf::WireErrorCode::InvalidParameter)}); +} + +TEST_CASE("ADC request with evt[2:0]==111b (config-write) and a well-formed payload actually " + "patches this server's own ADC functional config", + "[mock][REQ-MOCK-013]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + // Address 0x0008 (adc_base_clk_divider) + one data octet. + std::vector payload{0x00, 0x08, 0x09}; + auto req = standard_request(mock::kAdcByteBusId, /*write=*/true, /*evt_op=*/7); + acf::AcfMessageInfo resp; + std::vector resp_payload; + REQUIRE_FALSE(server.dispatch(0, req, payload, resp, resp_payload)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::WriteResponse); } TEST_CASE("ADC request is rejected before RCP_CONFIGURED, same operational gating as GPIO/SPI/I2C",