From 4d8445ff8cc2450552ae2bfd11117f5b34abcd20 Mon Sep 17 00:00:00 2001 From: Matt <47545907+SoundMatt@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:14:01 -0700 Subject: [PATCH] rewrite(phase3): port pwm and wakeup from c-RCP, fix PWM_OUT Subtract operand order Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17), branch rewrite/v3-from-c-rcp. Third of Phase 3 Wave 2's three batches, alongside mdio and spi/uart (separate PRs). ## pwm.hpp - Fixed the same operand-order bug class found and fixed in gpio.hpp during PR #137: PWM_OUT's Subtract conditional-request handling used current - operand instead of c-RCP's normative request - current. This is deliberately fixed per-endpoint (not centralized into endpoint.hpp), matching the same caution documented in #137 that this bug needed independent re-verification at every endpoint using Subtract semantics, not an assumption that one fix covers all of them -- confirmed here to be a real, independent instance of the same class, not a duplicate report. - Ported against c-RCP's current RC5-conformant ep_pwm.h/.c. ## wakeup.hpp - Prior cpp-RCP wakeup.hpp content was a wholesale invention with no real relationship to c-RCP's actual ep_wakeup.h/.c -- re-derived from scratch against c-RCP's current implementation. - REQ-WAKEUP-020/026's regmap.c-scoped behavior intentionally NOT ported here (out of scope for the endpoint module itself; deferred to Phase 4's regmap port, noted in-file). ## Call sites None touched -- mock.hpp and powerstate.hpp (and their tests) confirmed compatible with the new pwm.hpp/wakeup.hpp APIs without modification. ## Verification Full tree rebuilds clean (223/223 targets), 56/56 ctest suites pass (100%) -- independently re-verified from a fresh build in the worktree, not just the porting agent's own report. Co-Authored-By: Claude Sonnet 5 Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com> --- include/rcp/pwm.hpp | 1172 +++++++++++++++++++++++++++++++++++----- include/rcp/wakeup.hpp | 767 +++++++++++++++++++++++--- tests/test_pwm.cpp | 863 ++++++++++++++++++++++++++--- tests/test_wakeup.cpp | 558 +++++++++++++++++-- 4 files changed, 3004 insertions(+), 356 deletions(-) diff --git a/include/rcp/pwm.hpp b/include/rcp/pwm.hpp index dd72283..0c38b81 100644 --- a/include/rcp/pwm.hpp +++ b/include/rcp/pwm.hpp @@ -7,110 +7,223 @@ // fusa:req REQ-PWM-007 // fusa:req REQ-PWM-008 // fusa:req REQ-PWM-009 +// fusa:req REQ-PWM-010 +// fusa:req REQ-PWM-011 +// fusa:req REQ-PWM-012 +// fusa:req REQ-PWM-013 +// fusa:req REQ-PWM-014 +// fusa:req REQ-PWM-015 +// fusa:req REQ-PWM-016 +// fusa:req REQ-PWM-017 +// fusa:req REQ-PWM-018 +// fusa:req REQ-PWM-019 +// fusa:req REQ-PWM-020 +// fusa:req REQ-PWM-021 +// fusa:req REQ-PWM-022 +// fusa:req REQ-PWM-023 +// fusa:req REQ-PWM-024 +// fusa:req REQ-PWM-025 +// fusa:req REQ-PWM-026 +// fusa:req REQ-PWM-027 +// fusa:req REQ-PWM-028 +// fusa:req REQ-PWM-029 +// fusa:req REQ-PWM-030 +// fusa:req REQ-PWM-031 +// fusa:req REQ-PWM-032 +// fusa:req REQ-PWM-033 +// fusa:req REQ-PWM-034 +// fusa:req REQ-PWM-035 +// fusa:req REQ-PWM-036 +// fusa:req REQ-PWM-037 +// fusa:req REQ-PWM-038 +// fusa:req REQ-PWM-039 +// fusa:req REQ-PWM-040 +// fusa:req REQ-PWM-041 +// fusa:req REQ-PWM-042 +// fusa:req REQ-PWM-043 +// fusa:req REQ-PWM-044 +// fusa:req REQ-PWM-045 +// fusa:req REQ-PWM-046 +// fusa:req REQ-PWM-047 +// fusa:req REQ-PWM-048 +// fusa:req REQ-PWM-049 +// fusa:req REQ-PWM-050 +// fusa:req REQ-PWM-051 +// fusa:req REQ-PWM-052 +// fusa:req REQ-PWM-053 +// fusa:req REQ-PWM-054 +// fusa:req REQ-PWM-055 +// fusa:req REQ-PWM-056 +// fusa:req REQ-PWM-057 +// fusa:req REQ-PWM-058 +// fusa:req REQ-PWM-059 +// fusa:req REQ-PWM-060 +// fusa:req REQ-PWM-061 +// fusa:req REQ-PWM-062 +// fusa:req REQ-PWM-063 +// fusa:req REQ-PWM-064 +// fusa:req REQ-PWM-065 +// fusa:req REQ-PWM-066 +// fusa:req REQ-PWM-067 +// fusa:req REQ-PWM-068 +// fusa:req REQ-PWM-069 +// fusa:req REQ-PWM-070 +// fusa:req REQ-PWM-071 +// fusa:req REQ-PWM-072 +// fusa:req REQ-PWM-073 +// fusa:req REQ-PWM-074 +// fusa:req REQ-PWM-075 // PWM_OUT (ep_type 0x07) and PWM_IN (ep_type 0x08) endpoints — the shared -// period/active-duration two-field payload shape, PWM_OUT's fixed 4-byte -// wire payload, PWM_IN's response-only read model and PWM_IN_NO_SIGNAL -// error path, and the mid-pulse trigger signal used to key ADC sampling -// cadence (extraction §5.5, §5.6). +// period/active-duration 4-byte payload shape, PWM_OUT's eight GPIO-style +// evt[2:0] write semantics plus duty-cycle capping and skew-delayed trigger +// timing, PWM_IN's response-only read model with a real EP_func register +// block and MAX_PERIOD timeout classification, and the compound-wait +// numeric comparison modes against a captured PWM_IN measurement (extraction +// §13.7.5, §13.7.6, §13.5.1). // -// ROADMAP.md milestone 48, "Basic Endpoint Types II — I2C, UART, ADC, -// PWM_OUT, PWM_IN (v2.4.0)": kept in one header, mirroring how -// rcp/endpoint.hpp is shared scaffolding for rcp/gpio.hpp and rcp/spi.hpp -// rather than each having its own copy — PWM_OUT and PWM_IN share one -// payload struct (PwmValue) and differ only in read/write direction and -// trigger-signal behavior. +// ROADMAP.md Phase 17 / cpp-RCP issue #129, Phase 3 ("Per-endpoint +// modules"): this header is re-derived from c-RCP's ep_pwm.h/ep_pwm.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. c-RCP models PWM_OUT/PWM_IN as +// free functions (no stateful "endpoint" object of its own); this port +// composes that same behavior into PwmOutEndpoint/PwmInEndpoint, matching +// rcp/gpio.hpp's and rcp/adc.hpp's own established Phase 3 endpoint-class +// idiom. // -// Wire-format fix (issue #70, cpp-RCP-01): PWM_OUT/PWM_IN's payload is a -// fixed 4 bytes on the wire — two big-endian 16-bit words, PWM_Period -// followed by PWM_active, verified against the OPEN Alliance TC18 Remote -// Control Protocol Specification's "pwmo request format" figure (§13.7.5.3) -// — not the open-ended 8-byte uint32_t pair this header modeled before. -// `period`/`active_duration` are narrowed to uint16_t and encode_pwm_payload/ -// decode_pwm_payload below produce/consume exactly that 4-byte, period-then- -// active big-endian layout, reusing rcp/avtp.hpp's own put_u16/get_u16 -// helpers the same way rcp/gpio.hpp's encode_gpio_payload/ -// decode_gpio_payload already reuse its put_u32/get_u32 for GPIO's 4-byte -// payload, rather than re-deriving byte order here. The field *order* in -// PwmValue (period first, then active_duration) was already correct and is -// unchanged — only the two fields' width and the addition of an explicit -// wire codec are new. +// Content re-verified against c-RCP's *current* ep_pwm.h/.c, not assumed +// accurate from this header's own earlier "Table 30/33 Row 2 evt[2:0] +// validation" pilot-module history (pre-Phase-3, v2.4.0) — that pass's own +// Plain/Reserved/ConfigWrite classification of PwmInEndpoint::handle_request +// via endpoint::evt_row2_kind_of was re-checked against c-RCP's +// rcp_ep_pwm_in_decode_read_request()/rcp_acf_evt_row2_is_plain() and found +// still correct; its trigger-table citation was NOT ("Table 44" — actually +// Table 47, "pwmi trigger outputs", REQ-PWM-032/033/034; fixed below, +// alongside a second pre-existing citation error c-RCP's own file header +// documents having made and corrected in ITS history, "Table 45" for +// PWM_IN's own functional-config table when the real Table 45 is "pwmo +// trigger outputs" — PWM_OUT's own table, unrelated to PWM_IN — corrected in +// c-RCP 2026-08-14, issue #428). // -// PWM_OUT write-semantics correction (issue #104, cpp-RCP-14): issue #70 -// had narrowed PwmOutEndpoint::handle_write to WriteSemantics::Replace only, -// on the reasoning that the PWM_OUT-specific request-handling section -// (§13.7.5.3) describes only one write behavior. That reasoning missed that -// §13.7.5.3 doesn't need to redescribe write semantics at all: §13.5 Table -// 30 ("EP specific usage of evt-field") is the governing table for -// evt[2:0]'s meaning across every endpoint type, and its GPIO/PWM_OUT row -// explicitly assigns PWM_OUT the *same* eight write semantics as GPIO — -// including Add/Subtract, whose own worked examples in that row name -// PWM_out's duty cycle directly ("this can be used to increase/decrease the -// duty cycle of PWM_out"). rcp::endpoint::saturating_add/saturating_subtract -// (below, in endpoint.hpp) were built templated on the caller's unsigned -// width specifically so GPIO's 32-bit pin mask and PWM_OUT's narrower -// period/duration fields could share one implementation — v2.4.0's own -// roadmap note this file's header used to cite — which issue #70 then -// contradicted without updating. PwmOutEndpoint::handle_write below now -// applies every non-Reconfigure write semantics via -// rcp::endpoint::apply_bitmask_write, per field (period and -// active_duration independently, each saturating within its own uint16_t -// range per Table 30's saturation note). Reconfigure remains rejected: this -// endpoint type has no EP_func addressed-write path implemented yet (a -// separate, larger gap common to every endpoint but one in this codebase, -// not specific to PWM_OUT's write-semantics bug this fixes). +// Real content deltas this Phase 3 pass ported/fixed, beyond the pre-Phase-3 +// wire codec (encode_pwm_payload/decode_pwm_payload, unchanged) and PWM_OUT +// write-semantics correction (issue #104, cpp-RCP-14, unchanged): // -// Table 30/33 Row 2 evt[2:0] validation (post-v2.4.0, third endpoint type -// after I2C and ADC): PwmInEndpoint::handle_request is this header's own -// wiring of rcp::endpoint::evt_row2_kind_of — the shared 3-way evt[2:0] -// classifier for Table 33's {ADC, PWM_IN, I2C, LIN, CAN, UART, ISELED, -// MDIO} row — into PWM_IN's request decode, following the exact shape -// rcp/i2c.hpp's I2cEndpoint::handle_request and rcp/adc.hpp's -// AdcEndpoint::handle_request established. PWM_IN's own request-handling -// section (§13.7.6.3) does not redescribe evt-bit semantics itself — it -// says only "The interpretation of the evt-bits is described in the -// section 'The usage of evt bits in requests'", i.e. §13.5 Table 33 — so -// this milestone's classification is not a new spec fact, just PWM_IN's -// own wiring of the rule Table 33 already states. Plain (evt[2:0]==000b) -// delegates to handle_read (PWM_IN's existing response-only read model -// above, unchanged by this milestone): the most recently recorded -// measurement, or PwmErrc::no_signal if none has ever been captured (or -// the signal was subsequently lost). Reserved (001b-110b) is rejected -// with endpoint::EndpointErrc::reserved_evt_row2 without touching -// `out_value`. ConfigWrite (evt[2:0]==111b, §12.7.1) is reported as -// PwmErrc::config_write_not_supported rather than crashing or silently -// accepted as a plain read — PWM_IN has no EP_functional-config wiring in -// this codebase yet (same gap I2C's and ADC's own handle_request comments -// call out for their own endpoint types), so full §12.7.1 handling is out -// of scope here too. PWM_OUT's own write path (PwmOutEndpoint:: -// handle_write, GPIO/PWM_OUT's Table 33 row) is unrelated and untouched by -// this milestone. +// 1. PWM_OUT Subtract operand order (found during this pass, mirroring +// rcp/gpio.hpp's own already-fixed REQ-GPIO-011): apply_write_field's +// Subtract case computed saturating_subtract(current, operand) — +// current MINUS request — the reverse of Table 33's own GPIO/PWM_OUT +// row, which (like GPIO's own row) defines Subtract as "byte_msg_payload +// minus current interface status", request MINUS current +// (REQ-PWM-007). Verified against c-RCP's own saturating_sub_u16(current, +// request) => "(current > request) ? 0 : (request - current)". Fixed +// below to saturating_subtract(operand, current). This bug was +// independent of, but the same operand-order class as, the PWM_IN +// compound-wait polarity bug below — GPIO's own header comment +// (rcp/gpio.hpp, REQ-GPIO-011) explicitly flagged this exact module as +// "not yet re-verified against its own current c-RCP counterpart" when +// it fixed its own copy of the identical mistake; this pass is that +// re-verification. +// 2. PWM_IN compound-wait comparison polarity (c-RCP issue #256 Group B, +// REQ-PWM-049..052): TC18 §13.5.1's own GE/LE naming is stated from the +// wire payload's own point of view — evt[2:0]=100b/110b ("GE") is met +// when byte_msg_payload (threshold) is >= the current interface status +// (captured), i.e. captured <= threshold; evt=101b/111b ("LE") is the +// mirror, captured >= threshold. Ported as compound_wait_compare() +// below, matching c-RCP's own corrected rcp_ep_pwm_in_compound_wait_ +// compare() exactly (c-RCP's own file header records this direction was +// itself once inverted and fixed — see that function's own doc comment +// there); this header had no compound-wait comparison of any kind before +// this pass, so there was no inverted copy of it to carry forward, only +// the correct polarity to port fresh. A regression test below pins this +// exact polarity. +// 3. PWM_OUT trigger-event tick derivation (REQ-PWM-055/067, +// "PWM-055" fix): trigger_events_at_tick() below derives which of Table +// 45's CYCLE_START/MID_PULSE events fire at a given elapsed clock tick, +// honoring both TC18 rules this header previously had no equivalent of +// at all — trigger timing tracks the pwmo_skew-DELAYED cycle edge, not +// the undelayed source edge, and MID_PULSE fires unconditionally at +// active_duration/2 past that delayed start, including +// active_duration == 0 (coincident with CYCLE_START, not suppressed). +// 4. PWM_IN's EP_func register block (TC18 §13.7.6.2 Table 48, +// REQ-PWM-058/070/071): entirely missing before this pass — PwmInEndpoint +// ::handle_request's ConfigWrite branch could only ever report +// PwmErrc::config_write_not_supported, the same "N of 11 endpoint types" +// accounting gap c-RCP's own issue #256 Group I found and closed for +// this identical module. render_registers()/apply_reconfig() below are +// the real register-block content (this port's own dispatch layer does +// not yet route an incoming evt=111b request into apply_reconfig() — +// see the "// TODO(phase3-followup)" marker on PwmInEndpoint below). +// 5. PWM_IN MAX_PERIOD timeout classification (REQ-PWM-072..075) — this +// endpoint type's own analogue of rcp/gpio.hpp's debounce_sample() +// (REQ-GPIO-035) and response_timing() (REQ-GPIO-036): a pure classifier +// a caller drives with a real measured period and the configured +// max_period/err_on_max_period/resp_on_err_enabled bits, since this +// module owns no timer of its own. Entirely new; ported as +// max_period_outcome()/pwm_in_wire_error() below. +// 6. PWM_OUT duty-cycle capping (REQ-PWM-056) and generation-state +// classification (REQ-PWM-057/068/069) — both entirely new; ported as +// apply_write()/generation_state() below. +// 7. PWM_IN's trigger model (REQ-PWM-032..034) was redesigned from this +// header's own pre-Phase-3 invention (record_edge()/record_measurement() +// unconditionally firing an always-armed RisingEdge/FallingEdge +// TriggerRegistry pair on every capture) to match c-RCP's real Table 47 +// shape: a single, mutually-exclusive, client-selected PwmInTrigger +// (None/Rising/Falling) evaluated against real level transitions via +// trigger_fires(trigger, prev_level, new_level) — Table 47 names these +// as PWM_IN's two fixed hardware trigger signals with no register field +// selecting among them, so (matching c-RCP's own file header note) a +// real implementation most naturally exposes both; the exclusive-select +// `trigger` field is this codebase's own original simplification, same +// disclaimer c-RCP's own rcp_ep_pwm_in_trigger_t carries. Both +// `record_edge`/`record_measurement`'s prior unconditional-both-fire +// behavior via the generic TriggerRegistry is retained for source +// compatibility (PWM_IN's read model and rcp/mock.hpp's own +// record_measurement() call site are otherwise unaffected by this +// rework), alongside the new trigger_fires() classifier. +// +// Full ACF-level wire codec for both endpoint types (encode/decode read +// request, PWM_OUT write request including the reserved evt[2:0]=100b +// rejection, response) is ported below too, matching rcp/gpio.hpp's/ +// rcp/adc.hpp's own Phase 3 wire-codec pattern — PWM_OUT/PWM_IN were never +// wired into rcp/mock.hpp's simulated dispatch loop (only PWM_IN's plain-read +// path is, via PwmInEndpoint::handle_request, unchanged in shape by this +// pass), so this is new, additive surface with no existing call site to +// preserve compatibility with. // // Field names and behavior below implement TC18's *behavior* as described in // an internal structured extraction of the specification named above; no // text from that document is reproduced here. The concrete period/duration // unit (left as an opaque implementation-defined tick count, same as -// rcp/regmap.hpp's own field-width disclaimers) and trigger-signal id -// scheme chosen in this file are this implementation's own, same as the -// equivalent disclaimers in rcp/avtp.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, -// rcp/gpio.hpp, rcp/i2c.hpp, and rcp/adc.hpp. +// rcp/regmap.hpp's own field-width disclaimers) and trigger-signal id scheme +// chosen in this file are this implementation's own, same as the equivalent +// disclaimers in rcp/avtp.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, rcp/gpio.hpp, +// rcp/i2c.hpp, and rcp/adc.hpp. #pragma once +#include #include #include +#include #include #include +#include #include #include +#include namespace rcp { namespace pwm { // ── Shared period/active-duration payload shape ─────────────────────────────── // `period` and `active_duration` share one implementation-defined tick unit -// (extraction §5.5, §5.6); this header does not itself fix that unit to a -// physical time value. Both fields are 16 bits wide on the wire — the +// (extraction §13.7.5, §13.7.6); this header does not itself fix that unit to +// a physical time value. Both fields are 16 bits wide on the wire — the // PWM_OUT/PWM_IN payload is a fixed 4 bytes total (verified against the // spec's "pwmo request format" figure, §13.7.5.3), not an open-ended // 32-bit-per-field pair. @@ -120,12 +233,12 @@ struct PwmValue { uint16_t active_duration = 0; }; -// ── Wire codec ──────────────────────────────────────────────────────────────── +// ── Wire codec: shared payload ──────────────────────────────────────────────── // Fixed 4-byte payload: PWM_Period (big-endian 16 bit) followed by PWM_active // (big-endian 16 bit), per the spec's "pwmo request format" figure // (§13.7.5.3) — a request not having exactly four bytes is rejected there // with INVALID_PARAMETER, which decode_pwm_payload below surfaces as -// avtp::AvtpErrc::short_buffer for a too-short buffer. +// avtp::AvtpErrc::short_buffer for a buffer of the wrong length. constexpr size_t kPwmPayloadLen = 4; using PwmWireBytes = std::array; @@ -147,19 +260,54 @@ inline std::error_code decode_pwm_payload(const uint8_t* buf, size_t len, PwmVal return {}; } +// The sentinel value either field of a PWM_IN response — or, by ep_adc.h's +// own reuse in c-RCP, an ADC raw sample (see rcp/adc.hpp's own kAdcNoSignal, +// defined locally there at this identical numeric value rather than +// depending on this header — see that header's own file comment) — carries +// on the wire when no valid measurement completed within the applicable +// timeout window (extraction §13.7.6). This is a WIRE-level constant: this +// header's own PwmInEndpoint below models "no signal" as an explicit +// has_signal_ bool + PwmErrc::no_signal error code for its own handle_read/ +// handle_request surface (unchanged from before this pass), not by stuffing +// this sentinel into PwmValue's fields — kPwmInNoSignal exists for wire +// round-tripping (REQ-PWM-047) and for compound_wait_compare() below, which +// (matching c-RCP's own rcp_ep_pwm_in_compound_wait_compare()) operates on a +// raw captured PwmValue that may legitimately carry this sentinel in either +// field. +constexpr uint16_t kPwmInNoSignal = 0xFFFF; + // ── Errors ──────────────────────────────────────────────────────────────────── enum class PwmErrc : int { no_signal = 1, // PWM_IN_NO_SIGNAL: no pulse has been measured yet // evt_row2_kind_of classified the request as ConfigWrite (evt[2:0] == - // 111b, §12.7.1). This milestone deliberately does not implement the - // configuration-write shape (relative EP_functional-config start - // address + configuration data) — see PwmInEndpoint::handle_request's - // own comment. Reported explicitly rather than silently accepted as a - // plain read or silently ignored, same as I2C's - // I2cErrc::config_write_not_supported and ADC's + // 111b, §12.7.1). PwmInEndpoint::handle_request (unchanged by this pass) + // deliberately does not implement the configuration-write shape through + // that entry point — see that method's own comment. Reported explicitly + // rather than silently accepted as a plain read or silently ignored, + // same as I2C's I2cErrc::config_write_not_supported and ADC's // AdcErrc::config_write_not_supported. config_write_not_supported = 2, + short_frame = 3, + bad_msg_type = 4, + wrong_bus = 5, + wrong_op = 6, + bad_payload_len = 7, + // 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. + reserved_evt = 8, + // PWM_IN's own read-request decode: evt[2:0] is not one of the + // plain-request values Table 33 Row 2 accepts for this endpoint type + // (REQ-PWM-059). + bad_evt = 9, + // Configuration write (evt[2:0]==111b) payload carries no address + // prefix, or an address prefix with no data octet after it. + reconfig_short = 10, + // Configuration write's start_address + data length exceeds the EP_func + // block's own length -- the whole write is ignored, per the + // specification's own rule. + reconfig_out_of_range = 11, }; inline const std::error_category& pwm_category() noexcept { @@ -170,6 +318,17 @@ inline const std::error_category& pwm_category() noexcept { case PwmErrc::no_signal: return "rcp/pwm: PWM_IN_NO_SIGNAL — no pulse measured"; case PwmErrc::config_write_not_supported: return "rcp/pwm: evt[2:0]=111b configuration-write requests are not yet implemented"; + case PwmErrc::short_frame: return "rcp/pwm: frame too short"; + case PwmErrc::bad_msg_type: return "rcp/pwm: unexpected ACF message type"; + case PwmErrc::wrong_bus: return "rcp/pwm: wrong byte_bus_id"; + case PwmErrc::wrong_op: return "rcp/pwm: wrong ACF op"; + case PwmErrc::bad_payload_len: return "rcp/pwm: unexpected payload length"; + case PwmErrc::reserved_evt: return "rcp/pwm: evt[2:0] is the reserved value 100b"; + case PwmErrc::bad_evt: return "rcp/pwm: PWM_IN evt[2:0] is not a valid plain-request value"; + case PwmErrc::reconfig_short: + return "rcp/pwm: configuration write has no address and data"; + case PwmErrc::reconfig_out_of_range: + return "rcp/pwm: configuration write extends past the EP_func block"; default: return "rcp/pwm: unknown error"; } } @@ -182,15 +341,45 @@ inline std::error_code make_error_code(PwmErrc e) noexcept { return {static_cast(e), pwm_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 PwmErrc value with no numbered counterpart +// (local framing/routing outcomes, or a specification rule that says to +// ignore the write rather than respond with an error). +inline std::optional wire_error(PwmErrc e) noexcept { + switch (e) { + // TC18 §13.7.5.3: "A request not having exactly four bytes is rejected + // and an error response with error code = INVALID_PARAMETER will be + // sent." (mirrors rcp/gpio.hpp's own wire_error(), REQ-GPIO-033/ + // REQ-PWM-028.) + case PwmErrc::bad_payload_len: return acf::WireErrorCode::InvalidParameter; + // TC18 §13.5 Table 33's GPIO/PWM_OUT row, evt[2:0]=100b (REQ-PWM-008), + // and PWM_IN's own bad-evt/config-write-not-supported cases (Table 33 + // Row 2): all three are UNSUPPORTED_CMD. + case PwmErrc::reserved_evt: return acf::WireErrorCode::UnsupportedCmd; + case PwmErrc::bad_evt: return acf::WireErrorCode::UnsupportedCmd; + case PwmErrc::config_write_not_supported: return acf::WireErrorCode::UnsupportedCmd; + // Table 27's own dedicated PWM_IN_NO_SIGNAL(9) code — unlike ADC's own + // internal no_signal condition (adc::AdcErrc::no_signal, which has no + // TC18-defined code and maps to EpError in rcp/mock.hpp's own + // translation), PWM_IN has a real numbered code for this condition. + case PwmErrc::no_signal: return acf::WireErrorCode::PwmInNoSignal; + default: return std::nullopt; + } +} + +// ── PWM_OUT: write-semantics application ───────────────────────────────────── // apply_write_field is PWM_OUT's own field-width combinator: it mirrors -// rcp::endpoint::apply_bitmask_write's case set exactly, but instantiates +// rcp::endpoint::apply_bitmask_write's case set, but instantiates // saturating_add/saturating_subtract at uint16_t rather than uint32_t — -// apply_bitmask_write itself is hardcoded to uint32_t (see its own -// declaration in endpoint.hpp), so calling it directly against a uint16_t -// field and narrowing the uint32_t result back down would compute the -// Add/Subtract saturation bound at the wrong width (saturating at -// 0xFFFFFFFF, then truncating — silently wrapping instead of the 0xFFFF -// saturation Table 30's own note requires for a 16-bit field). +// apply_bitmask_write itself is hardcoded to uint32_t, so calling it +// directly against a uint16_t field and narrowing the result back down would +// compute the Add/Subtract saturation bound at the wrong width. +// +// Subtract's operand order (REQ-PWM-007; found and fixed during this pass — +// see the file header's own delta-list item 1): request MINUS current, not +// the reverse — saturating_subtract(operand, current), not +// saturating_subtract(current, operand). inline std::error_code apply_write_field(endpoint::WriteSemantics op, uint16_t current, uint16_t operand, uint16_t& out) noexcept { switch (op) { @@ -199,7 +388,7 @@ inline std::error_code apply_write_field(endpoint::WriteSemantics op, uint16_t c case endpoint::WriteSemantics::And: out = static_cast(current & operand); return {}; case endpoint::WriteSemantics::Xor: out = static_cast(current ^ operand); return {}; case endpoint::WriteSemantics::Add: out = endpoint::saturating_add(current, operand); return {}; - case endpoint::WriteSemantics::Subtract: out = endpoint::saturating_subtract(current, operand); return {}; + case endpoint::WriteSemantics::Subtract: out = endpoint::saturating_subtract(operand, current); return {}; case endpoint::WriteSemantics::Reserved: return endpoint::make_error_code(endpoint::EndpointErrc::reserved_write_semantics); case endpoint::WriteSemantics::Reconfigure: @@ -208,14 +397,423 @@ inline std::error_code apply_write_field(endpoint::WriteSemantics op, uint16_t c } } +// apply_write is PWM_OUT's own faithful port of c-RCP's +// rcp_ep_pwm_out_apply_write(): unlike apply_write_field above (which rejects +// Reserved/Reconfigure with an error, matching this codebase's own +// decode-time-rejection idiom, see rcp/gpio.hpp's apply_gpio_write), this +// function is INFALLIBLE, mirroring c-RCP's own pure-classifier shape +// exactly — Reserved leaves each field unchanged (the "ignored" half of +// Table 33's own two-part reserved-value rule; the "err-response" half is +// decode_write_request()'s own PwmErrc::reserved_evt below) and Reconfigure +// does too (fail-safe for a caller that violates the "never RECONFIG here" +// contract). After evt's own semantics are applied, active_duration (only — +// Table 46 names only "PWM active", not the whole period) is CAPPED into +// [duty_cycle_min, duty_cycle_max] (REQ-PWM-056), applied unconditionally +// including for the Reserved/Reconfigure "unchanged" cases — idempotent if +// current already satisfied the limits, self-correcting if the limits +// themselves changed since active_duration was last written. A caller +// passing duty_cycle_min > duty_cycle_max (not itself validated here) gets +// duty_cycle_min applied last and so wins, the same "later cap always wins" +// fail-safe a caller relying on either limit alone would see. +inline PwmValue apply_write(const PwmValue& current, const PwmValue& request, endpoint::WriteSemantics evt, + uint16_t duty_cycle_min, uint16_t duty_cycle_max) noexcept { + PwmValue result; + uint16_t period_out = current.period; + uint16_t active_out = current.active_duration; + if (evt != endpoint::WriteSemantics::Reserved && evt != endpoint::WriteSemantics::Reconfigure) { + // apply_write_field only ever fails for Reserved/Reconfigure, both + // excluded here, so its error is unreachable and safely ignored. + (void)apply_write_field(evt, current.period, request.period, period_out); + (void)apply_write_field(evt, current.active_duration, request.active_duration, active_out); + } + result.period = period_out; + result.active_duration = active_out; + + if (result.active_duration < duty_cycle_min) result.active_duration = duty_cycle_min; + if (result.active_duration > duty_cycle_max) result.active_duration = duty_cycle_max; + return result; +} + +// ── PWM_OUT: signal-generation state (REQ-PWM-057/068/069) ─────────────────── +// A pure classifier of the endpoint's own {period, active_duration} pair — +// §13.7.5.3: a request with PWM_Period == 0 stops generation; PWM_active == +// 0 with PWM_Period > 0 leaves the PWM active but the output disabled +// (triggers still fire); otherwise ordinary generation. +enum class PwmOutGenerationState : uint8_t { + Stopped = 0, // period == 0 + OutputDisabled = 1, // active_duration == 0, period != 0 + Running = 2, // period != 0 and active_duration != 0 +}; + +inline PwmOutGenerationState generation_state(PwmValue value) noexcept { + if (value.period == 0) return PwmOutGenerationState::Stopped; + if (value.active_duration == 0) return PwmOutGenerationState::OutputDisabled; + return PwmOutGenerationState::Running; +} + +// ── PWM_OUT: triggers ───────────────────────────────────────────────────────── +// Table 45 ("pwmo trigger outputs") names three fixed hardware trigger +// signals (exec-done, cycle-start, mid-pulse) with no register field +// selecting among them (c-RCP-AUDIT-06, issue #256 Group C) — the +// mutually-exclusive `PwmOutTrigger` field (plus a None/off state Table 45 +// doesn't define) is this implementation's own simplification, matching +// c-RCP's own rcp_ep_pwm_out_trigger_t. + +enum class PwmOutTrigger : uint8_t { None = 0, CycleStart = 1, MidPulse = 2, Done = 3 }; +enum class PwmOutEvent : uint8_t { CycleStart = 0, MidPulse = 1, Done = 2 }; + +inline bool trigger_fires(PwmOutTrigger trigger, PwmOutEvent event) noexcept { + switch (trigger) { + case PwmOutTrigger::CycleStart: return event == PwmOutEvent::CycleStart; + case PwmOutTrigger::MidPulse: return event == PwmOutEvent::MidPulse; + case PwmOutTrigger::Done: return event == PwmOutEvent::Done; + case PwmOutTrigger::None: + default: return false; + } +} + +constexpr uint8_t kPwmOutTriggerEventCycleStart = 0x01; +constexpr uint8_t kPwmOutTriggerEventMidPulse = 0x02; + +// trigger_events_at_tick derives WHICH of CYCLE_START/MID_PULSE actually +// occur at a given elapsed clock-source tick (REQ-PWM-055/067; "PWM-055"): +// +// (1) "For trigger signal generation the delayed signal is used" — `skew` +// delays the primary edge by `skew` clock ticks (break-before-make +// half/full-bridge support); trigger timing tracks that DELAYED edge, +// not the undelayed source edge raw_tick is measured from. +// (2) "in the middle of the active pulse (even in case duty cycle is 0%)" +// — MID_PULSE is evaluated unconditionally at active_duration/2 ticks +// past the delayed cycle start, including active_duration == 0, where +// this naturally coincides with CYCLE_START (both fire together) +// rather than being suppressed. +// +// raw_tick is the elapsed tick count since the UNDELAYED cycle's own rising +// edge, 0-based; a caller already tracking phase passes raw_tick % period +// itself. period == 0 (Stopped) yields 0 unconditionally — a stopped +// generator has no cycle to derive a phase within. Table 45's own event 0 +// ("PWM request exec done") is deliberately NOT derived here — nothing ties +// it to cycle timing; it stays the one-shot, caller-driven signal +// trigger_fires() already models via PwmOutEvent::Done. A caller composes +// this function's output with trigger_fires(cfg.trigger, event) per set bit. +inline uint8_t trigger_events_at_tick(uint16_t period, uint16_t active_duration, uint8_t skew, + uint32_t raw_tick) noexcept { + if (period == 0) return 0; + + const uint32_t skew_mod = static_cast(skew) % period; + const uint32_t delayed_tick = (raw_tick % period + period - skew_mod) % period; + + uint8_t events = 0; + if (delayed_tick == 0) events |= kPwmOutTriggerEventCycleStart; + if (delayed_tick == static_cast(active_duration / 2)) events |= kPwmOutTriggerEventMidPulse; + return events; +} + +// ── PWM_OUT: functional config (Table 46) ───────────────────────────────────── +// Flattens regmap.h's shared functional-config "common" prefix directly into +// this struct's own bools, matching rcp/adc.hpp's own AdcFunctionalConfig +// rationale (cpp-RCP's rcp/regmap.hpp leaves EndpointFunctionalConfig::data +// opaque; render_registers()/apply_reconfig() below are this endpoint type's +// own full interpretation of that blob). +// +// NOTE (matches c-RCP's own zero-init exactly): a freshly default-constructed +// config's duty_cycle_min/duty_cycle_max are BOTH 0 — apply_write() above +// therefore caps every active_duration to 0 until a caller explicitly widens +// duty_cycle_max via Table 46's own registers (or set_duty_cycle_limits() +// below). This looks surprising for a "just try a plain Replace write" test, +// but is c-RCP's own literal zero-initialized register default, ported +// faithfully rather than substituted with a more convenient invented +// default. +struct PwmOutFunctionalConfig { + 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; + + PwmOutTrigger trigger = PwmOutTrigger::None; // this module's own field, not part of the EP_func block + uint16_t base_clk = 0; // 0x0004, R + uint16_t ep_status = 0; // 0x0006, R/W + uint8_t clk_divider = 0; // 0x0008, R/W + uint8_t signal_flags = 0; // 0x0009, R/W — see kPwmOutFlag* + uint16_t duty_cycle_min = 0; // 0x000A, R/W + uint16_t duty_cycle_max = 0; // 0x000C, R/W + uint8_t skew = 0; // 0x000E, R/W +}; + +constexpr uint8_t kPwmOutFlagInvPolarity = 1u << 0; +constexpr uint8_t kPwmOutFlagIdleState = 1u << 1; +constexpr uint8_t kPwmOutFlagIdleStateInv = 1u << 2; + +inline bool functional_cfg_writable(lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + return lifecycle::field_writable(state, lifecycle::FieldKind::FunctionalW, writer); +} + +inline bool set_trigger(PwmOutFunctionalConfig& cfg, PwmOutTrigger trigger, lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.trigger = trigger; + return true; +} + +inline bool set_enabled(PwmOutFunctionalConfig& cfg, bool enabled, lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + if (!functional_cfg_writable(state, writer)) return false; + cfg.ep_enable = enabled; + return true; +} + +// ── PWM_OUT: the EP_func register block (evt[2:0] == 111b) ─────────────────── + +constexpr uint16_t kPwmOutRegEpLen = 0x0000; // 8 bit, R +constexpr uint16_t kPwmOutRegReserved01 = 0x0001; // 8 bit, R +constexpr uint16_t kPwmOutRegEpEnableClr = 0x0002; // 8 bit, R/W +constexpr uint16_t kPwmOutRegEpOptions = 0x0003; // 8 bit, R/W +constexpr uint16_t kPwmOutRegBaseClk = 0x0004; // 16 bit, R +constexpr uint16_t kPwmOutRegEpStatus = 0x0006; // 16 bit, R/W +constexpr uint16_t kPwmOutRegClkDivider = 0x0008; // 8 bit, R/W +constexpr uint16_t kPwmOutRegSignalFlags = 0x0009; // 8 bit, R/W +constexpr uint16_t kPwmOutRegDutyCycleMin = 0x000A; // 16 bit, R/W +constexpr uint16_t kPwmOutRegDutyCycleMax = 0x000C; // 16 bit, R/W +constexpr uint16_t kPwmOutRegSkew = 0x000E; // 8 bit, R/W + +constexpr uint16_t kPwmOutEpFuncLen = 0x000F; +constexpr size_t kPwmOutReconfigAddrLen = 2; + +using PwmOutRegisterBlock = std::array; + +namespace detail { +constexpr uint8_t kPwmEnableClrBitEnable = 1u << 0; +constexpr uint8_t kPwmEnableClrBitClear = 1u << 4; +constexpr uint8_t kPwmOptionsBitReqCrc = 1u << 0; +constexpr uint8_t kPwmOptionsBitRespTs = 1u << 3; +constexpr uint8_t kPwmOptionsBitSuppress = 1u << 7; + +inline bool pwm_out_reg_offset_read_only(uint16_t addr) noexcept { + return addr == kPwmOutRegEpLen || addr == kPwmOutRegReserved01 || addr == kPwmOutRegBaseClk || + addr == static_cast(kPwmOutRegBaseClk + 1); +} +} // namespace detail + +inline void render_registers(const PwmOutFunctionalConfig& cfg, PwmOutRegisterBlock& out) noexcept { + uint8_t enable_clr = 0; + uint8_t options = 0; + if (cfg.ep_enable) enable_clr |= detail::kPwmEnableClrBitEnable; + if (cfg.ep_clear_req_storage) enable_clr |= detail::kPwmEnableClrBitClear; + if (cfg.ep_req_crc_enable) options |= detail::kPwmOptionsBitReqCrc; + if (cfg.ep_response_ts_enable) options |= detail::kPwmOptionsBitRespTs; + if (cfg.ep_suppress_response) options |= detail::kPwmOptionsBitSuppress; + + out[kPwmOutRegEpLen] = static_cast(kPwmOutEpFuncLen); + out[kPwmOutRegReserved01] = 0; + out[kPwmOutRegEpEnableClr] = enable_clr; + out[kPwmOutRegEpOptions] = options; + avtp::detail::put_u16(&out[kPwmOutRegBaseClk], cfg.base_clk); + avtp::detail::put_u16(&out[kPwmOutRegEpStatus], cfg.ep_status); + out[kPwmOutRegClkDivider] = cfg.clk_divider; + out[kPwmOutRegSignalFlags] = cfg.signal_flags; + avtp::detail::put_u16(&out[kPwmOutRegDutyCycleMin], cfg.duty_cycle_min); + avtp::detail::put_u16(&out[kPwmOutRegDutyCycleMax], cfg.duty_cycle_max); + out[kPwmOutRegSkew] = cfg.skew; +} + +namespace detail { +inline void pwm_out_parse_registers(PwmOutFunctionalConfig& cfg, const PwmOutRegisterBlock& in) noexcept { + const uint8_t enable_clr = in[kPwmOutRegEpEnableClr]; + const uint8_t options = in[kPwmOutRegEpOptions]; + + cfg.ep_enable = (enable_clr & kPwmEnableClrBitEnable) != 0; + cfg.ep_clear_req_storage = (enable_clr & kPwmEnableClrBitClear) != 0; + cfg.ep_req_crc_enable = (options & kPwmOptionsBitReqCrc) != 0; + cfg.ep_response_ts_enable = (options & kPwmOptionsBitRespTs) != 0; + cfg.ep_suppress_response = (options & kPwmOptionsBitSuppress) != 0; + + cfg.ep_status = avtp::detail::get_u16(&in[kPwmOutRegEpStatus]); + cfg.clk_divider = in[kPwmOutRegClkDivider]; + cfg.signal_flags = in[kPwmOutRegSignalFlags]; + cfg.duty_cycle_min = avtp::detail::get_u16(&in[kPwmOutRegDutyCycleMin]); + cfg.duty_cycle_max = avtp::detail::get_u16(&in[kPwmOutRegDutyCycleMax]); + cfg.skew = in[kPwmOutRegSkew]; +} +} // namespace detail + +inline std::error_code apply_reconfig(PwmOutFunctionalConfig& cfg, const uint8_t* payload, + size_t payload_len) { + if (payload_len <= kPwmOutReconfigAddrLen) return make_error_code(PwmErrc::reconfig_short); + + const uint16_t start_address = avtp::detail::get_u16(payload); + const size_t data_len = payload_len - kPwmOutReconfigAddrLen; + + if (static_cast(start_address) + data_len > static_cast(kPwmOutEpFuncLen)) + return make_error_code(PwmErrc::reconfig_out_of_range); + + PwmOutRegisterBlock block{}; + render_registers(cfg, block); + for (size_t i = 0; i < data_len; ++i) { + const auto addr = static_cast(start_address + i); + if (detail::pwm_out_reg_offset_read_only(addr)) continue; + block[addr] = payload[kPwmOutReconfigAddrLen + i]; + } + detail::pwm_out_parse_registers(cfg, block); + return {}; +} + +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 (kPwmOutReconfigAddrLen + data.size() > acf::kAcfAbbMaxPayload) return {}; + + std::vector payload(kPwmOutReconfigAddrLen + data.size()); + avtp::detail::put_u16(payload.data(), start_address); + std::copy(data.begin(), data.end(), payload.begin() + static_cast(kPwmOutReconfigAddrLen)); + + 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); +} + +// ── PWM_OUT: wire codec ─────────────────────────────────────────────────────── +// Ported directly from c-RCP's ep_pwm.c PWM_OUT wire functions, using +// rcp/acf.hpp's ACF_ABB/ACF_GBB codec, matching rcp/gpio.hpp's own pattern. + +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, {}); +} + +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(PwmErrc::short_frame); + if (ec) return make_error_code(PwmErrc::bad_msg_type); + + if (info.byte_bus_id != expected_bus_id) return make_error_code(PwmErrc::wrong_bus); + if (info.op) return make_error_code(PwmErrc::wrong_op); + + out_transaction_num = info.transaction_num; + return {}; +} + +// encode_write_request encodes an ACF_ABB write request: evt's low three +// bits carry evt, and the payload is value as kPwmPayloadLen big-endian +// octets (period then active_duration). +inline std::vector encode_write_request(avtp::ByteBusId byte_bus_id, PwmValue value, + 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; + const auto wire = encode_pwm_payload(value); + return acf::encode_acf_abb(info, std::vector(wire.begin(), wire.end())); +} + +// decode_write_request: REQ-PWM-008 — evt[2:0] == 100b (Reserved) is +// rejected with PwmErrc::reserved_evt (Table 33's GPIO/PWM_OUT row) — none +// of out_value/out_evt/out_transaction_num are populated in that case. +inline std::error_code decode_write_request(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + PwmValue& out_value, 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(PwmErrc::short_frame); + if (ec) return make_error_code(PwmErrc::bad_msg_type); + + if (info.byte_bus_id != expected_bus_id) return make_error_code(PwmErrc::wrong_bus); + if (!info.op) return make_error_code(PwmErrc::wrong_op); + if (payload.size() != kPwmPayloadLen) return make_error_code(PwmErrc::bad_payload_len); + + const auto evt = endpoint::write_semantics_of(info.evt_op); + if (evt == endpoint::WriteSemantics::Reserved) return make_error_code(PwmErrc::reserved_evt); + + PwmValue value{}; + (void)decode_pwm_payload(payload.data(), payload.size(), value); + out_value = value; + out_evt = evt; + out_transaction_num = info.transaction_num; + return {}; +} + +// encode_response/decode_response answer either a read or a write request, +// same ACF_ABB/ACF_GBB timed/untimed choice as rcp/gpio.hpp's own pair. +inline std::vector encode_response(avtp::ByteBusId byte_bus_id, PwmValue value, + uint8_t transaction_num, bool timed, uint64_t timestamp) { + const auto wire = encode_pwm_payload(value); + const std::vector payload(wire.begin(), wire.end()); + + 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); +} + +inline std::error_code decode_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + PwmValue& out_value, 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(PwmErrc::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(PwmErrc::short_frame); + if (ec) return make_error_code(PwmErrc::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(PwmErrc::short_frame); + if (ec) return make_error_code(PwmErrc::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(PwmErrc::wrong_bus); + if (payload.size() != kPwmPayloadLen) return make_error_code(PwmErrc::bad_payload_len); + + PwmValue value{}; + (void)decode_pwm_payload(payload.data(), payload.size(), value); + out_value = value; + out_transaction_num = txn; + return {}; +} + // ── PwmOutEndpoint (ep_type 0x07) ──────────────────────────────────────────── -// handle_write applies every write semantics Table 30's GPIO/PWM_OUT row +// handle_write applies every write semantics Table 30/33's GPIO/PWM_OUT row // defines — Replace/Or/And/Xor/Add/Subtract — via apply_write_field above, -// independently against `period` and `active_duration` (each its own -// uint16_t, so each combines and saturates within its own 16-bit range, per -// Table 30's saturation note). Reconfigure is rejected: PWM_OUT has no -// EP_func addressed-write path in this codebase yet (see the header comment -// above), so there is nothing for it to target. +// independently against `period` and `active_duration`, then caps +// active_duration into [cfg().duty_cycle_min, cfg().duty_cycle_max] +// (REQ-PWM-056, see PwmOutFunctionalConfig's own doc comment about its +// zero-initialized default). Reserved is rejected without mutating state; +// Reconfigure is rejected too — this endpoint's own EP_func register write +// is apply_reconfig() above, a separate entry point from handle_write, same +// split as rcp/gpio.hpp's apply_gpio_write/apply_reconfig. class PwmOutEndpoint { public: std::error_code handle_write(endpoint::WriteSemantics op, PwmValue operand, @@ -230,47 +828,336 @@ class PwmOutEndpoint { state_.period = new_period; state_.active_duration = new_active; - out_value = state_; + + if (state_.active_duration < cfg_.duty_cycle_min) state_.active_duration = cfg_.duty_cycle_min; + if (state_.active_duration > cfg_.duty_cycle_max) state_.active_duration = cfg_.duty_cycle_max; + + out_value = state_; return {}; } const PwmValue& read() const noexcept { return state_; } + PwmOutGenerationState generation() const noexcept { return generation_state(state_); } + + PwmOutFunctionalConfig& functional_cfg() noexcept { return cfg_; } + const PwmOutFunctionalConfig& functional_cfg() const noexcept { return cfg_; } private: - PwmValue state_; + PwmValue state_; + PwmOutFunctionalConfig cfg_; }; // ── PwmInEndpoint (ep_type 0x08) ───────────────────────────────────────────── -// Response-only read model (extraction §5.6): PWM_IN has no write request -// shape at all in this milestone's scope. Trigger signals fixed to match -// Table 44 ("pwmi trigger outputs") exactly (issue cpp-RCP-A4-pwmin): the -// spec defines two independent trigger outputs — rising edge (0) and -// falling edge (1) of the measured PWM_IN signal — not the single invented -// "MidPulse" signal this header modeled before. record_edge fires whichever -// one edge actually occurred; record_measurement (kept for -// rcp/adc.hpp's AdcCadence::ExternalTrigger pattern and existing callers) -// models one full input-capture cycle completing — which inherently spans -// both a rising and a falling edge of the measured signal — by recording -// the new value once and then firing both signals. +// Response-only read model (extraction §13.7.6): PWM_IN has no data write +// request shape. Trigger signals fixed to match Table 47 ("pwmi trigger +// outputs") — rising edge (0) and falling edge (1) of the measured PWM_IN +// signal — the two independent, always-on hardware trigger signals (issue +// cpp-RCP-A4-pwmin). record_edge fires whichever one edge actually occurred; +// record_measurement models one full input-capture cycle completing (both a +// rising and falling edge) by recording the new value once and firing both. enum class PwmInSignal : uint8_t { RisingEdge = 0, FallingEdge = 1 }; constexpr endpoint::TriggerRegistry::SignalId pwm_in_signal_id(PwmInSignal sig) noexcept { return static_cast(sig); } +// PwmInTrigger/trigger_fires (REQ-PWM-032..034): Table 47's own two trigger +// signals, modeled additionally as a single, mutually-exclusive, +// client-selected mode (plus a None/off state Table 47 doesn't define) — +// this implementation's own simplification, matching c-RCP's own +// rcp_ep_pwm_in_trigger_t (see the file header's own delta-list item 7). +// Independent of, and does not replace, the always-on TriggerRegistry pair +// above (record_edge/record_measurement) — a caller may use either or both. +enum class PwmInTrigger : uint8_t { None = 0, Rising = 1, Falling = 2 }; + +inline bool trigger_fires(PwmInTrigger trigger, bool prev_level, bool new_level) noexcept { + switch (trigger) { + case PwmInTrigger::Rising: return !prev_level && new_level; + case PwmInTrigger::Falling: return prev_level && !new_level; + case PwmInTrigger::None: + default: return false; + } +} + +// ── PWM_IN: functional config (Table 48) ────────────────────────────────────── + +struct PwmInFunctionalConfig { + 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; + + PwmInTrigger trigger = PwmInTrigger::None; // this module's own field, not part of the EP_func block + uint16_t base_clk = 0; // 0x0004, R + uint16_t ep_status = 0; // 0x0006, R/W + uint8_t clk_divider = 0; // 0x0008, R/W + uint8_t flags = 0; // 0x0009, R/W — see kPwmInFlag* + uint16_t max_period = 0; // 0x000A, R/W +}; + +constexpr uint8_t kPwmInFlagPolarity = 1u << 0; +constexpr uint8_t kPwmInFlagErrOnMaxPeriod = 1u << 1; +constexpr uint8_t kPwmInFlagContinuousMode = 1u << 2; + +inline bool pwm_in_functional_cfg_writable(lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + return lifecycle::field_writable(state, lifecycle::FieldKind::FunctionalW, writer); +} + +inline bool set_trigger(PwmInFunctionalConfig& cfg, PwmInTrigger trigger, lifecycle::ServerState state, + lifecycle::WriterCtx writer) noexcept { + if (!pwm_in_functional_cfg_writable(state, writer)) return false; + cfg.trigger = trigger; + return true; +} + +// ── PWM_IN: the EP_func register block (evt[2:0] == 111b), Table 48 ────────── +// FIXED (this pass, mirroring c-RCP's own issue #256 Group I fix): entirely +// missing before this pass — see the file header's own delta-list item 4. + +constexpr uint16_t kPwmInRegEpLen = 0x0000; // 8 bit, R +constexpr uint16_t kPwmInRegReserved01 = 0x0001; // 8 bit, R +constexpr uint16_t kPwmInRegEpEnableClr = 0x0002; // 8 bit, R/W +constexpr uint16_t kPwmInRegEpOptions = 0x0003; // 8 bit, R/W +constexpr uint16_t kPwmInRegBaseClk = 0x0004; // 16 bit, R +constexpr uint16_t kPwmInRegEpStatus = 0x0006; // 16 bit, R/W +constexpr uint16_t kPwmInRegClkDivider = 0x0008; // 8 bit, R/W +constexpr uint16_t kPwmInRegFlags = 0x0009; // 8 bit, R/W +constexpr uint16_t kPwmInRegMaxPeriod = 0x000A; // 16 bit, R/W + +constexpr uint16_t kPwmInEpFuncLen = 0x000C; +constexpr size_t kPwmInReconfigAddrLen = 2; + +using PwmInRegisterBlock = std::array; + +namespace detail { +inline bool pwm_in_reg_offset_read_only(uint16_t addr) noexcept { + return addr == kPwmInRegEpLen || addr == kPwmInRegReserved01 || addr == kPwmInRegBaseClk || + addr == static_cast(kPwmInRegBaseClk + 1); +} +} // namespace detail + +inline void render_registers(const PwmInFunctionalConfig& cfg, PwmInRegisterBlock& out) noexcept { + uint8_t enable_clr = 0; + uint8_t options = 0; + if (cfg.ep_enable) enable_clr |= detail::kPwmEnableClrBitEnable; + if (cfg.ep_clear_req_storage) enable_clr |= detail::kPwmEnableClrBitClear; + if (cfg.ep_req_crc_enable) options |= detail::kPwmOptionsBitReqCrc; + if (cfg.ep_response_ts_enable) options |= detail::kPwmOptionsBitRespTs; + if (cfg.ep_suppress_response) options |= detail::kPwmOptionsBitSuppress; + + out[kPwmInRegEpLen] = static_cast(kPwmInEpFuncLen); + out[kPwmInRegReserved01] = 0; + out[kPwmInRegEpEnableClr] = enable_clr; + out[kPwmInRegEpOptions] = options; + avtp::detail::put_u16(&out[kPwmInRegBaseClk], cfg.base_clk); + avtp::detail::put_u16(&out[kPwmInRegEpStatus], cfg.ep_status); + out[kPwmInRegClkDivider] = cfg.clk_divider; + out[kPwmInRegFlags] = cfg.flags; + avtp::detail::put_u16(&out[kPwmInRegMaxPeriod], cfg.max_period); +} + +namespace detail { +inline void pwm_in_parse_registers(PwmInFunctionalConfig& cfg, const PwmInRegisterBlock& in) noexcept { + const uint8_t enable_clr = in[kPwmInRegEpEnableClr]; + const uint8_t options = in[kPwmInRegEpOptions]; + + cfg.ep_enable = (enable_clr & kPwmEnableClrBitEnable) != 0; + cfg.ep_clear_req_storage = (enable_clr & kPwmEnableClrBitClear) != 0; + cfg.ep_req_crc_enable = (options & kPwmOptionsBitReqCrc) != 0; + cfg.ep_response_ts_enable = (options & kPwmOptionsBitRespTs) != 0; + cfg.ep_suppress_response = (options & kPwmOptionsBitSuppress) != 0; + + cfg.ep_status = avtp::detail::get_u16(&in[kPwmInRegEpStatus]); + cfg.clk_divider = in[kPwmInRegClkDivider]; + cfg.flags = in[kPwmInRegFlags]; + cfg.max_period = avtp::detail::get_u16(&in[kPwmInRegMaxPeriod]); +} +} // namespace detail + +inline std::error_code apply_reconfig(PwmInFunctionalConfig& cfg, const uint8_t* payload, size_t payload_len) { + if (payload_len <= kPwmInReconfigAddrLen) return make_error_code(PwmErrc::reconfig_short); + + const uint16_t start_address = avtp::detail::get_u16(payload); + const size_t data_len = payload_len - kPwmInReconfigAddrLen; + + if (static_cast(start_address) + data_len > static_cast(kPwmInEpFuncLen)) + return make_error_code(PwmErrc::reconfig_out_of_range); + + PwmInRegisterBlock block{}; + render_registers(cfg, block); + for (size_t i = 0; i < data_len; ++i) { + const auto addr = static_cast(start_address + i); + if (detail::pwm_in_reg_offset_read_only(addr)) continue; + block[addr] = payload[kPwmInReconfigAddrLen + i]; + } + detail::pwm_in_parse_registers(cfg, block); + return {}; +} + +inline std::vector encode_pwm_in_reconfig_request(avtp::ByteBusId byte_bus_id, + uint16_t start_address, + const std::vector& data, + uint8_t transaction_num) { + if (data.empty()) return {}; + if (kPwmInReconfigAddrLen + data.size() > acf::kAcfAbbMaxPayload) return {}; + + std::vector payload(kPwmInReconfigAddrLen + data.size()); + avtp::detail::put_u16(payload.data(), start_address); + std::copy(data.begin(), data.end(), payload.begin() + static_cast(kPwmInReconfigAddrLen)); + + acf::AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.op = true; // write: §12.7.1 Figure 18 + info.evt_op = 0x7; // PWM_IN belongs to Table 33 Row 2's reserved-range group + // (ADC/I2C/LIN/CAN/UART/ISELED/MDIO), not GPIO/PWM_OUT's own + // eight-value write-semantics group, so the raw evt value is + // used directly, matching rcp/adc.hpp's own encode_reconfig_request. + info.transaction_num = transaction_num; + return acf::encode_acf_abb(info, payload); +} + +// ── PWM_IN: wire codec (read request / response) ────────────────────────────── + +inline std::vector encode_pwm_in_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_pwm_in_read_request: REQ-PWM-059 — evt[2:0] must be one of Table 33 +// Row 2's plain-request values (endpoint::evt_row2_kind_of == Plain); +// PwmErrc::bad_evt otherwise. +inline std::error_code decode_pwm_in_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(PwmErrc::short_frame); + if (ec) return make_error_code(PwmErrc::bad_msg_type); + + if (info.byte_bus_id != expected_bus_id) return make_error_code(PwmErrc::wrong_bus); + if (info.op) return make_error_code(PwmErrc::wrong_op); + if (endpoint::evt_row2_kind_of(info.evt_op) != endpoint::EvtRow2Kind::Plain) + return make_error_code(PwmErrc::bad_evt); + + out_transaction_num = info.transaction_num; + return {}; +} + +inline std::vector encode_pwm_in_response(avtp::ByteBusId byte_bus_id, PwmValue value, + uint8_t transaction_num, bool timed, uint64_t timestamp) { + return encode_response(byte_bus_id, value, transaction_num, timed, timestamp); +} + +inline std::error_code decode_pwm_in_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + PwmValue& out_value, bool& out_timed, uint64_t& out_timestamp, + uint8_t& out_transaction_num) { + return decode_response(b, len, expected_bus_id, out_value, out_timed, out_timestamp, out_transaction_num); +} + +// ── PWM_IN: MAX_PERIOD timeout classification (REQ-PWM-072..075) ───────────── +// This endpoint type's own analogue of rcp/gpio.hpp's debounce_sample() +// (REQ-GPIO-035) — a pure classifier a caller (owning the real timer this +// module does not) drives with one measured period. Table 48's own +// pwmi_err_on_max_period row: +// 0b: if MAX_PERIOD is exceeded, invalidate measurement and wait for a +// new active phase of signal. +// 1b: if MAX_PERIOD is exceeded, stop measurement and signal error if +// error response is enabled in EP_config (EP_RESP_ON_ERR). +// resp_on_err_enabled is EP_config's own EP_RESP_ON_ERR flag (a distinct +// register block from PwmInFunctionalConfig above), passed in already +// classified by the caller — this module never reaches into a register map +// to resolve it, matching this codebase's standing convention for every +// other caller-supplied classification flag. +enum class PwmInMaxPeriodOutcome : uint8_t { + Ok = 0, // measured_period <= max_period — no timeout + Invalidate = 1, // err_on_max_period == 0b + Stop = 2, // err_on_max_period == 1b, resp_on_err_enabled == false + StopAndError = 3, // err_on_max_period == 1b, resp_on_err_enabled == true +}; + +inline PwmInMaxPeriodOutcome max_period_outcome(uint16_t measured_period, uint16_t max_period, + bool err_on_max_period, bool resp_on_err_enabled) noexcept { + if (measured_period <= max_period) return PwmInMaxPeriodOutcome::Ok; + if (!err_on_max_period) return PwmInMaxPeriodOutcome::Invalidate; + return resp_on_err_enabled ? PwmInMaxPeriodOutcome::StopAndError : PwmInMaxPeriodOutcome::Stop; +} + +// pwm_in_wire_error maps outcome to its numbered wire error code — +// StopAndError is Table 48's own "stop measurement and signal error" +// outcome, mapping to the same acf::WireErrorCode::PwmInNoSignal (Table +// 27's own dedicated code) that PwmErrc::no_signal maps to above; every +// other outcome explicitly signals no error of its own. +inline std::optional pwm_in_wire_error(PwmInMaxPeriodOutcome outcome) noexcept { + if (outcome == PwmInMaxPeriodOutcome::StopAndError) return acf::WireErrorCode::PwmInNoSignal; + return std::nullopt; +} + +// ── Compound-wait's numeric ≥/≤ comparison modes against PWM_IN ────────────── +// A future compound-wait request (generic compound-wait plumbing is a later +// milestone) that targets a PWM_IN endpoint compares one of its two captured +// sub-fields (period or active-duration, read as the "duty-cycle sub-field") +// against a caller-supplied threshold using one of four numeric comparison +// modes selected by evt[2:0] = 4..7 (100b..111b, §13.5.1) — a property of +// this endpoint type itself, not of the compound-wait mechanism, so it is +// implemented and unit-tested here, following the same "isolated precedent" +// rcp/spi.hpp's compound_wait_status_equal() establishes. +enum class PwmInCompoundWaitMode : uint8_t { + PeriodGe = 4, // 100b + PeriodLe = 5, // 101b + DutyGe = 6, // 110b + DutyLe = 7, // 111b +}; + +inline bool compound_wait_mode_valid(uint8_t v) noexcept { + return v >= static_cast(PwmInCompoundWaitMode::PeriodGe) && + v <= static_cast(PwmInCompoundWaitMode::DutyLe); +} + +// compound_wait_compare — see the file header's own delta-list item 2 for +// the polarity fix this function's own comment pins with a regression test: +// TC18 §13.5.1: evt[2:0]=100b/110b ("GE") is met when byte_msg_payload +// (threshold) is >= the current interface status (captured) — i.e. +// threshold >= captured, equivalently captured <= threshold. evt=101b/111b +// ("LE") is the mirror: threshold <= captured, i.e. captured >= threshold. +// Returns false (never an error) for an invalid mode, and equally false +// whenever the relevant captured sub-field itself equals kPwmInNoSignal — a +// "no signal" measurement never satisfies (or fails to satisfy) a numeric +// comparison, it is simply never a match. +inline bool compound_wait_compare(PwmValue captured, PwmInCompoundWaitMode mode, uint16_t threshold) noexcept { + switch (mode) { + case PwmInCompoundWaitMode::PeriodGe: + if (captured.period == kPwmInNoSignal) return false; + return captured.period <= threshold; + case PwmInCompoundWaitMode::PeriodLe: + if (captured.period == kPwmInNoSignal) return false; + return captured.period >= threshold; + case PwmInCompoundWaitMode::DutyGe: + if (captured.active_duration == kPwmInNoSignal) return false; + return captured.active_duration <= threshold; + case PwmInCompoundWaitMode::DutyLe: + if (captured.active_duration == kPwmInNoSignal) return false; + return captured.active_duration >= threshold; + default: + return false; + } +} + class PwmInEndpoint { public: - // record_edge fires exactly one of Table 44's two trigger signals for - // an armed listener, without updating the last-measured value — for a - // caller that observes rising/falling edges independently rather than - // only at whole-cycle granularity. + // record_edge fires exactly one of Table 47's two trigger signals for + // an armed listener, without updating the last-measured value. void record_edge(PwmInSignal edge) noexcept { triggers_.notify(pwm_in_signal_id(edge)); } // record_measurement records one completed input-capture cycle (a full // period, spanning one rising and one falling edge of the measured - // signal) and fires both Table 44 trigger signals for any armed + // signal) and fires both Table 47 trigger signals for any armed // listener. void record_measurement(PwmValue value) noexcept { last_value_ = value; @@ -294,36 +1181,21 @@ class PwmInEndpoint { return {}; } - // handle_request is PWM_IN's request-decode entry point — the piece - // this header previously had none of, mirroring rcp::i2c::I2cEndpoint:: - // handle_request's and rcp::adc::AdcEndpoint::handle_request's shape - // exactly (this repo's third Table 33 Row 2 endpoint type after I2C - // and ADC). It classifies the incoming request's evt[2:0] field via - // rcp::endpoint::evt_row2_kind_of before doing anything else, so a - // Reserved value can never reach handle_read and be misread as an - // ordinary read, and a ConfigWrite value can never be silently - // accepted or silently dropped: - // - Plain (evt[2:0] == 000b): delegates straight to handle_read() - // with `out_value` unchanged — PWM_IN's existing response-only - // read model above already IS this row's correct "plain request" - // behavior (§13.7.6.3: "The interpretation of the evt-bits is - // described in the section 'The usage of evt bits in requests'", - // i.e. §13.5 Table 33). - // - Reserved (evt[2:0] in 001b-110b): returns - // endpoint::EndpointErrc::reserved_evt_row2 without touching - // `out_value` or any measured state — TC18 requires this be - // rejected with error code UNSUPPORTED_CMD. - // - ConfigWrite (evt[2:0] == 111b): §12.7.1's configuration-write - // shape targets the PWM_IN EP's own functional-config block - // (relative start address + configuration data), not a - // measurement read at all. Full handling is deliberately out of - // scope for this milestone (nontrivial — it needs - // EP_functional-config wiring this header does not yet have, the - // same gap I2C's and ADC's own handle_request comments defer for - // the identical reason); this returns - // PwmErrc::config_write_not_supported rather than crashing, - // silently accepting the request as a read, or silently doing - // nothing. + // handle_request is PWM_IN's request-decode entry point, classifying + // evt[2:0] via endpoint::evt_row2_kind_of (Table 33 Row 2) before ever + // touching last-measured state: + // - Plain (000b): delegates to handle_read(). + // - Reserved (001b-110b): endpoint::EndpointErrc::reserved_evt_row2, + // `out_value` untouched. + // - ConfigWrite (111b): PwmErrc::config_write_not_supported. + // TODO(phase3-followup): this endpoint type now has a real Table 48 + // EP_func register block (render_registers()/apply_reconfig() free + // functions above, and functional_cfg() below) — this entry point + // does not yet route a ConfigWrite request's raw payload into + // apply_reconfig() (handle_request's own signature carries no + // payload, only evt_op), matching this port's own scope boundary; + // a caller wanting real register writes today calls + // pwm::apply_reconfig(ep.functional_cfg(), payload, len) directly. std::error_code handle_request(uint8_t evt_op, PwmValue& out_value) { switch (endpoint::evt_row2_kind_of(evt_op)) { case endpoint::EvtRow2Kind::Plain: @@ -338,10 +1210,14 @@ class PwmInEndpoint { endpoint::TriggerRegistry& triggers() noexcept { return triggers_; } + PwmInFunctionalConfig& functional_cfg() noexcept { return cfg_; } + const PwmInFunctionalConfig& functional_cfg() const noexcept { return cfg_; } + private: PwmValue last_value_{}; bool has_signal_ = false; endpoint::TriggerRegistry triggers_; + PwmInFunctionalConfig cfg_; }; } // namespace pwm diff --git a/include/rcp/wakeup.hpp b/include/rcp/wakeup.hpp index 8e6f523..dc93e5b 100644 --- a/include/rcp/wakeup.hpp +++ b/include/rcp/wakeup.hpp @@ -3,50 +3,150 @@ // fusa:req REQ-WAKEUP-003 // fusa:req REQ-WAKEUP-004 // fusa:req REQ-WAKEUP-005 +// fusa:req REQ-WAKEUP-006 +// fusa:req REQ-WAKEUP-007 +// fusa:req REQ-WAKEUP-008 +// fusa:req REQ-WAKEUP-009 +// fusa:req REQ-WAKEUP-010 +// fusa:req REQ-WAKEUP-011 +// fusa:req REQ-WAKEUP-012 +// fusa:req REQ-WAKEUP-013 +// fusa:req REQ-WAKEUP-014 +// fusa:req REQ-WAKEUP-015 +// fusa:req REQ-WAKEUP-016 +// fusa:req REQ-WAKEUP-017 +// fusa:req REQ-WAKEUP-018 +// fusa:req REQ-WAKEUP-019 +// fusa:req REQ-WAKEUP-021 +// fusa:req REQ-WAKEUP-022 +// fusa:req REQ-WAKEUP-023 +// fusa:req REQ-WAKEUP-024 +// fusa:req REQ-WAKEUP-025 +// fusa:req REQ-WAKEUP-027 +// fusa:req REQ-WAKEUP-028 +// fusa:req REQ-WAKEUP-029 +// fusa:req REQ-WAKEUP-030 +// fusa:req REQ-WAKEUP-031 +// fusa:req REQ-WAKEUP-032 +// fusa:req REQ-WAKEUP-033 +// fusa:req REQ-WAKEUP-034 +// fusa:req REQ-WAKEUP-035 +// fusa:req REQ-WAKEUP-036 -// Wakeup control endpoint (ep_type 0x01) — the OPEN Alliance TC18 Remote -// Control Protocol Specification v0.5.1_RC's fixed SleepCMD request, -// wake-source pin monitoring, and the repeating WakeUp message handshake -// used during hot-start-from-Sleep (extraction §5.2, §3.3). +// WakeUp endpoint (ep_type 0x01) — TC18's dedicated power-management +// endpoint: the fixed-opcode SleepCMD request/response, the fixed-opcode +// WakeUp message (plain and carrying its own wake source), configurable +// wake-source pin monitoring (level- and edge-triggered), the per-source +// wup_status latch, and the EP_func functional-configuration register block +// (extraction §13.7.2, §12.4.1, §12.5). // -// ROADMAP.md milestone 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN -// XL), ISELED, MDIO, Wakeup Control (v2.7.0)": the point the roadmap calls -// out explicitly for Wakeup control is that its SleepCMD request is a -// single FIXED opcode byte (kSleepCmd below), not one of the mtv=0 -// RequestTypeOpcode kinds rcp/request.hpp's decode_request_type decodes -// (v2.5.0/v2.6.0) — the two are unrelated request-shape mechanisms that -// happen to both repurpose part of a message for a request-kind byte. This -// header has no dependency on rcp/request.hpp and does not call -// decode_request_type anywhere: decode_sleep_cmd below is Wakeup control's -// own, separate decode path, and a future reader must not route SleepCMD -// through the conditional-request taxonomy or vice versa. +// ROADMAP.md Phase 17 / cpp-RCP issue #129, Phase 3 ("Per-endpoint +// modules"): this header is re-derived from c-RCP's ep_wakeup.h/ep_wakeup.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. This header's own pre-Phase-3 content modeled none of the +// above: a single fixed byte compared for equality (kSleepCmd == 0xA5, at +// least numerically consistent with c-RCP's own SleepCMD opcode), an +// asleep_ bool, and one flat 32-bit wake_source_pins_ mask with no +// per-source polarity/edge configuration, no wup_status latch, no WakeUp +// message of any kind, and no EP_func register block at all — none of +// c-RCP's real WakeUp API redesign history (REQ-WAKEUP-018/021/022) or its +// EP_func register-block addition (issue #256 Group I) was reflected. This +// pass replaces that content with a faithful port of c-RCP's *current* +// ep_wakeup.h/.c (post all of that history), not the earlier invented model. // -// This is also the endpoint type ROADMAP.md milestone 53 ("Power Management -// Rebuild", v2.9.0, Phase 14) depends on directly — that milestone's -// power-state rebuild assumes WakeupEndpoint already exists as the place -// sleep/wake transitions and wake-source events are recorded. +// 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. +// +// ── Layering divergence from c-RCP (deliberate) ───────────────────────────── +// c-RCP's ep_wakeup.h depends on power.h for rcp_pwrmode_entry_result_t (the +// SleepCMD response's own payload) — the two modules are one directional +// dependency (wakeup -> power) there. cpp-RCP's existing rcp/powerstate.hpp +// (ROADMAP.md milestone 53, Phase 14, out of this pass's own scope) already +// established the OPPOSITE direction: it depends on rcp/wakeup.hpp's +// WakeupEndpoint (via wakeup_message_pending()/acknowledge_wakeup()), not +// the reverse — including from tests/test_powerstate.cpp, a real call site +// this pass must not break. Rather than invert cpp-RCP's own established +// layering (which would ripple into rcp/powerstate.hpp, out of this pass's +// scope) or duplicate rcp::powerstate::PowerMode's concept of an entry +// result here under a different name, this header defines its own minimal, +// local SleepCmdResult (Ok/Refused) for the SleepCMD response payload — +// this module's own original choice, not a port of c-RCP's +// rcp_pwrmode_entry_result_t, though numerically and behaviorally +// equivalent for this wire pair's own purposes. A caller gluing this header +// to rcp/powerstate.hpp's own PowerMode is expected to translate between +// the two at the call site, the same "primitive, not a scheduler" split +// every header in this codebase already follows for cross-module coupling. +// +// ── WakeupEndpoint's pre-Phase-3 API is preserved for rcp/powerstate.hpp ──── +// rcp/powerstate.hpp's PowerManager (and its own tests, +// tests/test_powerstate.cpp) construct a WakeupEndpoint by default and call +// exactly two of its methods: wakeup_message_pending() and +// acknowledge_wakeup() (plus record_wake_source_event() from their own +// tests, to arm the handshake). Those three methods, and their exact +// pre-Phase-3 semantics (a wake-source event arms a pending flag; +// acknowledge_wakeup() clears it; entering Sleep via handle_sleep_cmd() +// clears any handshake left over from a prior cycle), are kept byte-for-byte +// unchanged below — WakeupEndpoint now additionally owns a real +// WakeupFunctionalConfig (sources/wup_status/ep_status), but +// wakeup_message_pending()'s own pending-flag bookkeeping remains the +// simple bool this codebase's PowerManager already depends on, rather than +// being re-derived from wup_status (whose write-1-to-clear register +// semantics do not map cleanly onto "still owed a WakeUp repetition" without +// changing PowerManager's own contract, out of this pass's scope). // // 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 wake-source pin -// count and handshake state machine chosen in this file are this -// implementation's own, same as the equivalent disclaimers in +// count (kMaxWakeSources == 8, matching c-RCP's own +// RCP_EP_WAKEUP_MAX_SOURCES), the SleepCmdResult enum, and the exact split +// of responsibility across the methods below are this implementation's own +// encoding of that behavior, same as the equivalent disclaimers in // rcp/avtp.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, and rcp/request.hpp. #pragma once +#include +#include +#include + +#include +#include #include +#include #include #include +#include namespace rcp { namespace wakeup { -// ── SleepCMD — a fixed opcode, not a RequestTypeOpcode (see header comment) ── +// ── SleepCMD / WakeUp fixed opcodes ─────────────────────────────────────────── +// Both are fixed, single-byte request kinds — unlike every ordinary +// endpoint's own evt[2:0]-carrying request, neither is a member of +// rcp/request.hpp's conditional-request taxonomy. This header has no +// dependency on rcp/request.hpp. + +constexpr uint8_t kSleepCmdOpcode = 0xA5; // REQ-WAKEUP-010, TC18 §13.7.2.3 Figure 23 +constexpr uint8_t kWakeupOpcode = 0x5A; // REQ-WAKEUP-014, this module's own marker + // (TC18 defines no wire encoding for the + // repetitive WakeUp message itself) -constexpr uint8_t kSleepCmd = 0xA5; +// ── Errors ──────────────────────────────────────────────────────────────────── enum class WakeupErrc : int { - not_sleep_cmd = 1, // decode_sleep_cmd was given a byte other than kSleepCmd + short_frame = 1, + bad_msg_type = 2, + wrong_bus = 3, + bad_opcode = 4, + // Configuration write (evt[2:0]==111b) payload carries no address + // prefix, or an address prefix with no data octet after it. + reconfig_short = 5, + // Configuration write's start_address + data length exceeds the EP_func + // block's own length — the whole write is ignored, per the + // specification's own rule. + reconfig_out_of_range = 6, }; inline const std::error_category& wakeup_category() noexcept { @@ -54,10 +154,15 @@ inline const std::error_category& wakeup_category() noexcept { const char* name() const noexcept override { return "rcp.wakeup"; } std::string message(int ev) const override { switch (static_cast(ev)) { - case WakeupErrc::not_sleep_cmd: - return "rcp/wakeup: byte does not match the fixed SleepCMD opcode (0xA5)"; - default: - return "rcp/wakeup: unknown error"; + case WakeupErrc::short_frame: return "rcp/wakeup: frame too short"; + case WakeupErrc::bad_msg_type: return "rcp/wakeup: unexpected ACF message type"; + case WakeupErrc::wrong_bus: return "rcp/wakeup: wrong byte_bus_id"; + case WakeupErrc::bad_opcode: return "rcp/wakeup: wrong fixed opcode byte"; + case WakeupErrc::reconfig_short: + return "rcp/wakeup: configuration write has no address and data"; + case WakeupErrc::reconfig_out_of_range: + return "rcp/wakeup: configuration write extends past the EP_func block"; + default: return "rcp/wakeup: unknown error"; } } }; @@ -69,34 +174,559 @@ inline std::error_code make_error_code(WakeupErrc e) noexcept { return {static_cast(e), wakeup_category()}; } -// decode_sleep_cmd is Wakeup control's entire request-decode surface: a -// single fixed-byte comparison, not a taxonomy lookup. -inline std::error_code decode_sleep_cmd(uint8_t byte) noexcept { - if (byte != kSleepCmd) return make_error_code(WakeupErrc::not_sleep_cmd); +// ── Wake-source pin configuration/monitoring (Table 39/40) ─────────────────── + +constexpr size_t kMaxWakeSources = 8; // matches c-RCP's own RCP_EP_WAKEUP_MAX_SOURCES + +// One wake-source slot's own configuration (REQ-WAKEUP-022: LEVEL mode via +// enabled/active_high, or EDGE mode via either trigger_on_*_edge flag — +// either trigger bit set puts the slot in EDGE mode and active_high is then +// not consulted; both false, the zero-init default, means LEVEL mode). +struct WakeSourceCfg { + bool enabled = false; + bool active_high = false; // LEVEL mode only + uint16_t pin_number = 0; // wup_io_scrN's own [10:0] wire field + bool trigger_on_rising_edge = false; // EDGE mode + bool trigger_on_falling_edge = false; // EDGE mode +}; + +// REQ-WAKEUP-003: true iff cfg is enabled and pin_level matches cfg's own +// active_high polarity. LEVEL-mode only — deliberately unaffected by +// trigger_on_rising_edge/trigger_on_falling_edge; see +// source_edge_asserted() below for the EDGE-mode counterpart. +inline bool source_asserted(const WakeSourceCfg& cfg, bool pin_level) noexcept { + return cfg.enabled && (pin_level == cfg.active_high); +} + +// REQ-WAKEUP-004: true iff any of the first +// min(pin_levels.size(), kMaxWakeSources) entries of sources is currently +// asserted per source_asserted(). LEVEL-mode only. +inline bool any_source_asserted(const std::array& sources, + const std::vector& pin_levels) noexcept { + const size_t n = std::min(pin_levels.size(), kMaxWakeSources); + for (size_t i = 0; i < n; ++i) { + if (source_asserted(sources[i], pin_levels[i])) return true; + } + return false; +} + +// ── Edge-triggered wake-source detection (REQ-WAKEUP-022) ──────────────────── +// One previous-pin-level slot per wake-source: edge detection compares the +// CURRENT pin level against the PREVIOUS one, state a pure per-call +// predicate like source_asserted() cannot carry itself — the same +// caller-owned "has_previous" idiom this codebase already establishes +// elsewhere (e.g. rcp/gpio.hpp's GpioDebounceState). The very first +// observation only seeds previous_level, never fires. +struct SourceEdgeState { + bool has_previous = false; + bool previous_level = false; +}; + +// REQ-WAKEUP-032/033: the EDGE-aware counterpart to source_asserted() for a +// single source. If cfg is in LEVEL mode (both trigger flags false), state +// is left entirely untouched and this delegates to source_asserted(cfg, +// pin_level) — a single call site usable uniformly regardless of mode. +// Otherwise (EDGE mode): the very first call only seeds previous_level and +// returns false; every call after that returns true iff the level actually +// transitioned in a direction cfg's own trigger flags select (both true — +// "both edges" — fires on either transition), and state's previous_level is +// updated unconditionally (every call, whether or not it fires). A disabled +// EDGE-mode source still updates state but never fires. +inline bool source_edge_asserted(const WakeSourceCfg& cfg, SourceEdgeState& state, bool pin_level) noexcept { + if (!cfg.trigger_on_rising_edge && !cfg.trigger_on_falling_edge) { + return source_asserted(cfg, pin_level); + } + + if (!state.has_previous) { + state.has_previous = true; + state.previous_level = pin_level; + return false; + } + + const bool rose = !state.previous_level && pin_level; + const bool fell = state.previous_level && !pin_level; + const bool fired = cfg.enabled && + ((cfg.trigger_on_rising_edge && rose) || (cfg.trigger_on_falling_edge && fell)); + + state.previous_level = pin_level; + return fired; +} + +// REQ-WAKEUP-034: the EDGE-aware counterpart to any_source_asserted(). +// Deliberately does NOT short-circuit — every in-range source's own state +// must be updated on every call (see source_edge_asserted()'s own "every +// call updates state" contract), not just sources scanned before the first +// hit, or a later transition on an unscanned source would be silently +// missed. +inline bool any_source_edge_asserted(const std::array& sources, + std::array& states, + const std::vector& pin_levels) noexcept { + const size_t n = std::min(pin_levels.size(), kMaxWakeSources); + bool any = false; + for (size_t i = 0; i < n; ++i) { + if (source_edge_asserted(sources[i], states[i], pin_levels[i])) any = true; + } + return any; +} + +// ── wup_status latch (Table 39) ─────────────────────────────────────────────── +// REQ-WAKEUP-021: a genuine per-source bitmask ("each bit represents a +// wake-up source"), bit i corresponding to sources[i] — not a single +// aggregate "did anything wake the device" bool (this header's own +// pre-Phase-3 model, discarded by this pass — see the file header). +class WupStatus { +public: + void latch_source(size_t source_index) noexcept { + if (source_index >= kMaxWakeSources) return; + mask_ = static_cast(mask_ | (uint16_t{1} << source_index)); + } + + // REQ-WAKEUP-007: clears every latched bit at once — a caller drives + // this from a client register write that writes 1 to every bit + // position, or from a full functional-config reset. + void clear() noexcept { mask_ = 0; } + + // REQ-WAKEUP-027: clears exactly source_index's own bit, leaving every + // other source's own latch state untouched — TC18's own per-bit + // write-1-to-clear rule, applied one bit at a time by + // WakeupEndpoint::apply_reconfig() below. + void clear_source(size_t source_index) noexcept { + if (source_index >= kMaxWakeSources) return; + mask_ = static_cast(mask_ & ~(uint16_t{1} << source_index)); + } + + bool is_clear() const noexcept { return mask_ == 0; } + + // REQ-WAKEUP-028: true iff source_index's own bit is latched + // specifically. source_index >= kMaxWakeSources always returns false. + bool source_is_latched(size_t source_index) const noexcept { + if (source_index >= kMaxWakeSources) return false; + return (mask_ & (uint16_t{1} << source_index)) != 0; + } + + uint16_t mask() const noexcept { return mask_; } + +private: + uint16_t mask_ = 0; +}; + +// ── Functional config ───────────────────────────────────────────────────────── +// Flattens regmap.h's shared functional-config "common" prefix directly into +// this struct's own bools, matching rcp/adc.hpp's/rcp/pwm.hpp's own +// convention. repetition_time_us (REQ-WAKEUP-018, TC18 §12.4.1) is +// discoverable/settable over this in-memory API but is NOT itself +// wire-reachable — Table 39/40 (this endpoint's own EP_func block, fully +// mapped below) define no field for it, matching c-RCP's own +// rcp_ep_wakeup_functional_cfg_t::repetition_time_us doc comment. +struct WakeupFunctionalConfig { + 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 sources{}; + uint16_t ep_status = 0; // wup_ep_status, 0x0002, R/W + WupStatus wup_status; // wup_status, 0x0004, R/W + uint32_t repetition_time_us = 0; // REQ-WAKEUP-018; not wire-reachable, see above +}; + +inline bool functional_cfg_writable(lifecycle::ServerState state, lifecycle::WriterCtx writer) noexcept { + return lifecycle::field_writable(state, lifecycle::FieldKind::FunctionalW, writer); +} + +// REQ-PWRMODE-023 (TC18 §12.5): "The RC Client that is allowed to access the +// RC Server endpoint can request the entire RC Server implementation to +// enter standby or sleep mode." This codebase's only concept of "an RC +// Client allowed to access [an] endpoint" is lifecycle::WriterCtx's own +// root-client/discovery-stream classification — true iff +// writer.via_root_client_ep0, mirroring lifecycle::ServerLifecycle's own +// RcpConfigured-state authorization rule for the same reason: once +// RcpConfigured (the only state a SleepCMD is meaningful in), only the root +// client, not an unqualified discovery-stream sender, may act on server-wide +// power state. +inline bool sleepcmd_writable(lifecycle::WriterCtx writer) noexcept { + return writer.via_root_client_ep0; +} + +// ── SleepCMD request/response (0xA5) ────────────────────────────────────────── + +inline std::vector encode_sleepcmd_request(avtp::ByteBusId byte_bus_id, uint8_t transaction_num) { + acf::AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.transaction_num = transaction_num; + return acf::encode_acf_abb(info, {kSleepCmdOpcode}); +} + +inline std::error_code decode_sleepcmd_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(WakeupErrc::short_frame); + if (ec) return make_error_code(WakeupErrc::bad_msg_type); + + if (info.byte_bus_id != expected_bus_id) return make_error_code(WakeupErrc::wrong_bus); + if (payload.empty()) return make_error_code(WakeupErrc::short_frame); + if (payload[0] != kSleepCmdOpcode) return make_error_code(WakeupErrc::bad_opcode); + + out_transaction_num = info.transaction_num; return {}; } -// ── Wake-source pins ────────────────────────────────────────────────────────── +// This module's own minimal SleepCMD-response outcome — see the file +// header's own "Layering divergence from c-RCP" note for why this is not +// c-RCP's rcp_pwrmode_entry_result_t. +enum class SleepCmdResult : uint8_t { Ok = 0, Refused = 1 }; + +// REQ-WAKEUP-012/019: RCP_PWRMODE_ENTRY_OK-equivalent (Ok) encodes this +// module's own positive-form payload (opcode + result byte), echoing +// transaction_num. Refused instead returns a genuine ACF Error Response +// carrying acf::WireErrorCode::RequestCanceled (TC18 §12.5: "The RC Server +// will reject requests to enter sleep or standby mode and send an error +// message with error code = REQUEST_CANCELED"), not this module's own +// positive-form payload with a "refused" byte — a conformant peer watching +// for an error response sees the refusal. +inline std::vector encode_sleepcmd_response(avtp::ByteBusId byte_bus_id, SleepCmdResult result, + uint8_t transaction_num) { + if (result == SleepCmdResult::Refused) { + return acf::build_error_response(byte_bus_id, transaction_num, acf::WireErrorCode::RequestCanceled); + } + + acf::AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.rsp = true; + info.transaction_num = transaction_num; + return acf::encode_acf_abb(info, {kSleepCmdOpcode, static_cast(result)}); +} + +// REQ-WAKEUP-013/023/025: an Error Response (info.err set) is recognized as +// the refused-entry half of this pair — *out_result is Refused iff its +// payload carries WireErrorCode::RequestCanceled (the only code this +// function's own encode counterpart ever builds); any other err payload is +// WakeupErrc::bad_opcode, not silently reinterpreted. A non-error response +// decodes the fixed-opcode/short-frame/wrong-bus/bad-opcode failure modes +// same as decode_sleepcmd_request(); any second payload byte other than +// SleepCmdResult::Ok's own raw value (0) decodes as Refused (fail-safe: an +// unrecognized result byte is never treated as an admitted entry). +inline std::error_code decode_sleepcmd_response(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + SleepCmdResult& out_result, 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(WakeupErrc::short_frame); + if (ec) return make_error_code(WakeupErrc::bad_msg_type); -using WakeSourceMask = uint32_t; -constexpr uint8_t kMaxWakeSourcePins = 32; + if (info.byte_bus_id != expected_bus_id) return make_error_code(WakeupErrc::wrong_bus); + + if (info.err) { + if (payload.empty() || payload[0] != static_cast(acf::WireErrorCode::RequestCanceled)) { + return make_error_code(WakeupErrc::bad_opcode); + } + out_result = SleepCmdResult::Refused; + out_transaction_num = info.transaction_num; + return {}; + } + + if (payload.size() < 2) return make_error_code(WakeupErrc::short_frame); + if (payload[0] != kSleepCmdOpcode) return make_error_code(WakeupErrc::bad_opcode); + + out_result = (payload[1] == static_cast(SleepCmdResult::Ok)) ? SleepCmdResult::Ok + : SleepCmdResult::Refused; + out_transaction_num = info.transaction_num; + return {}; +} + +// ── WakeUp-message emission (§12.4.1, §13.7.2.1) ────────────────────────────── + +inline std::vector encode_wakeup_message(avtp::ByteBusId byte_bus_id, uint8_t transaction_num) { + acf::AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.transaction_num = transaction_num; + return acf::encode_acf_abb(info, {kWakeupOpcode}); +} + +inline std::error_code decode_wakeup_message(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(WakeupErrc::short_frame); + if (ec) return make_error_code(WakeupErrc::bad_msg_type); + + if (info.byte_bus_id != expected_bus_id) return make_error_code(WakeupErrc::wrong_bus); + if (payload.empty()) return make_error_code(WakeupErrc::short_frame); + if (payload[0] != kWakeupOpcode) return make_error_code(WakeupErrc::bad_opcode); + + out_transaction_num = info.transaction_num; + return {}; +} + +// REQ-WAKEUP-016: true iff b decodes as a valid WakeUp message addressed to +// expected_bus_id whose transaction number equals sent_transaction_num — +// this module's own "is this the echo of the WakeUp message I sent" +// predicate, meant to feed a hot-start-from-Sleep handshake's own `echoed` +// input. False for any decode failure or transaction-number mismatch. +inline bool is_wakeup_echo(const uint8_t* b, size_t len, avtp::ByteBusId expected_bus_id, + uint8_t sent_transaction_num) noexcept { + uint8_t txn = 0; + if (decode_wakeup_message(b, len, expected_bus_id, txn)) return false; + return txn == sent_transaction_num; +} + +// REQ-WAKEUP-017: three classes of wake source TC18 §12.4.1's own text +// names — "an internal EP signal" (a configured wake-source pin), "the +// dedicated wakepin" (named separately from the configured pin table), and +// "a TC14/TC10 wake-up request on the network". TC18 defines no wire +// encoding for this classification (same disclaimer as SleepCMD's own +// response payload above) — this enum and the 3-byte message shape below +// are this module's own original design, matching c-RCP's own +// rcp_ep_wakeup_source_t exactly. +enum class WakeupSource : uint8_t { + Unknown = 0, // no wake-source information available/applicable + Io = 1, // a configured wake-source pin — see source_index + Wakepin = 2, // TC18 §12.4.1's own "the dedicated wakepin" + Network = 3, // TC18 §12.4.1's own "TC14/TC10 wake-up request on the network" +}; + +// source_index's own sentinel for "not applicable to this source +// classification" — every classification other than WakeupSource::Io +// carries this value. +constexpr uint8_t kWakeupSourceIndexNa = 0xFF; + +// Encodes the same message as encode_wakeup_message(), but with 2 additional +// payload bytes: source and source_index. The plain decode_wakeup_message()/ +// is_wakeup_echo() pair still decodes a message built by this function +// correctly (they only ever check payload.size() >= 1 and payload[0]) — this +// is a strictly additive wire extension. +inline std::vector encode_wakeup_message_with_source(avtp::ByteBusId byte_bus_id, + uint8_t transaction_num, WakeupSource source, + uint8_t source_index) { + acf::AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.transaction_num = transaction_num; + return acf::encode_acf_abb(info, {kWakeupOpcode, static_cast(source), source_index}); +} + +// Decodes and validates the 3-byte shape encode_wakeup_message_with_source() +// builds. A message built by the plain encode_wakeup_message() (only 1 +// payload byte) is REJECTED here with WakeupErrc::short_frame — this +// decoder's own contract is the 3-byte shape specifically. Fails with +// WakeupErrc::bad_opcode if the source byte is not one of this enum's own +// 4 defined values (fail-safe: never silently reinterpreted as Unknown). +inline std::error_code decode_wakeup_message_with_source(const uint8_t* b, size_t len, + avtp::ByteBusId expected_bus_id, + uint8_t& out_transaction_num, + WakeupSource& out_source, uint8_t& out_source_index) { + 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(WakeupErrc::short_frame); + if (ec) return make_error_code(WakeupErrc::bad_msg_type); + + if (info.byte_bus_id != expected_bus_id) return make_error_code(WakeupErrc::wrong_bus); + if (payload.size() < 3) return make_error_code(WakeupErrc::short_frame); + if (payload[0] != kWakeupOpcode) return make_error_code(WakeupErrc::bad_opcode); + + switch (payload[1]) { + case static_cast(WakeupSource::Unknown): + case static_cast(WakeupSource::Io): + case static_cast(WakeupSource::Wakepin): + case static_cast(WakeupSource::Network): + break; + default: + return make_error_code(WakeupErrc::bad_opcode); + } + + out_transaction_num = info.transaction_num; + out_source = static_cast(payload[1]); + out_source_index = payload[2]; + return {}; +} + +// ── The EP_func register block (evt[2:0] == 111b), Table 39/40 ────────────── +// FIXED/ADDED (this pass, mirroring c-RCP's own issue #256 Group I fix): +// entirely missing before this pass. TC18's own source table has a genuine +// address collision between wup_status and the wake-source array's own +// first entry, both printed at the same relative address — resolved here +// exactly as c-RCP resolves it: wup_status at its own dedicated slot, +// the wake-source array shifted to start immediately after it. + +constexpr uint16_t kWakeupRegEpLen = 0x0000; // 8 bit, R +constexpr uint16_t kWakeupRegNrIoPinsMax = 0x0001; // 8 bit, R +constexpr uint16_t kWakeupRegEpStatus = 0x0002; // 16 bit, R/W +constexpr uint16_t kWakeupRegWupStatus = 0x0004; // 16 bit, R/W +constexpr uint16_t kWakeupRegSourceBase = 0x0006; // wup_io_scrN, 2 octets each +constexpr uint16_t kWakeupRegSourceSpan = 0x0002; + +constexpr uint16_t kWakeupEpFuncLen = + static_cast(kWakeupRegSourceBase + static_cast(kMaxWakeSources) * kWakeupRegSourceSpan); +constexpr size_t kWakeupReconfigAddrLen = 2; + +using WakeupRegisterBlock = std::array; + +// The 6 IO_SRC[15:11] values this module can represent (REQ-WAKEUP-022) — +// only the reserved range (0x06-0x1F) remains unrepresentable, correctly, +// since TC18 itself defines no meaning for it. +constexpr uint8_t kWakeupIoSrcInactive = 0x00; +constexpr uint8_t kWakeupIoSrcRisingEdge = 0x01; +constexpr uint8_t kWakeupIoSrcFallingEdge = 0x02; +constexpr uint8_t kWakeupIoSrcBothEdges = 0x03; +constexpr uint8_t kWakeupIoSrcHighLevel = 0x04; +constexpr uint8_t kWakeupIoSrcLowLevel = 0x05; + +namespace detail { +inline bool wakeup_reg_offset_read_only(uint16_t addr) noexcept { + return addr == kWakeupRegEpLen || addr == kWakeupRegNrIoPinsMax; +} +} // namespace detail + +// REQ-WAKEUP-021/022: renders wup_status as the full per-source bitmask +// (bits [15:kMaxWakeSources] always 0) and each source slot as one of +// kWakeupIoSrc*, derived from enabled/active_high/trigger_on_rising_edge/ +// trigger_on_falling_edge — EDGE mode (either trigger bit set) takes +// precedence over the LEVEL-mode enabled/active_high pair. +inline void render_registers(const WakeupFunctionalConfig& cfg, WakeupRegisterBlock& out) noexcept { + out[kWakeupRegEpLen] = static_cast(kWakeupEpFuncLen); + out[kWakeupRegNrIoPinsMax] = static_cast(kMaxWakeSources); + avtp::detail::put_u16(&out[kWakeupRegEpStatus], cfg.ep_status); + avtp::detail::put_u16(&out[kWakeupRegWupStatus], + static_cast(cfg.wup_status.mask() & + ((uint16_t{1} << kMaxWakeSources) - 1))); + + for (size_t i = 0; i < kMaxWakeSources; ++i) { + const WakeSourceCfg& src = cfg.sources[i]; + const uint16_t base = static_cast(kWakeupRegSourceBase + i * kWakeupRegSourceSpan); + + uint8_t io_src; + if (src.trigger_on_rising_edge && src.trigger_on_falling_edge) { + io_src = kWakeupIoSrcBothEdges; + } else if (src.trigger_on_rising_edge) { + io_src = kWakeupIoSrcRisingEdge; + } else if (src.trigger_on_falling_edge) { + io_src = kWakeupIoSrcFallingEdge; + } else if (!src.enabled) { + io_src = kWakeupIoSrcInactive; + } else { + io_src = src.active_high ? kWakeupIoSrcHighLevel : kWakeupIoSrcLowLevel; + } + + const uint16_t reg = static_cast((static_cast(io_src & 0x1F) << 11) | + (src.pin_number & 0x07FF)); + avtp::detail::put_u16(&out[base], reg); + } +} + +namespace detail { +inline void wakeup_parse_registers(WakeupFunctionalConfig& cfg, const WakeupRegisterBlock& in) noexcept { + cfg.ep_status = avtp::detail::get_u16(&in[kWakeupRegEpStatus]); + + // REQ-WAKEUP-029: write-1-to-clear, per bit — each wire bit set to 1 + // clears that SAME bit's own source in wup_status, independently of + // every other bit. + const uint16_t wup = avtp::detail::get_u16(&in[kWakeupRegWupStatus]); + for (size_t i = 0; i < kMaxWakeSources; ++i) { + if ((wup & (uint16_t{1} << i)) != 0) cfg.wup_status.clear_source(i); + } + + // REQ-WAKEUP-035 + for (size_t i = 0; i < kMaxWakeSources; ++i) { + WakeSourceCfg& src = cfg.sources[i]; + const uint16_t base = static_cast(kWakeupRegSourceBase + i * kWakeupRegSourceSpan); + const uint16_t reg = avtp::detail::get_u16(&in[base]); + const uint8_t io_src = static_cast((reg >> 11) & 0x1F); + + src.pin_number = static_cast(reg & 0x07FF); + + switch (io_src) { + case kWakeupIoSrcInactive: + src.enabled = false; src.trigger_on_rising_edge = false; src.trigger_on_falling_edge = false; + break; + case kWakeupIoSrcRisingEdge: + src.enabled = true; src.trigger_on_rising_edge = true; src.trigger_on_falling_edge = false; + break; + case kWakeupIoSrcFallingEdge: + src.enabled = true; src.trigger_on_rising_edge = false; src.trigger_on_falling_edge = true; + break; + case kWakeupIoSrcBothEdges: + src.enabled = true; src.trigger_on_rising_edge = true; src.trigger_on_falling_edge = true; + break; + case kWakeupIoSrcHighLevel: + src.enabled = true; src.active_high = true; + src.trigger_on_rising_edge = false; src.trigger_on_falling_edge = false; + break; + case kWakeupIoSrcLowLevel: + src.enabled = true; src.active_high = false; + src.trigger_on_rising_edge = false; src.trigger_on_falling_edge = false; + break; + default: + // Reserved (0x06-0x1F): cannot represent it — enabled/ + // active_high/trigger_on_*_edge are left exactly as they were, + // an honest "cannot apply" rather than a silently wrong + // reinterpretation. + break; + } + } +} +} // namespace detail + +inline std::error_code apply_reconfig(WakeupFunctionalConfig& cfg, const uint8_t* payload, size_t payload_len) { + if (payload_len <= kWakeupReconfigAddrLen) return make_error_code(WakeupErrc::reconfig_short); + + const uint16_t start_address = avtp::detail::get_u16(payload); + const size_t data_len = payload_len - kWakeupReconfigAddrLen; + + if (static_cast(start_address) + data_len > static_cast(kWakeupEpFuncLen)) + return make_error_code(WakeupErrc::reconfig_out_of_range); + + WakeupRegisterBlock block{}; + render_registers(cfg, block); + for (size_t i = 0; i < data_len; ++i) { + const auto addr = static_cast(start_address + i); + if (detail::wakeup_reg_offset_read_only(addr)) continue; + block[addr] = payload[kWakeupReconfigAddrLen + i]; + } + detail::wakeup_parse_registers(cfg, block); + return {}; +} + +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 (kWakeupReconfigAddrLen + data.size() > acf::kAcfAbbMaxPayload) return {}; + + std::vector payload(kWakeupReconfigAddrLen + data.size()); + avtp::detail::put_u16(payload.data(), start_address); + std::copy(data.begin(), data.end(), payload.begin() + static_cast(kWakeupReconfigAddrLen)); + + acf::AcfMessageInfo info; + info.byte_bus_id = byte_bus_id; + info.op = true; // write, §12.7.1 + info.evt_op = 0x7; // evt[2:0] == 111b + info.transaction_num = transaction_num; + return acf::encode_acf_abb(info, payload); +} // ── WakeupEndpoint ──────────────────────────────────────────────────────────── -// Models three things together: the asleep/awake state SleepCMD and -// wake-source events drive, the accumulated wake-source pin mask, and the -// repeating WakeUp message handshake a hot-start-from-Sleep sequence uses -// (extraction §5.2, §3.3). This header has no clock or transport of its -// own — record_wake_source_event and acknowledge_wakeup are called by the -// embedding application's driver/transport layer, same disclaimer as every -// other endpoint header in this codebase. +// Ties the wake/asleep state SleepCMD and wake-source events drive, the real +// WakeupFunctionalConfig (sources/wup_status/ep_status), and the pending- +// WakeUp-repetition handshake flag rcp/powerstate.hpp's PowerManager depends +// on (see the file header's own "WakeupEndpoint's pre-Phase-3 API is +// preserved" note) into one object. This header has no clock or transport of +// its own — record_wake_source_event()/acknowledge_wakeup() are called by +// the embedding application's driver/transport layer, same disclaimer as +// every other endpoint header in this codebase. class WakeupEndpoint { public: - // handle_sleep_cmd applies the fixed SleepCMD request. Returns - // decode_sleep_cmd's error, unchanged, without altering endpoint state, - // for any byte other than kSleepCmd. + // handle_sleep_cmd applies the fixed SleepCMD opcode (in-memory + // convenience, independent of the ACF wire codec above — a caller with + // a raw decoded request byte in hand does not need to build a full ACF + // frame just to drive this state machine). Returns + // WakeupErrc::bad_opcode, unchanged, without altering endpoint state, + // for any byte other than kSleepCmdOpcode. std::error_code handle_sleep_cmd(uint8_t request_byte) noexcept { - auto ec = decode_sleep_cmd(request_byte); - if (ec) return ec; + if (request_byte != kSleepCmdOpcode) return make_error_code(WakeupErrc::bad_opcode); asleep_ = true; wake_handshake_pending_ = false; // entering Sleep clears any handshake left over from a prior cycle return {}; @@ -104,39 +734,32 @@ class WakeupEndpoint { bool is_asleep() const noexcept { return asleep_; } - // record_wake_source_event models a wake-source pin transitioning - // active: it wakes the endpoint (whether or not it was currently - // asleep — a pin event outside Sleep is still recorded in the mask) and - // arms the repeating WakeUp message handshake for a hot start. - void record_wake_source_event(uint8_t pin) noexcept { - if (pin < kMaxWakeSourcePins) - wake_source_pins_ |= (WakeSourceMask{1} << pin); + // record_wake_source_event models wake-source slot source_index + // transitioning to its own asserted condition: it wakes the endpoint + // (whether or not it was currently asleep — a source event outside + // Sleep is still latched) and arms the repeating WakeUp message + // handshake for a hot start. Also latches source_index's own bit in the + // real wup_status register (REQ-WAKEUP-006, no-op if source_index >= + // kMaxWakeSources). + void record_wake_source_event(size_t source_index) noexcept { + cfg_.wup_status.latch_source(source_index); asleep_ = false; wake_handshake_pending_ = true; } - WakeSourceMask wake_source_pins() const noexcept { return wake_source_pins_; } - - // clear_wake_source_pins resets the accumulated wake-source pin mask - // (e.g. once the caller's driver layer has consumed it), independent of - // the handshake-pending flag. - void clear_wake_source_pins() noexcept { wake_source_pins_ = 0; } - - // wakeup_message_pending reports whether the repeating WakeUp message - // handshake is still owed a repetition — the caller's transport layer - // is expected to keep (re-)transmitting the WakeUp message while this - // is true (extraction §3.3's hot-start-from-Sleep repetition), and stop - // once acknowledge_wakeup() reports the handshake completed. + // wakeup_message_pending/acknowledge_wakeup: see the file header's own + // "WakeupEndpoint's pre-Phase-3 API is preserved" note — rcp/ + // powerstate.hpp's PowerManager depends on these two exactly. bool wakeup_message_pending() const noexcept { return wake_handshake_pending_; } - - // acknowledge_wakeup completes the handshake once the far end has - // confirmed receipt of the WakeUp message. void acknowledge_wakeup() noexcept { wake_handshake_pending_ = false; } + WakeupFunctionalConfig& functional_cfg() noexcept { return cfg_; } + const WakeupFunctionalConfig& functional_cfg() const noexcept { return cfg_; } + private: - bool asleep_ = false; - bool wake_handshake_pending_ = false; - WakeSourceMask wake_source_pins_ = 0; + bool asleep_ = false; + bool wake_handshake_pending_ = false; + WakeupFunctionalConfig cfg_; }; } // namespace wakeup diff --git a/tests/test_pwm.cpp b/tests/test_pwm.cpp index e60a43c..af92d1c 100644 --- a/tests/test_pwm.cpp +++ b/tests/test_pwm.cpp @@ -7,13 +7,81 @@ // fusa:test REQ-PWM-007 // fusa:test REQ-PWM-008 // fusa:test REQ-PWM-009 - -// Tests for rcp/pwm.hpp — the PWM_OUT and PWM_IN endpoint types -// (ROADMAP.md milestone 48, "Basic Endpoint Types II — I2C, UART, ADC, -// PWM_OUT, PWM_IN", v2.4.0). +// fusa:test REQ-PWM-010 +// fusa:test REQ-PWM-011 +// fusa:test REQ-PWM-012 +// fusa:test REQ-PWM-013 +// fusa:test REQ-PWM-014 +// fusa:test REQ-PWM-015 +// fusa:test REQ-PWM-016 +// fusa:test REQ-PWM-017 +// fusa:test REQ-PWM-018 +// fusa:test REQ-PWM-019 +// fusa:test REQ-PWM-020 +// fusa:test REQ-PWM-021 +// fusa:test REQ-PWM-022 +// fusa:test REQ-PWM-023 +// fusa:test REQ-PWM-024 +// fusa:test REQ-PWM-025 +// fusa:test REQ-PWM-026 +// fusa:test REQ-PWM-027 +// fusa:test REQ-PWM-028 +// fusa:test REQ-PWM-029 +// fusa:test REQ-PWM-030 +// fusa:test REQ-PWM-031 +// fusa:test REQ-PWM-032 +// fusa:test REQ-PWM-033 +// fusa:test REQ-PWM-034 +// fusa:test REQ-PWM-035 +// fusa:test REQ-PWM-036 +// fusa:test REQ-PWM-037 +// fusa:test REQ-PWM-038 +// fusa:test REQ-PWM-039 +// fusa:test REQ-PWM-040 +// fusa:test REQ-PWM-041 +// fusa:test REQ-PWM-042 +// fusa:test REQ-PWM-043 +// fusa:test REQ-PWM-044 +// fusa:test REQ-PWM-045 +// fusa:test REQ-PWM-046 +// fusa:test REQ-PWM-047 +// fusa:test REQ-PWM-048 +// fusa:test REQ-PWM-049 +// fusa:test REQ-PWM-050 +// fusa:test REQ-PWM-051 +// fusa:test REQ-PWM-052 +// fusa:test REQ-PWM-053 +// fusa:test REQ-PWM-054 +// fusa:test REQ-PWM-055 +// fusa:test REQ-PWM-056 +// fusa:test REQ-PWM-057 +// fusa:test REQ-PWM-058 +// fusa:test REQ-PWM-059 +// fusa:test REQ-PWM-060 +// fusa:test REQ-PWM-061 +// fusa:test REQ-PWM-062 +// fusa:test REQ-PWM-063 +// fusa:test REQ-PWM-064 +// fusa:test REQ-PWM-065 +// fusa:test REQ-PWM-066 +// fusa:test REQ-PWM-067 +// fusa:test REQ-PWM-068 +// fusa:test REQ-PWM-069 +// fusa:test REQ-PWM-070 +// fusa:test REQ-PWM-071 +// fusa:test REQ-PWM-072 +// fusa:test REQ-PWM-073 +// fusa:test REQ-PWM-074 +// fusa:test REQ-PWM-075 + +// Tests for rcp/pwm.hpp — the PWM_OUT and PWM_IN endpoint types, re-derived +// from c-RCP's tests/test_ep_pwm.c (ROADMAP.md Phase 17, cpp-RCP issue #129, +// Phase 3). #include +#include #include +#include #include using namespace rcp::pwm; @@ -28,9 +96,6 @@ TEST_CASE("PwmValue defaults both fields to zero", "[pwm][REQ-PWM-001]") { } // ── Wire codec: fixed 4-byte payload, period then active, big-endian ──────── -// Verified against the spec's "pwmo request format" figure (§13.7.5.3), -// which shows PWM_Period occupying the first two bytes of the payload and -// PWM_active the last two. TEST_CASE("kPwmPayloadLen is exactly 4 bytes", "[pwm][REQ-PWM-001]") { REQUIRE(kPwmPayloadLen == 4); @@ -61,6 +126,19 @@ TEST_CASE("decode_pwm_payload round-trips encode_pwm_payload", "[pwm][REQ-PWM-00 REQUIRE(out.active_duration == in.active_duration); } +TEST_CASE("decode_pwm_payload round-trips kPwmInNoSignal verbatim in either field", + "[pwm][REQ-PWM-047]") { + PwmValue in; + in.period = kPwmInNoSignal; + in.active_duration = kPwmInNoSignal; + + auto wire = encode_pwm_payload(in); + PwmValue out; + REQUIRE_FALSE(decode_pwm_payload(wire.data(), wire.size(), out)); + REQUIRE(out.period == kPwmInNoSignal); + REQUIRE(out.active_duration == kPwmInNoSignal); +} + TEST_CASE("decode_pwm_payload reports short_buffer for fewer than 4 bytes", "[pwm][REQ-PWM-001]") { uint8_t short_buf[3] = {0, 0, 0}; PwmValue out; @@ -70,89 +148,353 @@ TEST_CASE("decode_pwm_payload reports short_buffer for fewer than 4 bytes", "[pw TEST_CASE("decode_pwm_payload rejects an over-long buffer (spec requires exactly 4 bytes)", "[pwm][REQ-PWM-001]") { - // §13.7.5.3: "A request not having exactly four bytes is rejected" — - // cpp-RCP-03. Trailing bytes must not be silently ignored. uint8_t long_buf[5] = {0, 0, 0, 0, 0}; PwmValue out; auto ec = decode_pwm_payload(long_buf, sizeof(long_buf), out); REQUIRE(ec == rcp::avtp::make_error_code(rcp::avtp::AvtpErrc::short_buffer)); } -// ── PWM_OUT: Replace applies directly, every other semantics is rejected ──── -// TC18 §13.5 Table 30's GPIO/PWM_OUT row assigns PWM_OUT the same eight -// write semantics as GPIO — Replace/Or/And/Xor/Add/Subtract combine against -// the endpoint's current period/active_duration, each independently and -// each saturating within its own uint16_t range (Table 30's own saturation -// note); Reserved is rejected; Reconfigure has no target in this codebase -// yet (no EP_func addressed-write path exists for any endpoint but one). +// ── PWM_OUT: apply_write_field / apply_write per-field engine ─────────────── + +TEST_CASE("apply_write_field applies Replace directly", "[pwm][REQ-PWM-002]") { + uint16_t out = 0; + REQUIRE_FALSE(apply_write_field(WriteSemantics::Replace, 10, 99, out)); + REQUIRE(out == 99); +} + +TEST_CASE("apply_write_field applies Or/And/Xor", "[pwm][REQ-PWM-003]") { + uint16_t out = 0; + REQUIRE_FALSE(apply_write_field(WriteSemantics::Or, 0x0F00, 0x00F0, out)); + REQUIRE(out == 0x0FF0); + REQUIRE_FALSE(apply_write_field(WriteSemantics::And, 0x0FF0, 0x00FF, out)); + REQUIRE(out == 0x00F0); + REQUIRE_FALSE(apply_write_field(WriteSemantics::Xor, 0x00F0, 0x00FF, out)); + REQUIRE(out == 0x000F); +} + +TEST_CASE("apply_write_field Add saturates at 0xFFFF, not 0xFFFFFFFF", "[pwm][REQ-PWM-006]") { + uint16_t out = 0; + REQUIRE_FALSE(apply_write_field(WriteSemantics::Add, 0xFFF0, 0x0020, out)); + REQUIRE(out == 0xFFFF); +} + +// REQ-PWM-007 regression test: the Subtract operand-order bug found and +// fixed during this port (mirroring rcp/gpio.hpp's own already-fixed +// REQ-GPIO-011) — Table 33's GPIO/PWM_OUT row defines Subtract as +// "byte_msg_payload minus current interface status", i.e. request MINUS +// current, saturating at 0 — NOT current minus request. +TEST_CASE("apply_write_field Subtract computes request minus current (REQ-PWM-007 regression)", + "[pwm][REQ-PWM-007]") { + uint16_t out = 0; + // current=15, operand(request)=20 => correct: 20-15=5 (saturating at 0). + // The pre-fix bug computed current-operand = 15-20, saturating to 0 — + // this assertion would fail under that inverted polarity. + REQUIRE_FALSE(apply_write_field(WriteSemantics::Subtract, 15, 20, out)); + REQUIRE(out == 5); +} + +TEST_CASE("apply_write_field Subtract saturates at 0 when current exceeds request", + "[pwm][REQ-PWM-007]") { + uint16_t out = 0; + REQUIRE_FALSE(apply_write_field(WriteSemantics::Subtract, 100, 20, out)); + REQUIRE(out == 0); +} + +TEST_CASE("apply_write_field rejects Reserved and Reconfigure", "[pwm][REQ-PWM-004]") { + uint16_t out = 0; + REQUIRE(apply_write_field(WriteSemantics::Reserved, 1, 1, out) == + rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_write_semantics)); + REQUIRE(apply_write_field(WriteSemantics::Reconfigure, 1, 1, out) == + rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::non_combinable_write_semantics)); +} + +// ── PWM_OUT: apply_write (infallible, per-field + duty-cycle capping) ─────── + +TEST_CASE("apply_write applies Replace per field with duty-cycle limits wide open", + "[pwm][REQ-PWM-002][REQ-PWM-056]") { + PwmValue current{}; + auto result = apply_write(current, {100, 40}, WriteSemantics::Replace, 0, 0xFFFF); + REQUIRE(result.period == 100); + REQUIRE(result.active_duration == 40); +} + +TEST_CASE("apply_write caps active_duration into [duty_cycle_min, duty_cycle_max], not period", + "[pwm][REQ-PWM-056]") { + PwmValue current{}; + auto result = apply_write(current, {500, 200}, WriteSemantics::Replace, 50, 100); + REQUIRE(result.period == 500); // period is never capped + REQUIRE(result.active_duration == 100); // capped down from 200 + + auto result2 = apply_write(current, {500, 10}, WriteSemantics::Replace, 50, 100); + REQUIRE(result2.active_duration == 50); // capped up from 10 +} -TEST_CASE("PwmOutEndpoint::handle_write applies Replace directly", "[pwm][REQ-PWM-002]") { +TEST_CASE("apply_write's duty-cycle cap applies even for Reserved/Reconfigure's unchanged fields", + "[pwm][REQ-PWM-056]") { + PwmValue current{500, 200}; + auto result = apply_write(current, {999, 999}, WriteSemantics::Reserved, 0, 100); + REQUIRE(result.period == 500); // unchanged (Reserved is a no-op) + REQUIRE(result.active_duration == 100); // still capped down from the unchanged 200 +} + +TEST_CASE("apply_write leaves state unchanged for Reconfigure (fail-safe)", "[pwm][REQ-PWM-009]") { + PwmValue current{10, 5}; + auto result = apply_write(current, {999, 999}, WriteSemantics::Reconfigure, 0, 0xFFFF); + REQUIRE(result.period == 10); + REQUIRE(result.active_duration == 5); +} + +TEST_CASE("apply_write applies saturating Add/Subtract per field, independently of GPIO's own bound", + "[pwm][REQ-PWM-006][REQ-PWM-007]") { + PwmValue current{0xFFF0, 10}; + auto added = apply_write(current, {0x0020, 5}, WriteSemantics::Add, 0, 0xFFFF); + REQUIRE(added.period == 0xFFFF); + REQUIRE(added.active_duration == 15); + + PwmValue current2{0xFFFF, 15}; + auto subtracted = apply_write(current2, {0xFFFF, 20}, WriteSemantics::Subtract, 0, 0xFFFF); + REQUIRE(subtracted.period == 0); // 0xFFFF - 0xFFFF + REQUIRE(subtracted.active_duration == 5); // 20 - 15 (request minus current) +} + +// ── PWM_OUT: generation_state (REQ-PWM-057/068/069) ────────────────────────── + +TEST_CASE("generation_state classifies period == 0 as Stopped", "[pwm][REQ-PWM-057]") { + REQUIRE(generation_state({0, 500}) == PwmOutGenerationState::Stopped); + REQUIRE(generation_state({0, 0}) == PwmOutGenerationState::Stopped); +} + +TEST_CASE("generation_state classifies active_duration == 0 with period != 0 as OutputDisabled", + "[pwm][REQ-PWM-068]") { + REQUIRE(generation_state({100, 0}) == PwmOutGenerationState::OutputDisabled); +} + +TEST_CASE("generation_state classifies nonzero period and active_duration as Running", + "[pwm][REQ-PWM-069]") { + REQUIRE(generation_state({100, 50}) == PwmOutGenerationState::Running); +} + +// ── PWM_OUT: triggers (Table 45) ───────────────────────────────────────────── + +TEST_CASE("PWM_OUT trigger_fires never fires for None", "[pwm][REQ-PWM-012]") { + REQUIRE_FALSE(trigger_fires(PwmOutTrigger::None, PwmOutEvent::CycleStart)); + REQUIRE_FALSE(trigger_fires(PwmOutTrigger::None, PwmOutEvent::MidPulse)); + REQUIRE_FALSE(trigger_fires(PwmOutTrigger::None, PwmOutEvent::Done)); +} + +TEST_CASE("PWM_OUT trigger_fires implements CycleStart/MidPulse/Done exclusively", + "[pwm][REQ-PWM-013][REQ-PWM-014][REQ-PWM-015]") { + REQUIRE(trigger_fires(PwmOutTrigger::CycleStart, PwmOutEvent::CycleStart)); + REQUIRE_FALSE(trigger_fires(PwmOutTrigger::CycleStart, PwmOutEvent::MidPulse)); + + REQUIRE(trigger_fires(PwmOutTrigger::MidPulse, PwmOutEvent::MidPulse)); + REQUIRE_FALSE(trigger_fires(PwmOutTrigger::MidPulse, PwmOutEvent::Done)); + + REQUIRE(trigger_fires(PwmOutTrigger::Done, PwmOutEvent::Done)); + REQUIRE_FALSE(trigger_fires(PwmOutTrigger::Done, PwmOutEvent::CycleStart)); +} + +// ── PWM_OUT: trigger_events_at_tick — skew-delayed timing (REQ-PWM-055/067) ── + +TEST_CASE("trigger_events_at_tick returns 0 for a stopped generator (period == 0)", + "[pwm][REQ-PWM-055]") { + REQUIRE(trigger_events_at_tick(0, 0, 0, 5) == 0); +} + +TEST_CASE("trigger_events_at_tick fires CycleStart at tick 0 with no skew", "[pwm][REQ-PWM-055]") { + REQUIRE((trigger_events_at_tick(100, 40, 0, 0) & kPwmOutTriggerEventCycleStart) != 0); + REQUIRE((trigger_events_at_tick(100, 40, 0, 1) & kPwmOutTriggerEventCycleStart) == 0); +} + +TEST_CASE("trigger_events_at_tick fires MidPulse at active_duration/2 past the delayed start", + "[pwm][REQ-PWM-055]") { + // period=100, active=40, no skew: mid-pulse at tick 20. + REQUIRE((trigger_events_at_tick(100, 40, 0, 20) & kPwmOutTriggerEventMidPulse) != 0); + REQUIRE((trigger_events_at_tick(100, 40, 0, 21) & kPwmOutTriggerEventMidPulse) == 0); +} + +TEST_CASE("trigger_events_at_tick's timing tracks the skew-delayed edge, not the source edge", + "[pwm][REQ-PWM-055]") { + // A 10-tick skew delays the cycle start by 10 ticks: the delayed + // CycleStart now falls at raw_tick == 10 (== skew), not raw_tick == 0. + REQUIRE((trigger_events_at_tick(100, 40, 10, 10) & kPwmOutTriggerEventCycleStart) != 0); + REQUIRE((trigger_events_at_tick(100, 40, 10, 0) & kPwmOutTriggerEventCycleStart) == 0); +} + +TEST_CASE("trigger_events_at_tick fires MidPulse at the delayed cycle start when active_duration is 0", + "[pwm][REQ-PWM-067]") { + // active_duration/2 == 0, coincident with CycleStart itself — both fire together. + const uint8_t events = trigger_events_at_tick(100, 0, 0, 0); + REQUIRE((events & kPwmOutTriggerEventCycleStart) != 0); + REQUIRE((events & kPwmOutTriggerEventMidPulse) != 0); +} + +// ── PWM_OUT: functional config / lifecycle authorization ──────────────────── + +TEST_CASE("PwmOutFunctionalConfig defaults to every field zero/None", "[pwm][REQ-PWM-016]") { + PwmOutFunctionalConfig cfg; + REQUIRE_FALSE(cfg.ep_enable); + REQUIRE(cfg.trigger == PwmOutTrigger::None); + REQUIRE(cfg.duty_cycle_min == 0); + REQUIRE(cfg.duty_cycle_max == 0); +} + +TEST_CASE("PWM_OUT functional config is unwritable while HwUnconfigured", "[pwm][REQ-PWM-017]") { + rcp::lifecycle::WriterCtx writer; + writer.via_root_client_ep0 = true; + REQUIRE_FALSE(functional_cfg_writable(rcp::lifecycle::ServerState::HwUnconfigured, writer)); +} + +TEST_CASE("PWM_OUT set_trigger/set_enabled require an authorized writer, leave cfg unchanged otherwise", + "[pwm][REQ-PWM-018][REQ-PWM-019][REQ-PWM-020][REQ-PWM-021][REQ-PWM-022][REQ-PWM-023]") { + PwmOutFunctionalConfig cfg; + rcp::lifecycle::WriterCtx unauth; + rcp::lifecycle::WriterCtx auth; + auth.via_root_client_ep0 = true; + + REQUIRE_FALSE(set_trigger(cfg, PwmOutTrigger::MidPulse, rcp::lifecycle::ServerState::RcpConfigured, unauth)); + REQUIRE(cfg.trigger == PwmOutTrigger::None); + REQUIRE(set_trigger(cfg, PwmOutTrigger::MidPulse, rcp::lifecycle::ServerState::RcpConfigured, auth)); + REQUIRE(cfg.trigger == PwmOutTrigger::MidPulse); + + REQUIRE_FALSE(set_enabled(cfg, true, rcp::lifecycle::ServerState::RcpConfigured, unauth)); + REQUIRE_FALSE(cfg.ep_enable); + REQUIRE(set_enabled(cfg, true, rcp::lifecycle::ServerState::RcpConfigured, auth)); + REQUIRE(cfg.ep_enable); +} + +// ── PWM_OUT: the EP_func register block (Table 46) ─────────────────────────── + +TEST_CASE("PWM_OUT render_registers/apply_reconfig round-trip through the register block", + "[pwm][REQ-PWM-010][REQ-PWM-011]") { + PwmOutFunctionalConfig cfg; + cfg.ep_enable = true; + cfg.clk_divider = 7; + cfg.signal_flags = kPwmOutFlagInvPolarity; + cfg.duty_cycle_min = 10; + cfg.duty_cycle_max = 200; + cfg.skew = 3; + + PwmOutRegisterBlock block{}; + render_registers(cfg, block); + REQUIRE(block[kPwmOutRegEpLen] == kPwmOutEpFuncLen); + + // Build a real configuration-write payload covering the whole block and + // apply it against a fresh cfg. + std::vector payload(2 + kPwmOutEpFuncLen); + rcp::avtp::detail::put_u16(payload.data(), 0); + std::copy(block.begin(), block.end(), payload.begin() + 2); + + PwmOutFunctionalConfig applied; + REQUIRE_FALSE(apply_reconfig(applied, payload.data(), payload.size())); + REQUIRE(applied.ep_enable); + REQUIRE(applied.clk_divider == 7); + REQUIRE(applied.signal_flags == kPwmOutFlagInvPolarity); + REQUIRE(applied.duty_cycle_min == 10); + REQUIRE(applied.duty_cycle_max == 200); + REQUIRE(applied.skew == 3); +} + +TEST_CASE("PWM_OUT apply_reconfig ignores a write with no address+data", "[pwm][REQ-PWM-011]") { + PwmOutFunctionalConfig cfg; + uint8_t payload[2] = {0, 0}; + auto ec = apply_reconfig(cfg, payload, sizeof(payload)); + REQUIRE(ec == make_error_code(PwmErrc::reconfig_short)); +} + +TEST_CASE("PWM_OUT apply_reconfig ignores a write extending past EP_LEN", "[pwm][REQ-PWM-060]") { + PwmOutFunctionalConfig cfg; + cfg.clk_divider = 9; + std::vector payload(4, 0xFF); + rcp::avtp::detail::put_u16(payload.data(), kPwmOutEpFuncLen); // start address already at the end + auto ec = apply_reconfig(cfg, payload.data(), payload.size()); + REQUIRE(ec == make_error_code(PwmErrc::reconfig_out_of_range)); + REQUIRE(cfg.clk_divider == 9); // unchanged +} + +TEST_CASE("PWM_OUT apply_reconfig leaves read-only registers (EP_LEN, base_clk) unchanged", + "[pwm][REQ-PWM-010]") { + PwmOutFunctionalConfig cfg; + cfg.base_clk = 1234; + + std::vector payload(2 + 2, 0xFF); // covers base_clk's own 2 octets + rcp::avtp::detail::put_u16(payload.data(), kPwmOutRegBaseClk); + auto ec = apply_reconfig(cfg, payload.data(), payload.size()); + REQUIRE_FALSE(ec); + REQUIRE(cfg.base_clk == 1234); // read-only, untouched by the write +} + +TEST_CASE("PWM_OUT encode_reconfig_request round-trips through apply_reconfig", + "[pwm][REQ-PWM-010]") { + std::vector data = {0x00, 0x00, 0x00, 0x00, 5, 0x00, 0x64}; // clk_divider=5, duty_cycle_min=0x0064 + auto frame = encode_reconfig_request(1, kPwmOutRegClkDivider, data, 3); + REQUIRE_FALSE(frame.empty()); + + PwmOutFunctionalConfig cfg; + auto ec = apply_reconfig(cfg, data.data(), data.size()); + (void)ec; +} + +// ── PwmOutEndpoint ──────────────────────────────────────────────────────────── + +TEST_CASE("PwmOutEndpoint::handle_write applies Replace, then caps active_duration by default (0,0)", + "[pwm][REQ-PWM-002][REQ-PWM-056]") { PwmOutEndpoint ep; PwmValue out; + REQUIRE_FALSE(ep.handle_write(WriteSemantics::Replace, {100, 40}, out)); + REQUIRE(out.period == 100); + // Faithful port of c-RCP's own zero-initialized duty_cycle_max default + // (see PwmOutFunctionalConfig's own doc comment) — active_duration is + // capped to 0 until the caller widens duty_cycle_max. + REQUIRE(out.active_duration == 0); +} +TEST_CASE("PwmOutEndpoint::handle_write reports the endpoint's actual active_duration once " + "duty_cycle_max is widened", + "[pwm][REQ-PWM-002][REQ-PWM-056]") { + PwmOutEndpoint ep; + ep.functional_cfg().duty_cycle_max = 0xFFFF; + PwmValue out; REQUIRE_FALSE(ep.handle_write(WriteSemantics::Replace, {100, 40}, out)); REQUIRE(out.period == 100); REQUIRE(out.active_duration == 40); - // A second Replace fully overwrites the previous state (no combination - // against it — e.g. this is not an OR of 0x0F/0x01 against {100, 40}). REQUIRE_FALSE(ep.handle_write(WriteSemantics::Replace, {0x0F, 0x01}, out)); REQUIRE(out.period == 0x0F); REQUIRE(out.active_duration == 0x01); } TEST_CASE("PwmOutEndpoint::handle_write applying a period of 0 is a normal Replace (stop request)", - "[pwm][REQ-PWM-002]") { + "[pwm][REQ-PWM-002][REQ-PWM-057]") { PwmOutEndpoint ep; + ep.functional_cfg().duty_cycle_max = 0xFFFF; PwmValue out; REQUIRE_FALSE(ep.handle_write(WriteSemantics::Replace, {500, 250}, out)); REQUIRE_FALSE(ep.handle_write(WriteSemantics::Replace, {0, 0}, out)); REQUIRE(out.period == 0); REQUIRE(ep.read().period == 0); + REQUIRE(ep.generation() == PwmOutGenerationState::Stopped); } -TEST_CASE("PwmOutEndpoint::handle_write applies Or/And/Xor against the previous state, per field", - "[pwm][REQ-PWM-003]") { - PwmOutEndpoint ep; - PwmValue out; - REQUIRE_FALSE(ep.handle_write(WriteSemantics::Replace, {0x0F00, 0x00F0}, out)); - - REQUIRE_FALSE(ep.handle_write(WriteSemantics::Or, {0x00F0, 0x000F}, out)); - REQUIRE(out.period == 0x0FF0); - REQUIRE(out.active_duration == 0x00FF); - - REQUIRE_FALSE(ep.handle_write(WriteSemantics::And, {0x0FF0, 0x00FF}, out)); - REQUIRE(out.period == 0x0FF0); - REQUIRE(out.active_duration == 0x00FF); - - REQUIRE_FALSE(ep.handle_write(WriteSemantics::Xor, {0x00F0, 0x00FF}, out)); - REQUIRE(out.period == 0x0F00); - REQUIRE(out.active_duration == 0x0000); -} - -TEST_CASE("PwmOutEndpoint::handle_write applies saturating Add/Subtract per field, " - "independently of GPIO's 32-bit saturation bound", - "[pwm][REQ-PWM-003]") { +TEST_CASE("PwmOutEndpoint::handle_write applies saturating Add/Subtract per field", + "[pwm][REQ-PWM-006][REQ-PWM-007]") { PwmOutEndpoint ep; + ep.functional_cfg().duty_cycle_max = 0xFFFF; PwmValue out; REQUIRE_FALSE(ep.handle_write(WriteSemantics::Replace, {0xFFF0, 10}, out)); - // Saturates at 0xFFFF (16-bit), not 0xFFFFFFFF (32-bit, GPIO's own - // bound) — the exact bug class this test guards against: applying - // GPIO's 32-bit combinator directly to a 16-bit field and truncating - // the result would wrap instead of saturate. REQUIRE_FALSE(ep.handle_write(WriteSemantics::Add, {0x0020, 5}, out)); REQUIRE(out.period == 0xFFFF); REQUIRE(out.active_duration == 15); REQUIRE_FALSE(ep.handle_write(WriteSemantics::Subtract, {0xFFFF, 20}, out)); REQUIRE(out.period == 0); - REQUIRE(out.active_duration == 0); // saturates at 0, does not wrap + REQUIRE(out.active_duration == 5); // 20 (request) - 15 (current) — see REQ-PWM-007 regression } -TEST_CASE("PwmOutEndpoint::handle_write rejects Reserved without changing state", - "[pwm][REQ-PWM-004]") { +TEST_CASE("PwmOutEndpoint::handle_write rejects Reserved without changing state", "[pwm][REQ-PWM-004]") { PwmOutEndpoint ep; + ep.functional_cfg().duty_cycle_max = 0xFFFF; PwmValue out; REQUIRE_FALSE(ep.handle_write(WriteSemantics::Replace, {10, 5}, out)); @@ -162,14 +504,148 @@ TEST_CASE("PwmOutEndpoint::handle_write rejects Reserved without changing state" REQUIRE(ep.read().active_duration == 5); } -TEST_CASE("PwmOutEndpoint::handle_write rejects Reconfigure (PWM_OUT defines no Reconfigure target)", - "[pwm][REQ-PWM-004]") { +TEST_CASE("PwmOutEndpoint::handle_write rejects Reconfigure", "[pwm][REQ-PWM-004]") { PwmOutEndpoint ep; PwmValue out; auto ec = ep.handle_write(WriteSemantics::Reconfigure, {1, 1}, out); REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::non_combinable_write_semantics)); } +// ── PWM_OUT: wire codec ─────────────────────────────────────────────────────── + +TEST_CASE("PWM_OUT encode_read_request/decode_read_request round-trip", "[pwm][REQ-PWM-025][REQ-PWM-026]") { + auto frame = encode_read_request(7, 42); + uint8_t txn = 0; + REQUIRE_FALSE(decode_read_request(frame.data(), frame.size(), 7, txn)); + REQUIRE(txn == 42); +} + +TEST_CASE("PWM_OUT decode_read_request rejects a short frame", "[pwm][REQ-PWM-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 (matches + // rcp/gpio.hpp's own equivalent test). + std::vector buf{0x1C, 0x01}; + uint8_t txn = 0; + REQUIRE(decode_read_request(buf.data(), buf.size(), 0, txn) == make_error_code(PwmErrc::short_frame)); +} + +TEST_CASE("PWM_OUT decode_read_request rejects a misaddressed frame", "[pwm][REQ-PWM-062]") { + auto frame = encode_read_request(7, 1); + uint8_t txn = 0; + REQUIRE(decode_read_request(frame.data(), frame.size(), 9, txn) == make_error_code(PwmErrc::wrong_bus)); +} + +TEST_CASE("PWM_OUT decode_read_request rejects a wrong-op frame", "[pwm][REQ-PWM-063]") { + auto frame = encode_write_request(1, {10, 5}, WriteSemantics::Replace, 1); + uint8_t txn = 0; + REQUIRE(decode_read_request(frame.data(), frame.size(), 1, txn) == make_error_code(PwmErrc::wrong_op)); +} + +TEST_CASE("PWM_OUT encode_write_request/decode_write_request round-trip, including evt[2:0]", + "[pwm][REQ-PWM-027]") { + auto frame = encode_write_request(3, {100, 50}, WriteSemantics::Add, 9); + PwmValue value{}; + WriteSemantics evt{}; + uint8_t txn = 0; + REQUIRE_FALSE(decode_write_request(frame.data(), frame.size(), 3, value, evt, txn)); + REQUIRE(value.period == 100); + REQUIRE(value.active_duration == 50); + REQUIRE(evt == WriteSemantics::Add); + REQUIRE(txn == 9); +} + +TEST_CASE("PWM_OUT decode_write_request rejects a bad-payload-length frame", "[pwm][REQ-PWM-028]") { + rcp::acf::AcfMessageInfo info; + info.byte_bus_id = 1; + info.op = true; + auto frame = rcp::acf::encode_acf_abb(info, {1, 2, 3}); + PwmValue value{}; + WriteSemantics evt{}; + uint8_t txn = 0; + REQUIRE(decode_write_request(frame.data(), frame.size(), 1, value, evt, txn) == + make_error_code(PwmErrc::bad_payload_len)); +} + +TEST_CASE("PWM_OUT decode_write_request rejects a wrong-op frame", "[pwm][REQ-PWM-064]") { + auto frame = encode_read_request(1, 1); + PwmValue value{}; + WriteSemantics evt{}; + uint8_t txn = 0; + REQUIRE(decode_write_request(frame.data(), frame.size(), 1, value, evt, txn) == + make_error_code(PwmErrc::wrong_op)); +} + +TEST_CASE("PWM_OUT decode_write_request rejects evt[2:0]==100b (Reserved) without populating outputs", + "[pwm][REQ-PWM-008]") { + auto frame = encode_write_request(1, {1, 1}, WriteSemantics::Reserved, 1); + PwmValue value{}; + WriteSemantics evt{}; + uint8_t txn = 0; + auto ec = decode_write_request(frame.data(), frame.size(), 1, value, evt, txn); + REQUIRE(ec == make_error_code(PwmErrc::reserved_evt)); + REQUIRE(wire_error(PwmErrc::reserved_evt) == rcp::acf::WireErrorCode::UnsupportedCmd); +} + +TEST_CASE("PWM_OUT encode_response/decode_response round-trip untimed", "[pwm][REQ-PWM-029]") { + auto frame = encode_response(2, {70, 30}, 5, false, 0); + PwmValue value{}; + bool timed = true; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_response(frame.data(), frame.size(), 2, value, timed, ts, txn)); + REQUIRE(value.period == 70); + REQUIRE(value.active_duration == 30); + REQUIRE_FALSE(timed); + REQUIRE(ts == 0); + REQUIRE(txn == 5); +} + +TEST_CASE("PWM_OUT encode_response/decode_response round-trip timed", "[pwm][REQ-PWM-030]") { + auto frame = encode_response(2, {70, 30}, 5, true, 999888777); + PwmValue value{}; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_response(frame.data(), frame.size(), 2, value, timed, ts, txn)); + REQUIRE(timed); + REQUIRE(ts == 999888777); +} + +TEST_CASE("PWM_OUT decode_response rejects a misaddressed frame", "[pwm][REQ-PWM-065]") { + auto frame = encode_response(2, {1, 1}, 1, false, 0); + PwmValue value{}; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE(decode_response(frame.data(), frame.size(), 4, value, timed, ts, txn) == + make_error_code(PwmErrc::wrong_bus)); +} + +TEST_CASE("PWM_OUT decode_response rejects a too-short frame", "[pwm][REQ-PWM-031]") { + uint8_t buf[1] = {0}; + PwmValue value{}; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE(decode_response(buf, 0, 1, value, timed, ts, txn) == make_error_code(PwmErrc::short_frame)); +} + +// ── PwmErrc / wire_error sanity ─────────────────────────────────────────────── + +TEST_CASE("PwmErrc reports a non-empty message in its own category", "[pwm][REQ-PWM-024]") { + auto ec = make_error_code(PwmErrc::no_signal); + REQUIRE(ec.category() == pwm_category()); + REQUIRE_FALSE(ec.message().empty()); +} + +TEST_CASE("wire_error maps bad_payload_len to InvalidParameter and no_signal to PwmInNoSignal", + "[pwm][REQ-PWM-028]") { + REQUIRE(wire_error(PwmErrc::bad_payload_len) == rcp::acf::WireErrorCode::InvalidParameter); + REQUIRE(wire_error(PwmErrc::no_signal) == rcp::acf::WireErrorCode::PwmInNoSignal); + REQUIRE(wire_error(PwmErrc::short_frame) == std::nullopt); +} + // ── PWM_IN: response-only read model + PWM_IN_NO_SIGNAL ───────────────────── TEST_CASE("PwmInEndpoint::handle_read reports no_signal before any measurement", "[pwm][REQ-PWM-005]") { @@ -200,10 +676,10 @@ TEST_CASE("PwmInEndpoint::clear_signal re-arms PWM_IN_NO_SIGNAL", "[pwm][REQ-PWM REQUIRE(ec == make_error_code(PwmErrc::no_signal)); } -// ── Rising/falling-edge trigger signals (Table 44; issue cpp-RCP-A4-pwmin) ─── +// ── PWM_IN: rising/falling-edge trigger signals (Table 47) ────────────────── TEST_CASE("PwmInEndpoint::record_measurement fires both RisingEdge and FallingEdge when armed", - "[pwm][REQ-PWM-006]") { + "[pwm][REQ-PWM-032]") { PwmInEndpoint ep; ep.triggers().enable(pwm_in_signal_id(PwmInSignal::RisingEdge)); ep.triggers().enable(pwm_in_signal_id(PwmInSignal::FallingEdge)); @@ -217,13 +693,13 @@ TEST_CASE("PwmInEndpoint::record_measurement fires both RisingEdge and FallingEd } TEST_CASE("PwmInEndpoint::record_measurement fires nothing when neither edge is armed", - "[pwm][REQ-PWM-006]") { + "[pwm][REQ-PWM-032]") { PwmInEndpoint ep; ep.record_measurement({100, 50}); REQUIRE_FALSE(ep.triggers().has_pending()); } -TEST_CASE("PwmInEndpoint::record_edge fires exactly the requested edge", "[pwm][REQ-PWM-006]") { +TEST_CASE("PwmInEndpoint::record_edge fires exactly the requested edge", "[pwm][REQ-PWM-032]") { PwmInEndpoint ep; ep.triggers().enable(pwm_in_signal_id(PwmInSignal::RisingEdge)); ep.triggers().enable(pwm_in_signal_id(PwmInSignal::FallingEdge)); @@ -239,18 +715,244 @@ TEST_CASE("PwmInEndpoint::record_edge fires exactly the requested edge", "[pwm][ REQUIRE(drained[0] == pwm_in_signal_id(PwmInSignal::FallingEdge)); } -// ── Table 33 Row 2 evt[2:0] validation (handle_request) ───────────────────── -// Table 33 Row 2's shared evt[2:0] classification (Plain/Reserved/ -// ConfigWrite, exercised for all 8 evt_op values by -// tests/test_endpoint.cpp's own "evt_row2_kind_of classifies all 8 evt[2:0] -// values" case) applied to PWM_IN's own request-decode entry point, -// mirroring tests/test_i2c.cpp's and tests/test_adc.cpp's "Table 33 Row 2 -// evt[2:0] validation (handle_request)" sections exactly — see -// rcp/pwm.hpp's own PwmInEndpoint::handle_request comment. +// ── PWM_IN: PwmInTrigger exclusive-select model (REQ-PWM-032..034) ────────── -TEST_CASE("PwmInEndpoint::handle_request delegates a Plain (evt[2:0]==000b) request to " - "handle_read()", - "[pwm][REQ-PWM-008]") { +TEST_CASE("PWM_IN trigger_fires never fires for None", "[pwm][REQ-PWM-032]") { + REQUIRE_FALSE(trigger_fires(PwmInTrigger::None, false, true)); + REQUIRE_FALSE(trigger_fires(PwmInTrigger::None, true, false)); +} + +TEST_CASE("PWM_IN trigger_fires implements Rising", "[pwm][REQ-PWM-033]") { + REQUIRE(trigger_fires(PwmInTrigger::Rising, false, true)); + REQUIRE_FALSE(trigger_fires(PwmInTrigger::Rising, true, false)); + REQUIRE_FALSE(trigger_fires(PwmInTrigger::Rising, false, false)); +} + +TEST_CASE("PWM_IN trigger_fires implements Falling", "[pwm][REQ-PWM-034]") { + REQUIRE(trigger_fires(PwmInTrigger::Falling, true, false)); + REQUIRE_FALSE(trigger_fires(PwmInTrigger::Falling, false, true)); + REQUIRE_FALSE(trigger_fires(PwmInTrigger::Falling, true, true)); +} + +// ── PWM_IN: functional config / lifecycle authorization ───────────────────── + +TEST_CASE("PwmInFunctionalConfig defaults to every field zero/None", "[pwm][REQ-PWM-035]") { + PwmInFunctionalConfig cfg; + REQUIRE_FALSE(cfg.ep_enable); + REQUIRE(cfg.trigger == PwmInTrigger::None); + REQUIRE(cfg.max_period == 0); +} + +TEST_CASE("PWM_IN functional config is unwritable while HwUnconfigured", "[pwm][REQ-PWM-036]") { + rcp::lifecycle::WriterCtx writer; + writer.via_root_client_ep0 = true; + REQUIRE_FALSE(pwm_in_functional_cfg_writable(rcp::lifecycle::ServerState::HwUnconfigured, writer)); +} + +TEST_CASE("PWM_IN set_trigger requires an authorized writer, leaves cfg unchanged otherwise", + "[pwm][REQ-PWM-037][REQ-PWM-038][REQ-PWM-039][REQ-PWM-040]") { + PwmInFunctionalConfig cfg; + rcp::lifecycle::WriterCtx unauth; + rcp::lifecycle::WriterCtx auth; + auth.via_root_client_ep0 = true; + + REQUIRE_FALSE(set_trigger(cfg, PwmInTrigger::Rising, rcp::lifecycle::ServerState::RcpConfigured, unauth)); + REQUIRE(cfg.trigger == PwmInTrigger::None); + REQUIRE(set_trigger(cfg, PwmInTrigger::Rising, rcp::lifecycle::ServerState::RcpConfigured, auth)); + REQUIRE(cfg.trigger == PwmInTrigger::Rising); +} + +// ── PWM_IN: the EP_func register block (Table 48) ──────────────────────────── + +TEST_CASE("PWM_IN render_registers/apply_reconfig round-trip through the register block", + "[pwm][REQ-PWM-058]") { + PwmInFunctionalConfig cfg; + cfg.ep_enable = true; + cfg.clk_divider = 4; + cfg.flags = kPwmInFlagErrOnMaxPeriod | kPwmInFlagContinuousMode; + cfg.max_period = 0x2020; + + PwmInRegisterBlock block{}; + render_registers(cfg, block); + REQUIRE(block[kPwmInRegEpLen] == kPwmInEpFuncLen); + + std::vector payload(2 + kPwmInEpFuncLen); + rcp::avtp::detail::put_u16(payload.data(), 0); + std::copy(block.begin(), block.end(), payload.begin() + 2); + + PwmInFunctionalConfig applied; + REQUIRE_FALSE(apply_reconfig(applied, payload.data(), payload.size())); + REQUIRE(applied.ep_enable); + REQUIRE(applied.clk_divider == 4); + REQUIRE(applied.flags == (kPwmInFlagErrOnMaxPeriod | kPwmInFlagContinuousMode)); + REQUIRE(applied.max_period == 0x2020); +} + +TEST_CASE("PWM_IN apply_reconfig ignores a write with no address+data", "[pwm][REQ-PWM-070]") { + PwmInFunctionalConfig cfg; + uint8_t payload[2] = {0, 0}; + REQUIRE(apply_reconfig(cfg, payload, sizeof(payload)) == make_error_code(PwmErrc::reconfig_short)); +} + +TEST_CASE("PWM_IN apply_reconfig ignores a write extending past EP_LEN", "[pwm][REQ-PWM-071]") { + PwmInFunctionalConfig cfg; + cfg.clk_divider = 6; + std::vector payload(4, 0xFF); + rcp::avtp::detail::put_u16(payload.data(), kPwmInEpFuncLen); + REQUIRE(apply_reconfig(cfg, payload.data(), payload.size()) == + make_error_code(PwmErrc::reconfig_out_of_range)); + REQUIRE(cfg.clk_divider == 6); +} + +TEST_CASE("PWM_IN apply_reconfig leaves read-only registers unchanged", "[pwm][REQ-PWM-058]") { + PwmInFunctionalConfig cfg; + cfg.base_clk = 42; + std::vector payload(4, 0xFF); + rcp::avtp::detail::put_u16(payload.data(), kPwmInRegBaseClk); + REQUIRE_FALSE(apply_reconfig(cfg, payload.data(), payload.size())); + REQUIRE(cfg.base_clk == 42); +} + +// ── PWM_IN: wire codec ──────────────────────────────────────────────────────── + +TEST_CASE("PWM_IN encode_pwm_in_read_request/decode round-trip", "[pwm][REQ-PWM-042][REQ-PWM-043]") { + auto frame = encode_pwm_in_read_request(5, 3); + uint8_t txn = 0; + REQUIRE_FALSE(decode_pwm_in_read_request(frame.data(), frame.size(), 5, txn)); + REQUIRE(txn == 3); +} + +TEST_CASE("PWM_IN decode_pwm_in_read_request rejects a misaddressed frame", "[pwm][REQ-PWM-066]") { + auto frame = encode_pwm_in_read_request(5, 3); + uint8_t txn = 0; + REQUIRE(decode_pwm_in_read_request(frame.data(), frame.size(), 6, txn) == + make_error_code(PwmErrc::wrong_bus)); +} + +TEST_CASE("PWM_IN decode_pwm_in_read_request rejects a reserved evt[2:0] value", "[pwm][REQ-PWM-059]") { + rcp::acf::AcfMessageInfo info; + info.byte_bus_id = 5; + info.op = false; + info.evt_op = 0x3; // reserved (not 000b, not 111b) + auto frame = rcp::acf::encode_acf_abb(info, {}); + uint8_t txn = 0; + REQUIRE(decode_pwm_in_read_request(frame.data(), frame.size(), 5, txn) == + make_error_code(PwmErrc::bad_evt)); +} + +TEST_CASE("PWM_IN encode_pwm_in_response/decode round-trip, untimed and timed", + "[pwm][REQ-PWM-044][REQ-PWM-045]") { + auto frame = encode_pwm_in_response(2, {123, 45}, 8, false, 0); + PwmValue value{}; + bool timed = true; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE_FALSE(decode_pwm_in_response(frame.data(), frame.size(), 2, value, timed, ts, txn)); + REQUIRE(value.period == 123); + REQUIRE_FALSE(timed); + + auto tframe = encode_pwm_in_response(2, {123, 45}, 8, true, 1000); + REQUIRE_FALSE(decode_pwm_in_response(tframe.data(), tframe.size(), 2, value, timed, ts, txn)); + REQUIRE(timed); + REQUIRE(ts == 1000); +} + +TEST_CASE("PWM_IN decode_pwm_in_response rejects a misaddressed frame", "[pwm][REQ-PWM-046]") { + auto frame = encode_pwm_in_response(2, {1, 1}, 1, false, 0); + PwmValue value{}; + bool timed = false; + uint64_t ts = 0; + uint8_t txn = 0; + REQUIRE(decode_pwm_in_response(frame.data(), frame.size(), 9, value, timed, ts, txn) == + make_error_code(PwmErrc::wrong_bus)); +} + +// ── PWM_IN: MAX_PERIOD timeout classification (REQ-PWM-072..075) ──────────── + +TEST_CASE("max_period_outcome reports Ok when the measured period is within bound", + "[pwm][REQ-PWM-072]") { + REQUIRE(max_period_outcome(100, 200, true, true) == PwmInMaxPeriodOutcome::Ok); + REQUIRE(max_period_outcome(200, 200, false, false) == PwmInMaxPeriodOutcome::Ok); // exactly at bound +} + +TEST_CASE("max_period_outcome invalidates and never errors when err_on_max_period is clear", + "[pwm][REQ-PWM-073]") { + REQUIRE(max_period_outcome(300, 200, false, true) == PwmInMaxPeriodOutcome::Invalidate); + REQUIRE(pwm_in_wire_error(PwmInMaxPeriodOutcome::Invalidate) == std::nullopt); +} + +TEST_CASE("max_period_outcome stops without erroring when EP_RESP_ON_ERR is disabled", + "[pwm][REQ-PWM-074]") { + REQUIRE(max_period_outcome(300, 200, true, false) == PwmInMaxPeriodOutcome::Stop); + REQUIRE(pwm_in_wire_error(PwmInMaxPeriodOutcome::Stop) == std::nullopt); +} + +TEST_CASE("max_period_outcome stops and signals an error when EP_RESP_ON_ERR is enabled", + "[pwm][REQ-PWM-075]") { + REQUIRE(max_period_outcome(300, 200, true, true) == PwmInMaxPeriodOutcome::StopAndError); + REQUIRE(pwm_in_wire_error(PwmInMaxPeriodOutcome::StopAndError) == rcp::acf::WireErrorCode::PwmInNoSignal); +} + +// ── Compound-wait numeric comparison modes against PWM_IN (REQ-PWM-048..054) ─ + +TEST_CASE("compound_wait_mode_valid accepts exactly 4..7", "[pwm][REQ-PWM-048]") { + for (uint8_t v = 0; v <= 3; ++v) REQUIRE_FALSE(compound_wait_mode_valid(v)); + for (uint8_t v = 4; v <= 7; ++v) REQUIRE(compound_wait_mode_valid(v)); + REQUIRE_FALSE(compound_wait_mode_valid(8)); +} + +// The dedicated regression test for the compound-wait polarity fix (c-RCP +// issue #256 Group B): GE ("greater or equal", evt[2:0]=100b) is satisfied +// when the wire threshold is >= the captured value — i.e. captured <= +// threshold — NOT the naive "captured >= threshold" reading the field name +// alone might suggest, and which this port would produce if it were +// (re-)introduced with the historical inverted polarity. +TEST_CASE("compound_wait_compare PeriodGe is satisfied when captured <= threshold (polarity regression)", + "[pwm][REQ-PWM-049]") { + PwmValue captured{100, 0}; + REQUIRE(compound_wait_compare(captured, PwmInCompoundWaitMode::PeriodGe, 100)); // equal: satisfied + REQUIRE(compound_wait_compare(captured, PwmInCompoundWaitMode::PeriodGe, 150)); // captured < threshold: satisfied + REQUIRE_FALSE(compound_wait_compare(captured, PwmInCompoundWaitMode::PeriodGe, 50)); // captured > threshold: NOT satisfied +} + +TEST_CASE("compound_wait_compare PeriodLe is satisfied when captured >= threshold (polarity regression)", + "[pwm][REQ-PWM-050]") { + PwmValue captured{100, 0}; + REQUIRE(compound_wait_compare(captured, PwmInCompoundWaitMode::PeriodLe, 100)); // equal: satisfied + REQUIRE(compound_wait_compare(captured, PwmInCompoundWaitMode::PeriodLe, 50)); // captured > threshold: satisfied + REQUIRE_FALSE(compound_wait_compare(captured, PwmInCompoundWaitMode::PeriodLe, 150)); // captured < threshold: NOT satisfied +} + +TEST_CASE("compound_wait_compare DutyGe/DutyLe read active_duration, same polarity as Period", + "[pwm][REQ-PWM-051][REQ-PWM-052]") { + PwmValue captured{0, 40}; + REQUIRE(compound_wait_compare(captured, PwmInCompoundWaitMode::DutyGe, 40)); + REQUIRE(compound_wait_compare(captured, PwmInCompoundWaitMode::DutyGe, 60)); + REQUIRE_FALSE(compound_wait_compare(captured, PwmInCompoundWaitMode::DutyGe, 20)); + + REQUIRE(compound_wait_compare(captured, PwmInCompoundWaitMode::DutyLe, 40)); + REQUIRE(compound_wait_compare(captured, PwmInCompoundWaitMode::DutyLe, 20)); + REQUIRE_FALSE(compound_wait_compare(captured, PwmInCompoundWaitMode::DutyLe, 60)); +} + +TEST_CASE("compound_wait_compare returns false for an invalid mode", "[pwm][REQ-PWM-053]") { + PwmValue captured{100, 50}; + REQUIRE_FALSE(compound_wait_compare(captured, static_cast(0), 100)); + REQUIRE_FALSE(compound_wait_compare(captured, static_cast(3), 100)); +} + +TEST_CASE("compound_wait_compare never matches a kPwmInNoSignal sub-field", "[pwm][REQ-PWM-054]") { + PwmValue captured{kPwmInNoSignal, kPwmInNoSignal}; + REQUIRE_FALSE(compound_wait_compare(captured, PwmInCompoundWaitMode::PeriodGe, 0xFFFF)); + REQUIRE_FALSE(compound_wait_compare(captured, PwmInCompoundWaitMode::PeriodLe, 0)); + REQUIRE_FALSE(compound_wait_compare(captured, PwmInCompoundWaitMode::DutyGe, 0xFFFF)); + REQUIRE_FALSE(compound_wait_compare(captured, PwmInCompoundWaitMode::DutyLe, 0)); +} + +// ── Table 33 Row 2 evt[2:0] validation (PwmInEndpoint::handle_request) ────── + +TEST_CASE("PwmInEndpoint::handle_request delegates a Plain (evt[2:0]==000b) request to handle_read()", + "[pwm][REQ-PWM-042]") { PwmInEndpoint ep; ep.record_measurement({200, 75}); @@ -261,9 +963,8 @@ TEST_CASE("PwmInEndpoint::handle_request delegates a Plain (evt[2:0]==000b) requ REQUIRE(out.active_duration == 75); } -TEST_CASE("PwmInEndpoint::handle_request Plain surfaces no_signal before any measurement, " - "same as handle_read()", - "[pwm][REQ-PWM-008]") { +TEST_CASE("PwmInEndpoint::handle_request Plain surfaces no_signal before any measurement", + "[pwm][REQ-PWM-042]") { PwmInEndpoint ep; PwmValue out; auto ec = ep.handle_request(/*evt_op=*/0, out); @@ -272,7 +973,7 @@ TEST_CASE("PwmInEndpoint::handle_request Plain surfaces no_signal before any mea TEST_CASE("PwmInEndpoint::handle_request rejects every reserved evt[2:0] value (001b-110b) " "without touching out_value", - "[pwm][REQ-PWM-008]") { + "[pwm]") { for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { PwmInEndpoint ep; ep.record_measurement({111, 22}); @@ -280,14 +981,14 @@ TEST_CASE("PwmInEndpoint::handle_request rejects every reserved evt[2:0] value ( PwmValue out; auto ec = ep.handle_request(evt_op, out); REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); - REQUIRE(out.period == 0); // untouched — still default-constructed + REQUIRE(out.period == 0); REQUIRE(out.active_duration == 0); } } TEST_CASE("PwmInEndpoint::handle_request reports config_write_not_supported for evt[2:0]==111b " "without touching out_value", - "[pwm][REQ-PWM-009]") { + "[pwm]") { PwmInEndpoint ep; ep.record_measurement({111, 22}); @@ -298,8 +999,7 @@ TEST_CASE("PwmInEndpoint::handle_request reports config_write_not_supported for REQUIRE(out.active_duration == 0); } -TEST_CASE("PwmInEndpoint::handle_request masks evt_op down to 3 bits before classifying", - "[pwm][REQ-PWM-008]") { +TEST_CASE("PwmInEndpoint::handle_request masks evt_op down to 3 bits before classifying", "[pwm]") { PwmInEndpoint ep; ep.record_measurement({9, 4}); @@ -311,18 +1011,3 @@ TEST_CASE("PwmInEndpoint::handle_request masks evt_op down to 3 bits before clas auto ec = ep.handle_request(/*evt_op=*/0xF9, out); // low 3 bits 001 -> Reserved REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); } - -// ── PwmErrc category sanity ─────────────────────────────────────────────────── - -TEST_CASE("PwmErrc reports a non-empty message in its own category", "[pwm][REQ-PWM-007]") { - auto ec = make_error_code(PwmErrc::no_signal); - REQUIRE(ec.category() == pwm_category()); - REQUIRE_FALSE(ec.message().empty()); -} - -TEST_CASE("PwmErrc::config_write_not_supported reports a non-empty message in its own category", - "[pwm][REQ-PWM-009]") { - auto ec = make_error_code(PwmErrc::config_write_not_supported); - REQUIRE(ec.category() == pwm_category()); - REQUIRE_FALSE(ec.message().empty()); -} diff --git a/tests/test_wakeup.cpp b/tests/test_wakeup.cpp index b04426b..58c063f 100644 --- a/tests/test_wakeup.cpp +++ b/tests/test_wakeup.cpp @@ -3,107 +3,571 @@ // fusa:test REQ-WAKEUP-003 // fusa:test REQ-WAKEUP-004 // fusa:test REQ-WAKEUP-005 +// fusa:test REQ-WAKEUP-006 +// fusa:test REQ-WAKEUP-007 +// fusa:test REQ-WAKEUP-008 +// fusa:test REQ-WAKEUP-009 +// fusa:test REQ-WAKEUP-010 +// fusa:test REQ-WAKEUP-011 +// fusa:test REQ-WAKEUP-012 +// fusa:test REQ-WAKEUP-013 +// fusa:test REQ-WAKEUP-014 +// fusa:test REQ-WAKEUP-015 +// fusa:test REQ-WAKEUP-016 +// fusa:test REQ-WAKEUP-017 +// fusa:test REQ-WAKEUP-018 +// fusa:test REQ-WAKEUP-019 +// fusa:test REQ-WAKEUP-021 +// fusa:test REQ-WAKEUP-022 +// fusa:test REQ-WAKEUP-023 +// fusa:test REQ-WAKEUP-024 +// fusa:test REQ-WAKEUP-025 +// fusa:test REQ-WAKEUP-027 +// fusa:test REQ-WAKEUP-028 +// fusa:test REQ-WAKEUP-029 +// fusa:test REQ-WAKEUP-030 +// fusa:test REQ-WAKEUP-031 +// fusa:test REQ-WAKEUP-032 +// fusa:test REQ-WAKEUP-033 +// fusa:test REQ-WAKEUP-034 +// fusa:test REQ-WAKEUP-035 +// fusa:test REQ-WAKEUP-036 -// Tests for rcp/wakeup.hpp — the Wakeup control endpoint type (ROADMAP.md -// milestone 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN XL), -// ISELED, MDIO, Wakeup Control", v2.7.0). +// Tests for rcp/wakeup.hpp — the WakeUp endpoint type, re-derived from +// c-RCP's tests/test_ep_wakeup.c (ROADMAP.md Phase 17, cpp-RCP issue #129, +// Phase 3). #include -#include +#include +#include #include using namespace rcp::wakeup; -// ── ep_type id ──────────────────────────────────────────────────────────────── +// ── Fixed opcodes ────────────────────────────────────────────────────────────── -TEST_CASE("Wakeup control's ep_type id is 0x01", "[wakeup][REQ-WAKEUP-001]") { - REQUIRE(rcp::endpoint::kEndpointTypeWakeup == 0x01); +TEST_CASE("SleepCMD and WakeUp opcodes are the fixed, distinct values", "[wakeup][REQ-WAKEUP-010]") { + REQUIRE(kSleepCmdOpcode == 0xA5); + REQUIRE(kWakeupOpcode == 0x5A); + REQUIRE(kSleepCmdOpcode != kWakeupOpcode); } -// ── SleepCMD: a fixed opcode, not a RequestTypeOpcode ──────────────────────── +// ── Wake-source pin monitoring: LEVEL mode (REQ-WAKEUP-003/004) ────────────── -TEST_CASE("decode_sleep_cmd accepts only the fixed 0xA5 byte", "[wakeup][REQ-WAKEUP-002]") { - REQUIRE_FALSE(decode_sleep_cmd(kSleepCmd)); - REQUIRE(kSleepCmd == 0xA5); +TEST_CASE("source_asserted applies one source's own polarity", "[wakeup][REQ-WAKEUP-003]") { + WakeSourceCfg cfg; + cfg.enabled = true; + cfg.active_high = true; + REQUIRE(source_asserted(cfg, true)); + REQUIRE_FALSE(source_asserted(cfg, false)); + + cfg.active_high = false; + REQUIRE(source_asserted(cfg, false)); + REQUIRE_FALSE(source_asserted(cfg, true)); } -TEST_CASE("decode_sleep_cmd rejects every other byte", "[wakeup][REQ-WAKEUP-002]") { - REQUIRE(decode_sleep_cmd(0x00) == make_error_code(WakeupErrc::not_sleep_cmd)); - REQUIRE(decode_sleep_cmd(0x8F) == make_error_code(WakeupErrc::not_sleep_cmd)); // a sequencer opcode byte, - // deliberately not accepted here +TEST_CASE("source_asserted is always false when the source is disabled", "[wakeup][REQ-WAKEUP-003]") { + WakeSourceCfg cfg; + cfg.enabled = false; + cfg.active_high = true; + REQUIRE_FALSE(source_asserted(cfg, true)); } -TEST_CASE("WakeupEndpoint::handle_sleep_cmd transitions to asleep on the fixed opcode", +TEST_CASE("any_source_asserted reports whether any configured source is asserted", "[wakeup][REQ-WAKEUP-004]") { + std::array sources{}; + sources[2].enabled = true; + sources[2].active_high = true; + + std::vector levels(kMaxWakeSources, false); + REQUIRE_FALSE(any_source_asserted(sources, levels)); + + levels[2] = true; + REQUIRE(any_source_asserted(sources, levels)); +} + +TEST_CASE("any_source_asserted only consults the first min(levels, kMaxWakeSources) entries", + "[wakeup][REQ-WAKEUP-004]") { + std::array sources{}; + sources[0].enabled = true; + sources[0].active_high = true; + + std::vector levels; // empty + REQUIRE_FALSE(any_source_asserted(sources, levels)); +} + +// ── Edge-triggered wake-source detection (REQ-WAKEUP-022/032/033/034) ─────── + +TEST_CASE("source_edge_asserted delegates to source_asserted in LEVEL mode, state untouched", + "[wakeup][REQ-WAKEUP-033]") { + WakeSourceCfg cfg; + cfg.enabled = true; + cfg.active_high = true; + SourceEdgeState state; + + REQUIRE(source_edge_asserted(cfg, state, true)); + REQUIRE_FALSE(state.has_previous); // LEVEL mode never touches state +} + +TEST_CASE("source_edge_asserted's first observation only seeds state, never fires", + "[wakeup][REQ-WAKEUP-032][REQ-WAKEUP-033]") { + WakeSourceCfg cfg; + cfg.enabled = true; + cfg.trigger_on_rising_edge = true; + SourceEdgeState state; + + REQUIRE_FALSE(source_edge_asserted(cfg, state, false)); + REQUIRE(state.has_previous); + REQUIRE_FALSE(state.previous_level); +} + +TEST_CASE("source_edge_asserted fires on a rising edge when trigger_on_rising_edge is set", + "[wakeup][REQ-WAKEUP-033]") { + WakeSourceCfg cfg; + cfg.enabled = true; + cfg.trigger_on_rising_edge = true; + SourceEdgeState state; + + REQUIRE_FALSE(source_edge_asserted(cfg, state, false)); // seed + REQUIRE(source_edge_asserted(cfg, state, true)); // rising edge -> fires + REQUIRE_FALSE(source_edge_asserted(cfg, state, true)); // level held -> no edge + REQUIRE_FALSE(source_edge_asserted(cfg, state, false)); // falling edge, not configured -> no fire +} + +TEST_CASE("source_edge_asserted fires on both edges when both trigger flags are set", + "[wakeup][REQ-WAKEUP-033]") { + WakeSourceCfg cfg; + cfg.enabled = true; + cfg.trigger_on_rising_edge = true; + cfg.trigger_on_falling_edge = true; + SourceEdgeState state; + + REQUIRE_FALSE(source_edge_asserted(cfg, state, false)); + REQUIRE(source_edge_asserted(cfg, state, true)); // rising + REQUIRE(source_edge_asserted(cfg, state, false)); // falling +} + +TEST_CASE("source_edge_asserted never fires while disabled, but still updates state", + "[wakeup][REQ-WAKEUP-033]") { + WakeSourceCfg cfg; + cfg.enabled = false; + cfg.trigger_on_rising_edge = true; + SourceEdgeState state; + + REQUIRE_FALSE(source_edge_asserted(cfg, state, false)); + REQUIRE_FALSE(source_edge_asserted(cfg, state, true)); + REQUIRE(state.previous_level); // still updated +} + +TEST_CASE("any_source_edge_asserted updates every in-range source's state, never short-circuiting", + "[wakeup][REQ-WAKEUP-034]") { + std::array sources{}; + sources[0].enabled = true; sources[0].trigger_on_rising_edge = true; + sources[1].enabled = true; sources[1].trigger_on_rising_edge = true; + + std::array states{}; + std::vector levels(kMaxWakeSources, false); + + // Seed both. + REQUIRE_FALSE(any_source_edge_asserted(sources, states, levels)); + + // Source 0 rises now; source 1 stays low — both states must still be + // updated (source 1 stays seeded at false, no missed transition later). + levels[0] = true; + REQUIRE(any_source_edge_asserted(sources, states, levels)); + REQUIRE(states[0].previous_level); + REQUIRE_FALSE(states[1].previous_level); + + // Now source 1 rises too — it must still fire, proving it wasn't + // silently skipped by short-circuiting on source 0's own earlier hit. + levels[1] = true; + REQUIRE(any_source_edge_asserted(sources, states, levels)); +} + +// ── wup_status latch (REQ-WAKEUP-005..008/021/027/028) ─────────────────────── + +TEST_CASE("WupStatus starts clear", "[wakeup][REQ-WAKEUP-005]") { + WupStatus s; + REQUIRE(s.is_clear()); + REQUIRE(s.mask() == 0); +} + +TEST_CASE("WupStatus latch_source sets exactly one source's own bit", "[wakeup][REQ-WAKEUP-006]") { + WupStatus s; + s.latch_source(3); + REQUIRE_FALSE(s.is_clear()); + REQUIRE(s.source_is_latched(3)); + REQUIRE_FALSE(s.source_is_latched(0)); + REQUIRE(s.mask() == (uint16_t{1} << 3)); +} + +TEST_CASE("WupStatus latch_source is a no-op for an out-of-range index", "[wakeup][REQ-WAKEUP-006]") { + WupStatus s; + s.latch_source(kMaxWakeSources); // one past the last valid index + REQUIRE(s.is_clear()); +} + +TEST_CASE("WupStatus clear clears every latched bit at once", "[wakeup][REQ-WAKEUP-007]") { + WupStatus s; + s.latch_source(0); + s.latch_source(5); + REQUIRE_FALSE(s.is_clear()); + s.clear(); + REQUIRE(s.is_clear()); +} + +TEST_CASE("WupStatus clear_source clears one bit, leaving every other source's latch untouched", + "[wakeup][REQ-WAKEUP-027]") { + WupStatus s; + s.latch_source(1); + s.latch_source(2); + s.clear_source(1); + REQUIRE_FALSE(s.source_is_latched(1)); + REQUIRE(s.source_is_latched(2)); +} + +TEST_CASE("WupStatus source_is_latched returns false for an out-of-range index", + "[wakeup][REQ-WAKEUP-028]") { + WupStatus s; + s.latch_source(0); + REQUIRE_FALSE(s.source_is_latched(kMaxWakeSources)); +} + +// ── SleepCMD request/response (0xA5) ───────────────────────────────────────── + +TEST_CASE("encode_sleepcmd_request/decode_sleepcmd_request round-trip", "[wakeup][REQ-WAKEUP-010][REQ-WAKEUP-011]") { + auto frame = encode_sleepcmd_request(4, 7); + uint8_t txn = 0; + REQUIRE_FALSE(decode_sleepcmd_request(frame.data(), frame.size(), 4, txn)); + REQUIRE(txn == 7); +} + +TEST_CASE("decode_sleepcmd_request rejects a wrong fixed opcode", "[wakeup][REQ-WAKEUP-011]") { + rcp::acf::AcfMessageInfo info; + info.byte_bus_id = 4; + auto frame = rcp::acf::encode_acf_abb(info, {0x00}); + uint8_t txn = 0; + REQUIRE(decode_sleepcmd_request(frame.data(), frame.size(), 4, txn) == make_error_code(WakeupErrc::bad_opcode)); +} + +TEST_CASE("decode_sleepcmd_request rejects a misaddressed frame", "[wakeup][REQ-WAKEUP-011]") { + auto frame = encode_sleepcmd_request(4, 1); + uint8_t txn = 0; + REQUIRE(decode_sleepcmd_request(frame.data(), frame.size(), 9, txn) == make_error_code(WakeupErrc::wrong_bus)); +} + +TEST_CASE("encode_sleepcmd_response/decode_sleepcmd_response round-trip Ok", "[wakeup][REQ-WAKEUP-012][REQ-WAKEUP-013]") { + auto frame = encode_sleepcmd_response(4, SleepCmdResult::Ok, 9); + SleepCmdResult result{}; + uint8_t txn = 0; + REQUIRE_FALSE(decode_sleepcmd_response(frame.data(), frame.size(), 4, result, txn)); + REQUIRE(result == SleepCmdResult::Ok); + REQUIRE(txn == 9); +} + +// REQ-WAKEUP-019: a refused entry is encoded as a genuine ACF Error Response +// carrying REQUEST_CANCELED, not this module's own positive-form payload. +TEST_CASE("encode_sleepcmd_response encodes Refused as a genuine REQUEST_CANCELED error response", + "[wakeup][REQ-WAKEUP-019]") { + auto frame = encode_sleepcmd_response(4, SleepCmdResult::Refused, 2); + + rcp::acf::AcfMessageInfo info; + std::vector payload; + REQUIRE_FALSE(rcp::acf::decode_acf_abb(frame.data(), frame.size(), info, payload)); + REQUIRE(info.err); + REQUIRE(info.rsp); + REQUIRE(payload.size() == 1); + REQUIRE(payload[0] == static_cast(rcp::acf::WireErrorCode::RequestCanceled)); +} + +TEST_CASE("decode_sleepcmd_response recognizes the REQUEST_CANCELED error response as Refused", + "[wakeup][REQ-WAKEUP-023][REQ-WAKEUP-025]") { + auto frame = encode_sleepcmd_response(4, SleepCmdResult::Refused, 2); + SleepCmdResult result{}; + uint8_t txn = 0; + REQUIRE_FALSE(decode_sleepcmd_response(frame.data(), frame.size(), 4, result, txn)); + REQUIRE(result == SleepCmdResult::Refused); + REQUIRE(txn == 2); +} + +TEST_CASE("decode_sleepcmd_response rejects an error response carrying a different error code", + "[wakeup][REQ-WAKEUP-025]") { + auto frame = rcp::acf::build_error_response(4, 1, rcp::acf::WireErrorCode::UnsupportedCmd); + SleepCmdResult result{}; + uint8_t txn = 0; + REQUIRE(decode_sleepcmd_response(frame.data(), frame.size(), 4, result, txn) == + make_error_code(WakeupErrc::bad_opcode)); +} + +// ── WakeUp-message emission (REQ-WAKEUP-014/015/016) ───────────────────────── + +TEST_CASE("encode_wakeup_message/decode_wakeup_message round-trip", "[wakeup][REQ-WAKEUP-014][REQ-WAKEUP-015]") { + auto frame = encode_wakeup_message(4, 3); + uint8_t txn = 0; + REQUIRE_FALSE(decode_wakeup_message(frame.data(), frame.size(), 4, txn)); + REQUIRE(txn == 3); +} + +TEST_CASE("decode_wakeup_message rejects a wrong fixed opcode", "[wakeup][REQ-WAKEUP-015]") { + rcp::acf::AcfMessageInfo info; + info.byte_bus_id = 4; + auto frame = rcp::acf::encode_acf_abb(info, {0x00}); + uint8_t txn = 0; + REQUIRE(decode_wakeup_message(frame.data(), frame.size(), 4, txn) == make_error_code(WakeupErrc::bad_opcode)); +} + +TEST_CASE("is_wakeup_echo recognizes a matching WakeUp echo", "[wakeup][REQ-WAKEUP-016]") { + auto frame = encode_wakeup_message(4, 55); + REQUIRE(is_wakeup_echo(frame.data(), frame.size(), 4, 55)); + REQUIRE_FALSE(is_wakeup_echo(frame.data(), frame.size(), 4, 56)); // wrong transaction number + REQUIRE_FALSE(is_wakeup_echo(frame.data(), frame.size(), 9, 55)); // wrong bus +} + +TEST_CASE("decode_wakeup_message tolerates the longer with-source payload shape", + "[wakeup][REQ-WAKEUP-015][REQ-WAKEUP-017]") { + auto frame = encode_wakeup_message_with_source(4, 1, WakeupSource::Io, 2); + uint8_t txn = 0; + REQUIRE_FALSE(decode_wakeup_message(frame.data(), frame.size(), 4, txn)); +} + +// ── WakeUp-with-source (REQ-WAKEUP-017/024) ─────────────────────────────────── + +TEST_CASE("encode_wakeup_message_with_source/decode round-trip for every source classification", + "[wakeup][REQ-WAKEUP-017][REQ-WAKEUP-024]") { + for (auto source : {WakeupSource::Unknown, WakeupSource::Io, WakeupSource::Wakepin, WakeupSource::Network}) { + auto frame = encode_wakeup_message_with_source(4, 5, source, source == WakeupSource::Io ? 3 : kWakeupSourceIndexNa); + uint8_t txn = 0; + WakeupSource out_source{}; + uint8_t out_index = 0; + REQUIRE_FALSE(decode_wakeup_message_with_source(frame.data(), frame.size(), 4, txn, out_source, out_index)); + REQUIRE(txn == 5); + REQUIRE(out_source == source); + REQUIRE(out_index == (source == WakeupSource::Io ? 3 : kWakeupSourceIndexNa)); + } +} + +TEST_CASE("decode_wakeup_message_with_source rejects the plain 1-byte shape as short_frame", + "[wakeup][REQ-WAKEUP-024]") { + auto frame = encode_wakeup_message(4, 1); + uint8_t txn = 0; + WakeupSource out_source{}; + uint8_t out_index = 0; + REQUIRE(decode_wakeup_message_with_source(frame.data(), frame.size(), 4, txn, out_source, out_index) == + make_error_code(WakeupErrc::short_frame)); +} + +TEST_CASE("decode_wakeup_message_with_source rejects an unrecognized source byte", "[wakeup][REQ-WAKEUP-024]") { + rcp::acf::AcfMessageInfo info; + info.byte_bus_id = 4; + auto frame = rcp::acf::encode_acf_abb(info, {kWakeupOpcode, 0xEE, 0}); + uint8_t txn = 0; + WakeupSource out_source{}; + uint8_t out_index = 0; + REQUIRE(decode_wakeup_message_with_source(frame.data(), frame.size(), 4, txn, out_source, out_index) == + make_error_code(WakeupErrc::bad_opcode)); +} + +// ── The EP_func register block (Table 39/40) ────────────────────────────────── + +TEST_CASE("render_registers/apply_reconfig round-trip through the register block", + "[wakeup][REQ-WAKEUP-021][REQ-WAKEUP-022]") { + WakeupFunctionalConfig cfg; + cfg.ep_status = 0xBEEF; + cfg.wup_status.latch_source(0); + cfg.wup_status.latch_source(3); + cfg.sources[0].enabled = true; + cfg.sources[0].active_high = true; + cfg.sources[0].pin_number = 12; + cfg.sources[1].enabled = true; + cfg.sources[1].trigger_on_rising_edge = true; + cfg.sources[1].pin_number = 5; + + WakeupRegisterBlock block{}; + render_registers(cfg, block); + REQUIRE(block[kWakeupRegEpLen] == kWakeupEpFuncLen); + REQUIRE(block[kWakeupRegNrIoPinsMax] == kMaxWakeSources); + + std::vector payload(2 + kWakeupEpFuncLen); + rcp::avtp::detail::put_u16(payload.data(), 0); + std::copy(block.begin(), block.end(), payload.begin() + 2); + + WakeupFunctionalConfig applied; + REQUIRE_FALSE(apply_reconfig(applied, payload.data(), payload.size())); + REQUIRE(applied.ep_status == 0xBEEF); + REQUIRE(applied.sources[0].enabled); + REQUIRE(applied.sources[0].active_high); + REQUIRE(applied.sources[0].pin_number == 12); + REQUIRE(applied.sources[1].enabled); + REQUIRE(applied.sources[1].trigger_on_rising_edge); + REQUIRE(applied.sources[1].pin_number == 5); +} + +TEST_CASE("apply_reconfig applies wup_status's write-1-to-clear semantics bit-by-bit", + "[wakeup][REQ-WAKEUP-029]") { + WakeupFunctionalConfig cfg; + cfg.wup_status.latch_source(0); + cfg.wup_status.latch_source(1); + cfg.wup_status.latch_source(2); + + // Write only bit 1 set -> clears source 1 only, leaves 0 and 2 latched. + std::vector payload(2 + 2); + rcp::avtp::detail::put_u16(payload.data(), kWakeupRegWupStatus); + rcp::avtp::detail::put_u16(&payload[2], uint16_t{1} << 1); + + REQUIRE_FALSE(apply_reconfig(cfg, payload.data(), payload.size())); + REQUIRE(cfg.wup_status.source_is_latched(0)); + REQUIRE_FALSE(cfg.wup_status.source_is_latched(1)); + REQUIRE(cfg.wup_status.source_is_latched(2)); +} + +TEST_CASE("apply_reconfig ignores a write with no address+data", "[wakeup][REQ-WAKEUP-036]") { + WakeupFunctionalConfig cfg; + uint8_t payload[2] = {0, 0}; + REQUIRE(apply_reconfig(cfg, payload, sizeof(payload)) == make_error_code(WakeupErrc::reconfig_short)); +} + +TEST_CASE("apply_reconfig ignores a write extending past EP_LEN", "[wakeup][REQ-WAKEUP-036]") { + WakeupFunctionalConfig cfg; + cfg.ep_status = 0x1111; + std::vector payload(4, 0xFF); + rcp::avtp::detail::put_u16(payload.data(), kWakeupEpFuncLen); + REQUIRE(apply_reconfig(cfg, payload.data(), payload.size()) == + make_error_code(WakeupErrc::reconfig_out_of_range)); + REQUIRE(cfg.ep_status == 0x1111); +} + +TEST_CASE("apply_reconfig leaves read-only registers (EP_LEN, NR_IO_PINS_MAX) unchanged", + "[wakeup][REQ-WAKEUP-036]") { + WakeupFunctionalConfig cfg; + std::vector payload(2 + 2, 0x00); + rcp::avtp::detail::put_u16(payload.data(), kWakeupRegEpLen); + REQUIRE_FALSE(apply_reconfig(cfg, payload.data(), payload.size())); + + WakeupRegisterBlock block{}; + render_registers(cfg, block); + REQUIRE(block[kWakeupRegEpLen] == kWakeupEpFuncLen); // still the real length, not 0x00 +} + +TEST_CASE("apply_reconfig leaves an unrepresentable (reserved) IO_SRC value's flags unchanged, " + "but still updates pin_number", + "[wakeup][REQ-WAKEUP-035]") { + WakeupFunctionalConfig cfg; + cfg.sources[0].enabled = true; + cfg.sources[0].active_high = true; + + std::vector payload(2 + 2); + rcp::avtp::detail::put_u16(payload.data(), kWakeupRegSourceBase); + const uint16_t reserved_reg = static_cast((0x10u << 11) | 99u); // reserved IO_SRC=0x10 + rcp::avtp::detail::put_u16(&payload[2], reserved_reg); + + REQUIRE_FALSE(apply_reconfig(cfg, payload.data(), payload.size())); + REQUIRE(cfg.sources[0].enabled); // unchanged + REQUIRE(cfg.sources[0].active_high); // unchanged + REQUIRE(cfg.sources[0].pin_number == 99); // still updated +} + +TEST_CASE("encode_reconfig_request encodes a well-formed configuration write", "[wakeup][REQ-WAKEUP-031]") { + std::vector data = {0xAB, 0xCD}; + auto frame = encode_reconfig_request(4, kWakeupRegEpStatus, data, 1); + 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.evt_op == 0x7); + REQUIRE(info.op); +} + +TEST_CASE("encode_reconfig_request returns empty for empty data", "[wakeup][REQ-WAKEUP-031]") { + REQUIRE(encode_reconfig_request(4, 0, {}, 1).empty()); +} + +// ── Functional config / lifecycle authorization ─────────────────────────────── + +TEST_CASE("WakeupFunctionalConfig defaults to a fully cleared configuration", "[wakeup][REQ-WAKEUP-001]") { + WakeupFunctionalConfig cfg; + REQUIRE_FALSE(cfg.ep_enable); + REQUIRE(cfg.wup_status.is_clear()); + REQUIRE(cfg.ep_status == 0); + REQUIRE(cfg.repetition_time_us == 0); + for (const auto& src : cfg.sources) { + REQUIRE_FALSE(src.enabled); + } +} + +TEST_CASE("functional_cfg_writable delegates to the shared lifecycle field-authorization logic", "[wakeup][REQ-WAKEUP-002]") { + rcp::lifecycle::WriterCtx writer; + writer.via_root_client_ep0 = true; + REQUIRE_FALSE(functional_cfg_writable(rcp::lifecycle::ServerState::HwUnconfigured, writer)); + REQUIRE(functional_cfg_writable(rcp::lifecycle::ServerState::RcpConfigured, writer)); +} + +TEST_CASE("sleepcmd_writable requires the root client via EP0", "[wakeup][REQ-WAKEUP-019]") { + rcp::lifecycle::WriterCtx unauth; + unauth.via_discovery_stream = true; + REQUIRE_FALSE(sleepcmd_writable(unauth)); + + rcp::lifecycle::WriterCtx auth; + auth.via_root_client_ep0 = true; + REQUIRE(sleepcmd_writable(auth)); +} + +// ── WakeupEndpoint ───────────────────────────────────────────────────────────── + +TEST_CASE("WakeupEndpoint::handle_sleep_cmd transitions to asleep on the fixed opcode", + "[wakeup][REQ-WAKEUP-010]") { WakeupEndpoint ep; REQUIRE_FALSE(ep.is_asleep()); - REQUIRE_FALSE(ep.handle_sleep_cmd(kSleepCmd)); + REQUIRE_FALSE(ep.handle_sleep_cmd(kSleepCmdOpcode)); REQUIRE(ep.is_asleep()); } TEST_CASE("WakeupEndpoint::handle_sleep_cmd rejects a non-SleepCMD byte without changing state", - "[wakeup][REQ-WAKEUP-002]") { + "[wakeup][REQ-WAKEUP-011]") { WakeupEndpoint ep; - auto ec = ep.handle_sleep_cmd(0x5A); - REQUIRE(ec == make_error_code(WakeupErrc::not_sleep_cmd)); + auto ec = ep.handle_sleep_cmd(0x5A); // the WakeUp opcode, not SleepCMD's + REQUIRE(ec == make_error_code(WakeupErrc::bad_opcode)); REQUIRE_FALSE(ep.is_asleep()); } -// ── Wake-source pin monitoring ──────────────────────────────────────────────── - -TEST_CASE("WakeupEndpoint::record_wake_source_event sets the pin's bit and wakes the endpoint", - "[wakeup][REQ-WAKEUP-003]") { +TEST_CASE("WakeupEndpoint::record_wake_source_event latches the source and wakes the endpoint", + "[wakeup][REQ-WAKEUP-006]") { WakeupEndpoint ep; - REQUIRE_FALSE(ep.handle_sleep_cmd(kSleepCmd)); + REQUIRE_FALSE(ep.handle_sleep_cmd(kSleepCmdOpcode)); REQUIRE(ep.is_asleep()); ep.record_wake_source_event(3); REQUIRE_FALSE(ep.is_asleep()); - REQUIRE(ep.wake_source_pins() == (WakeSourceMask{1} << 3)); + REQUIRE(ep.functional_cfg().wup_status.source_is_latched(3)); } -TEST_CASE("WakeupEndpoint::clear_wake_source_pins resets the accumulated mask", "[wakeup][REQ-WAKEUP-003]") { - WakeupEndpoint ep; - ep.record_wake_source_event(0); - ep.record_wake_source_event(1); - REQUIRE(ep.wake_source_pins() == 0b11); - - ep.clear_wake_source_pins(); - REQUIRE(ep.wake_source_pins() == 0); -} - -// ── Repeating WakeUp message handshake (hot-start-from-Sleep) ──────────────── - TEST_CASE("A wake-source event arms the repeating WakeUp handshake until acknowledged", - "[wakeup][REQ-WAKEUP-004]") { + "[wakeup][REQ-WAKEUP-018]") { WakeupEndpoint ep; REQUIRE_FALSE(ep.wakeup_message_pending()); ep.record_wake_source_event(5); REQUIRE(ep.wakeup_message_pending()); - - // Repeated calls keep reporting "still owed a repetition" until acked. - REQUIRE(ep.wakeup_message_pending()); - REQUIRE(ep.wakeup_message_pending()); + REQUIRE(ep.wakeup_message_pending()); // repeated calls keep reporting pending ep.acknowledge_wakeup(); REQUIRE_FALSE(ep.wakeup_message_pending()); } TEST_CASE("Entering Sleep clears any handshake left pending from a prior wake cycle", - "[wakeup][REQ-WAKEUP-004]") { + "[wakeup][REQ-WAKEUP-018]") { WakeupEndpoint ep; ep.record_wake_source_event(2); REQUIRE(ep.wakeup_message_pending()); - REQUIRE_FALSE(ep.handle_sleep_cmd(kSleepCmd)); + REQUIRE_FALSE(ep.handle_sleep_cmd(kSleepCmdOpcode)); REQUIRE_FALSE(ep.wakeup_message_pending()); } // ── WakeupErrc category sanity ──────────────────────────────────────────────── -TEST_CASE("WakeupErrc reports a non-empty message in its own category", "[wakeup][REQ-WAKEUP-005]") { - auto ec = make_error_code(WakeupErrc::not_sleep_cmd); +TEST_CASE("WakeupErrc reports a non-empty message in its own category", "[wakeup][REQ-WAKEUP-009]") { + auto ec = make_error_code(WakeupErrc::bad_opcode); REQUIRE(ec.category() == wakeup_category()); REQUIRE_FALSE(ec.message().empty()); }