diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 698e95c..306860b 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -3503,6 +3503,214 @@ "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" + }, + { + "id": "REQ-LINEP-001", + "title": "LIN's ep_type id is 0x06", + "text": "rcp::endpoint::kEndpointTypeLin shall equal 0x06.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-LINEP-002", + "title": "LIN is a raw-byte-pusher with no frame-level concepts", + "text": "rcp::lin::LinEndpoint::transfer shall record the exact byte sequences passed as out_bytes and in_bytes, retrievable via last_sent()/last_received(), performing no checksum selection, PID generation, schedule-table lookup, or any other LIN frame-structure interpretation of either sequence, including for an empty byte sequence.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-LINEP-003", + "title": "LIN TransferComplete and NoResponse trigger signals", + "text": "rcp::lin::LinEndpoint::transfer shall notify only TransferComplete for a responded transfer (responded=true) and shall notify only NoResponse and return LinErrc::no_response for an unresponded transfer (responded=false).", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-LINEP-004", + "title": "LinErrc is a distinct error category with a non-empty message", + "text": "rcp::lin::make_error_code(LinErrc::no_response) shall report rcp::lin::lin_category() as its category and shall have a non-empty diagnostic message.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-CANEP-001", + "title": "CAN's ep_type id is 0x0B", + "text": "rcp::endpoint::kEndpointTypeCan shall equal 0x0B.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-CANEP-002", + "title": "CAN identifier range validation", + "text": "rcp::can::validate_identifier shall accept a CanIdentifier whose value is within its own width's range (0..0x7FF for extended=false, 0..0x1FFFFFFF for extended=true) and shall return CanErrc::identifier_out_of_range for a value exceeding that range.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-CANEP-003", + "title": "CAN frame-format payload ceilings, data frames only", + "text": "rcp::can::validate_frame shall accept a CanDataFrame whose data length is within its FrameFormat's own ceiling (kMaxClassicalPayload=8 for Classical, kMaxFdPayload=64 for Fd) and shall return CanErrc::payload_exceeds_format_limit when that ceiling is exceeded; this header shall define no remote-frame request or response shape.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-CANEP-004", + "title": "CAN XL accepted single-AVTPDU payload limitation", + "text": "rcp::can::validate_frame shall accept a FrameFormat::Xl frame whose data length is within kMaxXlPayloadSingleAvtpdu, shall return CanErrc::xl_payload_exceeds_single_avtpdu_bound for a length between kMaxXlPayloadSingleAvtpdu (exclusive) and kMaxXlPayloadSpec (inclusive), and shall return CanErrc::payload_exceeds_format_limit for a length exceeding kMaxXlPayloadSpec (2054), consistent with ROADMAP.md milestone 52's already-decided fragmentation no-go.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-CANEP-005", + "title": "CAN per-phase bit-timing register sets", + "text": "rcp::can::CanEndpoint::configure_bit_timing shall store the arbitration, fd_data, and xl_data CanBitTimingPhase register sets independently of one another, retrievable unchanged via bit_timing().", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-CANEP-006", + "title": "CAN-XL-specific acceptance/receive filters", + "text": "rcp::can::CanEndpoint::receive shall accept every frame when its format-scoped filter bank is empty, shall drop a frame matching no entry in that bank when the bank is non-empty (per rcp::can::acceptance_filter_matches's masked-id comparison), and shall match a FrameFormat::Xl frame against xl_receive_filters rather than the general acceptance_filters bank whenever xl_receive_filters is non-empty.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-CANEP-007", + "title": "CAN has no trigger-signal table; CanErrc is a distinct error category", + "text": "rcp::can::CanEndpoint shall expose no TriggerRegistry accessor or signal-id helper, unlike every other device-facing endpoint type in this codebase, and rcp::can::make_error_code(CanErrc::identifier_out_of_range) shall report rcp::can::can_category() as its category with a non-empty diagnostic message.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-ISELED-001", + "title": "ISELED's ep_type id is 0x0C", + "text": "rcp::endpoint::kEndpointTypeIseled shall equal 0x0C.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-ISELED-002", + "title": "ISELED Instruction/Address/Data request and Address/Data(/CRC) response shape", + "text": "rcp::iseled::IseledEndpoint::transact shall record the exact IseledRequest (instruction/address/data) and IseledResponse (address/data/optional native_crc) fields passed to it, retrievable via last_request()/last_response().", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-ISELED-003", + "title": "ISELED native CRC is additional to, not a replacement for, the general E2E CRC", + "text": "rcp::iseled::verify_native_crc shall succeed without error when IseledResponse::native_crc is std::nullopt, shall succeed when native_crc equals compute_native_crc8(crc_coverage_bytes(response)), and shall return IseledErrc::native_crc_mismatch when it does not; this header shall have no dependency on and shall not call into rcp/e2e.hpp.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-ISELED-004", + "title": "ISELED TransferComplete and NativeCrcError trigger signals", + "text": "rcp::iseled::IseledEndpoint::transact shall notify only TransferComplete for a transaction whose response native CRC verifies (or carries none), and shall notify only NativeCrcError and return IseledErrc::native_crc_mismatch for a transaction whose response native CRC does not verify.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-ISELED-005", + "title": "IseledErrc is a distinct error category with a non-empty message", + "text": "rcp::iseled::make_error_code(IseledErrc::native_crc_mismatch) shall report rcp::iseled::iseled_category() as its category and shall have a non-empty diagnostic message.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-MDIO-001", + "title": "MDIO's ep_type id is 0x0D", + "text": "rcp::endpoint::kEndpointTypeMdio shall equal 0x0D.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-MDIO-002", + "title": "MDIO address-range validation", + "text": "rcp::mdio::validate_request shall return MdioErrc::phy_address_out_of_range for phy_address > kMaxPhyAddress, MdioErrc::device_or_reg_out_of_range for device_or_reg > kMaxDeviceOrRegField, and shall accept every field at or below its own bound.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-MDIO-003", + "title": "MDIO Clause22 flat register write-then-read round-trip", + "text": "rcp::mdio::MdioEndpoint::handle_request shall, for MdioClause::Clause22, return the value most recently written to a given (phy_address, device_or_reg) register on a subsequent read, and shall return 0 for a register that has never been written.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-MDIO-004", + "title": "MDIO Clause45 device+register addressing is distinct from Clause22's address space", + "text": "rcp::mdio::MdioEndpoint::handle_request shall key a Clause45 access on (phy_address, device_or_reg, register_address) such that it never observes a value written via MdioClause::Clause22 at the same phy_address/device_or_reg bit pattern, or vice versa, and shall notify TransferComplete on every request regardless of clause.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-MDIO-005", + "title": "MdioErrc is a distinct error category with a non-empty message", + "text": "rcp::mdio::make_error_code(MdioErrc::phy_address_out_of_range) shall report rcp::mdio::mdio_category() as its category and shall have a non-empty diagnostic message.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-WAKEUP-001", + "title": "Wakeup control's ep_type id is 0x01", + "text": "rcp::endpoint::kEndpointTypeWakeup shall equal 0x01.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-WAKEUP-002", + "title": "SleepCMD is a fixed opcode, distinct from rcp::sequencer's RequestTypeOpcode taxonomy", + "text": "rcp::wakeup::kSleepCmd shall equal 0xA5; rcp::wakeup::decode_sleep_cmd shall accept only that exact byte value and return WakeupErrc::not_sleep_cmd for every other byte (including 0x8x safety-tagged sequencer opcode bytes); rcp::wakeup::WakeupEndpoint::handle_sleep_cmd shall transition to asleep on kSleepCmd and shall leave state unchanged and return an error for any other byte.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-WAKEUP-003", + "title": "Wake-source pin monitoring", + "text": "rcp::wakeup::WakeupEndpoint::record_wake_source_event shall set the corresponding bit in wake_source_pins() and shall clear is_asleep(), and clear_wake_source_pins() shall reset the accumulated mask to 0 without affecting is_asleep() or the handshake-pending state.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-WAKEUP-004", + "title": "Repeating WakeUp message handshake for hot-start-from-Sleep", + "text": "rcp::wakeup::WakeupEndpoint::record_wake_source_event shall arm wakeup_message_pending() until acknowledge_wakeup() is called, wakeup_message_pending() shall continue reporting true across repeated calls while armed, and handle_sleep_cmd(kSleepCmd) shall clear any handshake left pending from a prior wake cycle.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, + { + "id": "REQ-WAKEUP-005", + "title": "WakeupErrc is a distinct error category with a non-empty message", + "text": "rcp::wakeup::make_error_code(WakeupErrc::not_sleep_cmd) shall report rcp::wakeup::wakeup_category() as its category and shall have a non-empty diagnostic message.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" } ] } diff --git a/CMakeLists.txt b/CMakeLists.txt index 467bc65..229f568 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.21) project(cpp-rcp - VERSION 2.6.0 + VERSION 2.7.0 DESCRIPTION "Remote Control Protocol for automotive zonal architecture (C++)" HOMEPAGE_URL "https://github.com/SoundMatt/cpp-RCP" LANGUAGES CXX diff --git a/ROADMAP.md b/ROADMAP.md index 28e229b..53f37b8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -598,6 +598,66 @@ exist." ### 51. Remaining Endpoint Types — LIN, CAN (incl. CAN XL), ISELED, MDIO, Wakeup Control (v2.7.0) +**Done (v2.7.0):** `rcp/endpoint.hpp` gains the five remaining `ep_type` id +constants this milestone assigns (`kEndpointTypeWakeup`=`0x01`, +`kEndpointTypeLin`=`0x06`, `kEndpointTypeCan`=`0x0B`, +`kEndpointTypeIseled`=`0x0C`, `kEndpointTypeMdio`=`0x0D`), replacing the +comment placeholder milestone 48 left for them; `0x0A` (DAC) remains +deliberately unallocated per this milestone's explicit DAC-out-of-scope +call. `rcp/lin.hpp` implements the LIN commander (`ep_type 0x06`): +`LinEndpoint::transfer` is a pure raw-byte-pusher — it records exactly the +bytes passed to it in both directions and performs no checksum selection, +PID generation, or schedule-table lookup, deliberately breaking with any +frame-aware assumption the deprecated `linbr.hpp` bridge's era might imply +(that file remains untouched — DEPRECATE, not ADAPT, per the Satellite +Package Disposition table). `rcp/can.hpp` implements the CAN controller +(`ep_type 0x0B`): an explicit `FrameFormat` (Classical/Fd/Xl) selects each +format's own payload ceiling via `max_payload_for`/`validate_frame`, with no +remote-frame shape defined anywhere in the header; CAN XL's payload is +bounded by `kMaxXlPayloadSingleAvtpdu`, a conservative implementation-chosen +ceiling strictly below the specification's own 2054-byte +`kMaxXlPayloadSpec`, consistent with milestone 52's already-decided +fragmentation no-go — a payload between the two is reported via +`CanErrc::xl_payload_exceeds_single_avtpdu_bound` rather than silently +accepted or truncated; `CanBitTimingConfig` carries the three independent +per-phase register sets (arbitration/fd_data/xl_data); `CanEndpoint::receive` +matches CAN-XL frames against a separate `xl_receive_filters` bank distinct +from the general `acceptance_filters` bank; and `CanEndpoint` deliberately +exposes no `TriggerRegistry` at all — the one endpoint type in this codebase +without one, since the specification defines no trigger-signal table for +it. `rcp/iseled.hpp` implements ISELED (`ep_type 0x0C`): +`IseledEndpoint::transact` carries the Instruction/Address/Data request and +Address/Data/optional-native-CRC response shape, with `compute_native_crc8`/ +`verify_native_crc` implementing that optional native CRC as a check fully +independent of — and with no dependency on — `rcp/e2e.hpp`'s general +RCP-level E2E CRC from v2.6.0, layered on top of it rather than replacing +any part of it. `rcp/mdio.hpp` implements MDIO (`ep_type 0x0D`): +`MdioRequest`/`MdioResponse` are Clause-22/Clause-45-mode-selected, with +`MdioEndpoint`'s internal `register_key` composition keeping Clause 22's +flat register address space and Clause 45's device+register address space +from ever colliding; this header defines no MDIO-specific functional-config +block, since the extraction calls for none beyond the common +generic/functional split. `rcp/wakeup.hpp` implements Wakeup control +(`ep_type 0x01`): `decode_sleep_cmd`/`WakeupEndpoint::handle_sleep_cmd` +decode the fixed `kSleepCmd` (`0xA5`) opcode via a single byte comparison +with no dependency on `rcp/sequencer.hpp`'s `RequestTypeOpcode` taxonomy; +`record_wake_source_event`/`wake_source_pins` track wake-source pin state; +and `wakeup_message_pending`/`acknowledge_wakeup` model the repeating +`WakeUp` message handshake milestone 53 (v2.9.0, Phase 14) depends on +directly. New coverage lives in `tests/test_lin.cpp` (REQ-LINEP-001..004), +`tests/test_can.cpp` (REQ-CANEP-001..007), `tests/test_iseled.cpp` +(REQ-ISELED-001..005), `tests/test_mdio.cpp` (REQ-MDIO-001..005), and +`tests/test_wakeup.cpp` (REQ-WAKEUP-001..005), traced in `.fusa-reqs.json`. +The `REQ-LINEP-*`/`REQ-CANEP-*` prefixes (rather than `REQ-LIN-*`/ +`REQ-CAN-*`) are deliberate: those shorter ids are already in use by the +untouched, deprecated `linbr.hpp`/`canbr.hpp` bridge stubs' own requirements, +and this milestone's native endpoint types needed ids of their own rather +than colliding with them. DAC (`ep_type 0x0A`) has no header, no stub, and +no requirements — left entirely unbuilt per this milestone's explicit scope +call. Full wire-level conformance against other TC18 implementations is +still not claimed — see this file's own disclaimer pattern in +`rcp/wire.hpp`/`rcp/regmap.hpp`/`rcp/sequencer.hpp`/`rcp/e2e.hpp`. + - LIN commander (`ep_type 0x06`): raw-byte-pusher model with no frame-level concepts (no checksum selection, no PID generation, no schedule tables) — validate explicitly against any assumption in the diff --git a/include/rcp/can.hpp b/include/rcp/can.hpp new file mode 100644 index 0000000..3aa6de0 --- /dev/null +++ b/include/rcp/can.hpp @@ -0,0 +1,293 @@ +// fusa:req REQ-CANEP-001 +// fusa:req REQ-CANEP-002 +// fusa:req REQ-CANEP-003 +// fusa:req REQ-CANEP-004 +// fusa:req REQ-CANEP-005 +// fusa:req REQ-CANEP-006 +// fusa:req REQ-CANEP-007 + +// CAN controller endpoint (ep_type 0x0B) — the OPEN Alliance TC18 Remote +// Control Protocol Specification v0.5.1_RC's Classical/FD/XL frame-format +// selection, data-frame-only transfer model, CAN XL's extra header region +// and extended payload ceiling, the three per-phase bit-timing register +// sets, and CAN-XL-specific acceptance/receive filtering (extraction §5.11, +// §7). +// +// ROADMAP.md milestone 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN +// XL), ISELED, MDIO, Wakeup Control (v2.7.0)": CAN is the most structurally +// involved of this milestone's five endpoint types. Three points the +// roadmap calls out explicitly: +// +// 1. Data frames only — this header has no remote-frame request/response +// shape anywhere in it. The extraction does not name a remote-frame +// concept for this endpoint type, so none is modeled (not even as a +// rejected/unsupported code path) rather than inventing one. +// 2. CAN XL's payload can reach up to 2054 bytes (`kMaxXlPayloadSpec` +// below), which necessarily spans multiple AVTPDUs once it exceeds a +// single frame's budget. ROADMAP.md milestone 52 ("Fragmentation — +// Go/No-Go Decision", v2.8.0) has already made its go/no-go call for +// this development cycle: no-go. Consistent with that already-final +// decision — and following the same pattern rcp/uart.hpp's +// `kMaxReadSize` already established for UART's own single-AVTPDU +// accepted limitation — `kMaxXlPayloadSingleAvtpdu` bounds what this +// header will accept for a single CAN XL transfer to a conservative +// value strictly below the specification's own 2054-byte field width. +// A frame whose payload falls between that bound and the spec ceiling +// is reported via `CanErrc::xl_payload_exceeds_single_avtpdu_bound` +// rather than silently truncated or accepted — this header does not +// introduce any new fragmentation logic to carry the excess. +// 3. Unlike every other device-facing endpoint type in this codebase +// (GPIO, SPI, I2C, UART, ADC, PWM_OUT, PWM_IN, LIN, ISELED, MDIO — all +// of which build on rcp/endpoint.hpp's TriggerRegistry), the +// specification defines no trigger-signal table for CAN at all. This +// is not an oversight in this header — CanEndpoint deliberately has no +// TriggerRegistry member and no signal-id helper function, and no +// "fire a trigger" call anywhere below. A future reader adding one +// should first confirm the extraction actually defines one for this +// endpoint type, not assume it was merely forgotten here. +// +// 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 bit-timing +// register field layout, the accepted single-AVTPDU XL payload bound, and +// the acceptance-filter matching rule chosen in this file are this +// implementation's own, same as the equivalent disclaimers in rcp/wire.hpp, +// rcp/regmap.hpp, rcp/endpoint.hpp, and rcp/uart.hpp. +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace rcp { +namespace can { + +// ── Frame format ────────────────────────────────────────────────────────────── +// The explicit FrameFormat sub-field a CAN request/response selects between +// (extraction §5.11): Classical CAN, CAN FD, and CAN XL each have their own +// payload ceiling and, for FD/XL, their own additional framing flags. + +enum class FrameFormat : uint8_t { Classical = 0, Fd = 1, Xl = 2 }; + +constexpr size_t kMaxClassicalPayload = 8; +constexpr size_t kMaxFdPayload = 64; + +// The specification's own CAN XL payload field width (extraction §5.11) — +// distinct from, and larger than, this implementation's accepted +// single-AVTPDU bound below. +constexpr size_t kMaxXlPayloadSpec = 2054; + +// This implementation's accepted single-AVTPDU ceiling for CAN XL, per the +// header-comment disclaimer above (point 2). Deliberately well below +// kMaxXlPayloadSpec; not derived from any exact MTU/overhead computation, +// same conservative-bound disclaimer as rcp/uart.hpp's kMaxReadSize. +constexpr size_t kMaxXlPayloadSingleAvtpdu = 256; + +// CAN XL's extra header region beyond the framing Classical/FD already carry +// (extraction §5.11) — this header does not itself lay out that region's +// sub-fields, only accounts for its length where relevant (e.g. a future +// wire codec sizing a CAN XL frame's on-wire footprint). +constexpr size_t kXlHeaderExtraLen = 6; + +constexpr size_t max_payload_for(FrameFormat fmt) noexcept { + switch (fmt) { + case FrameFormat::Classical: return kMaxClassicalPayload; + case FrameFormat::Fd: return kMaxFdPayload; + case FrameFormat::Xl: return kMaxXlPayloadSingleAvtpdu; + default: return 0; + } +} + +// ── Identifier ──────────────────────────────────────────────────────────────── + +constexpr uint32_t kMaxStandardId = 0x7FFu; // 11-bit identifier +constexpr uint32_t kMaxExtendedId = 0x1FFFFFFFu; // 29-bit identifier + +struct CanIdentifier { + uint32_t value = 0; + bool extended = false; +}; + +// ── Errors ──────────────────────────────────────────────────────────────────── + +enum class CanErrc : int { + identifier_out_of_range = 1, // CanIdentifier::value exceeds its 11-/29-bit range + payload_exceeds_format_limit = 2, // payload length exceeds the selected FrameFormat's own max + xl_payload_exceeds_single_avtpdu_bound = 3, // within the spec's 2054-byte ceiling, but beyond this + // implementation's accepted single-AVTPDU bound (see header comment) +}; + +inline const std::error_category& can_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.can"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case CanErrc::identifier_out_of_range: + return "rcp/can: identifier exceeds its 11-/29-bit range"; + case CanErrc::payload_exceeds_format_limit: + return "rcp/can: payload exceeds the selected FrameFormat's payload limit"; + case CanErrc::xl_payload_exceeds_single_avtpdu_bound: + return "rcp/can: CAN XL payload requires multi-AVTPDU fragmentation, not supported this cycle"; + default: + return "rcp/can: unknown error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(CanErrc e) noexcept { + return {static_cast(e), can_category()}; +} + +inline std::error_code validate_identifier(const CanIdentifier& id) noexcept { + const uint32_t max = id.extended ? kMaxExtendedId : kMaxStandardId; + if (id.value > max) return make_error_code(CanErrc::identifier_out_of_range); + return {}; +} + +// ── Data frame (no remote-frame shape — see header comment point 1) ───────── + +struct CanDataFrame { + CanIdentifier id; + FrameFormat format = FrameFormat::Classical; + bool bit_rate_switch = false; // FD/XL data-phase BRS; ignored for Classical + bool error_state_indicator = false; // FD/XL ESI; ignored for Classical + std::vector data; +}; + +// validate_frame checks the identifier's own range, then the payload against +// the selected format's ceiling — for FrameFormat::Xl specifically +// distinguishing "exceeds the specification's own field width" from +// "exceeds this implementation's accepted single-AVTPDU bound" so callers +// can tell a genuinely invalid frame from one this development cycle simply +// does not carry (extraction §5.11; see header comment point 2). +inline std::error_code validate_frame(const CanDataFrame& f) noexcept { + auto ec = validate_identifier(f.id); + if (ec) return ec; + + if (f.format == FrameFormat::Xl && f.data.size() > kMaxXlPayloadSingleAvtpdu) { + if (f.data.size() > kMaxXlPayloadSpec) + return make_error_code(CanErrc::payload_exceeds_format_limit); + return make_error_code(CanErrc::xl_payload_exceeds_single_avtpdu_bound); + } + + if (f.data.size() > max_payload_for(f.format)) + return make_error_code(CanErrc::payload_exceeds_format_limit); + + return {}; +} + +// ── Per-phase bit-timing register sets ─────────────────────────────────────── +// Separate register sets for the arbitration phase (used by every frame +// format for at least its identifier/arbitration field) and the FD/XL +// data-phase bit rates, which run faster than arbitration once BRS is set +// (extraction §5.11). CAN XL's data phase is its own third register set, +// distinct from FD's — the two are not required to share timing. + +struct CanBitTimingPhase { + uint32_t prescaler = 1; + uint16_t prop_seg = 0; + uint16_t phase_seg1 = 0; + uint16_t phase_seg2 = 0; + uint16_t sync_jump_width = 0; +}; + +struct CanBitTimingConfig { + CanBitTimingPhase arbitration; // Classical's only phase; FD/XL's arbitration-phase register set + CanBitTimingPhase fd_data; // FD's data-phase register set + CanBitTimingPhase xl_data; // XL's data-phase register set, independent of fd_data +}; + +// ── CAN-XL-specific acceptance / receive filters ───────────────────────────── +// A single id/mask acceptance filter shape reused for both CAN's general +// acceptance filtering and CAN XL's own receive filters (extraction §5.11). +// acceptance_filter_matches implements the standard masked-compare rule: +// bits set in `mask` must match between the filter's `id` and the candidate +// identifier; bits clear in `mask` are don't-care. + +struct CanAcceptanceFilter { + uint32_t id = 0; + uint32_t mask = 0; + bool extended = false; +}; + +inline bool acceptance_filter_matches(const CanAcceptanceFilter& filt, const CanIdentifier& id) noexcept { + if (filt.extended != id.extended) return false; + return (id.value & filt.mask) == (filt.id & filt.mask); +} + +// ── CanEndpoint ─────────────────────────────────────────────────────────────── +// No TriggerRegistry member — see header comment point 3. transmit/receive +// model one data-frame exchange each; receive() applies the configured +// acceptance filters (general, then CAN-XL-specific for an XL-format frame) +// when any are configured, dropping frames that match none. +class CanEndpoint { +public: + std::error_code configure_bit_timing(CanBitTimingConfig cfg) noexcept { + timing_ = cfg; + return {}; + } + const CanBitTimingConfig& bit_timing() const noexcept { return timing_; } + + void set_acceptance_filters(std::vector filters) { + filters_ = std::move(filters); + } + const std::vector& acceptance_filters() const noexcept { return filters_; } + + // set_xl_receive_filters configures CAN-XL-specific receive filters, + // kept as a bank distinct from the general acceptance_filters() above + // (extraction §5.11) — a CAN XL frame is matched against this bank + // instead of the general one when it is non-empty. + void set_xl_receive_filters(std::vector filters) { + xl_filters_ = std::move(filters); + } + const std::vector& xl_receive_filters() const noexcept { return xl_filters_; } + + std::error_code transmit(CanDataFrame frame) { + auto ec = validate_frame(frame); + if (ec) return ec; + last_tx_ = std::move(frame); + return {}; + } + + // receive models one inbound data frame arriving off the bus. Returns + // false (and does not record the frame) when the frame's format-scoped + // filter bank is non-empty and none of its entries match — true + // otherwise, including whenever no filter bank applies at all. + bool receive(CanDataFrame frame) { + const std::vector& bank = + (frame.format == FrameFormat::Xl && !xl_filters_.empty()) ? xl_filters_ : filters_; + if (!bank.empty()) { + const bool matched = std::any_of(bank.begin(), bank.end(), [&](const CanAcceptanceFilter& f) { + return acceptance_filter_matches(f, frame.id); + }); + if (!matched) return false; + } + last_rx_ = std::move(frame); + return true; + } + + const CanDataFrame& last_transmitted() const noexcept { return last_tx_; } + const CanDataFrame& last_received() const noexcept { return last_rx_; } + +private: + CanBitTimingConfig timing_; + std::vector filters_; + std::vector xl_filters_; + CanDataFrame last_tx_; + CanDataFrame last_rx_; +}; + +} // namespace can +} // namespace rcp + +// Enable std::error_code construction from rcp::can::CanErrc. +namespace std { +template <> +struct is_error_code_enum : true_type {}; +} // namespace std diff --git a/include/rcp/endpoint.hpp b/include/rcp/endpoint.hpp index 23a6a6a..44c3711 100644 --- a/include/rcp/endpoint.hpp +++ b/include/rcp/endpoint.hpp @@ -61,9 +61,12 @@ namespace endpoint { // this numbering for endpoint milestones. Ids are added here as each // milestone's endpoint types are implemented, so the numbering stays // visibly centralized in one place rather than getting re-derived ad hoc by -// each new header. 0x06 has no assignment in either v2.3.0 or v2.4.0's -// scope (the extraction does not name an endpoint type for it) and is left -// unallocated rather than guessed at. +// each new header. 0x06 (LIN) was deliberately left unassigned through +// v2.3.0/v2.4.0, pending the endpoint types v2.7.0 covers (ROADMAP.md +// milestone 51) — it and the other four ids that milestone assigns +// (Wakeup/CAN/ISELED/MDIO) are filled in below rather than guessed at +// early. 0x0A (DAC) remains deliberately unallocated — see the comment +// beside the v2.7.0 ids below. using EndpointTypeId = uint8_t; @@ -74,7 +77,15 @@ constexpr EndpointTypeId kEndpointTypeUart = 0x05; // this milestone (v2.4.0) constexpr EndpointTypeId kEndpointTypePwmOut = 0x07; // this milestone (v2.4.0) constexpr EndpointTypeId kEndpointTypePwmIn = 0x08; // this milestone (v2.4.0) constexpr EndpointTypeId kEndpointTypeAdc = 0x09; // this milestone (v2.4.0) -// LIN, CAN (incl. CAN XL), ISELED, MDIO, Wakeup control — v2.7.0 (ids TBD there) +constexpr EndpointTypeId kEndpointTypeWakeup = 0x01; // v2.7.0 +constexpr EndpointTypeId kEndpointTypeLin = 0x06; // v2.7.0 +constexpr EndpointTypeId kEndpointTypeCan = 0x0B; // v2.7.0 +constexpr EndpointTypeId kEndpointTypeIseled = 0x0C; // v2.7.0 +constexpr EndpointTypeId kEndpointTypeMdio = 0x0D; // v2.7.0 +// 0x0A (DAC) is explicitly out of scope for v2.7.0 — the specification +// reserves the id and a DAC_OUT pin signal but defines no functional-config +// chapter for it in v0.5.1_RC; left unallocated here rather than guessed at +// until a future spec revision actually defines it (ROADMAP.md milestone 51). // ── evt[2:0] write semantics ────────────────────────────────────────────────── // The 8-way write-semantics selector every combinable-payload endpoint type diff --git a/include/rcp/iseled.hpp b/include/rcp/iseled.hpp new file mode 100644 index 0000000..b279b04 --- /dev/null +++ b/include/rcp/iseled.hpp @@ -0,0 +1,188 @@ +// fusa:req REQ-ISELED-001 +// fusa:req REQ-ISELED-002 +// fusa:req REQ-ISELED-003 +// fusa:req REQ-ISELED-004 +// fusa:req REQ-ISELED-005 + +// ISELED endpoint (ep_type 0x0C) — the OPEN Alliance TC18 Remote Control +// Protocol Specification v0.5.1_RC's native ISELED daisy-chain framing: an +// Instruction/Address/Data request shape and an Address/Data/optional- +// native-CRC response shape (extraction §5.12). +// +// 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 ISELED is that its own native CRC (the optional +// trailing field in the response shape below) is ADDITIONAL to, not a +// replacement for, the general RCP-level end-to-end CRC rcp/e2e.hpp already +// implements (v2.6.0). This header has no dependency on rcp/e2e.hpp and does +// not call into it: rcp/e2e.hpp's CRC covers the whole ACF message +// (stream_id + avtp_timestamp + ACF header + payload) regardless of +// endpoint type, and continues to do so unmodified around an ISELED +// request/response exactly as it does around every other endpoint type's +// payload — ISELED's native CRC below is a second, independent, narrower +// check scoped only to the Address/Data content of one daisy-chain +// response, layered on top of that outer coverage rather than folded into +// or substituted for it. +// +// The ISELED daisy-chain protocol itself (its addressing scheme, its actual +// native CRC polynomial/width, its per-device propagation timing) is +// defined by the ISELED standard, not by TC18 RCP, and no such external +// specification is reproduced or matched exactly here — `compute_native_crc8` +// below computes a CRC-8 of this implementation's own choosing, present so +// this header's encode/decode/verify round-trip has a concrete algorithm to +// exercise, not a claim of bit-exact conformance with any particular ISELED +// silicon's real CRC. +// +// 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 request/response +// field widths, the native-CRC algorithm, and the trigger-signal id +// encoding chosen in this file are this implementation's own, same as the +// equivalent disclaimers in rcp/wire.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, +// and rcp/e2e.hpp. +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace rcp { +namespace iseled { + +// ── Request / response shapes ──────────────────────────────────────────────── + +struct IseledRequest { + uint8_t instruction = 0; + uint16_t address = 0; // this implementation's own width for a chain-position + register address + std::vector data; +}; + +struct IseledResponse { + uint16_t address = 0; + std::vector data; + std::optional native_crc; // optional, per the response shape's own "optional-native-CRC" naming +}; + +// ── Native CRC (this implementation's own algorithm — see header comment) ─── + +constexpr uint8_t kNativeCrcPoly = 0x07; // CRC-8/CCITT-shaped choice; not a claim of matching real ISELED silicon + +inline uint8_t compute_native_crc8(const std::vector& bytes) noexcept { + uint8_t crc = 0; + for (uint8_t b : bytes) { + crc = static_cast(crc ^ b); + for (int bit = 0; bit < 8; ++bit) { + crc = (crc & 0x80u) ? static_cast((crc << 1) ^ kNativeCrcPoly) + : static_cast(crc << 1); + } + } + return crc; +} + +// crc_coverage_bytes assembles exactly the bytes compute_native_crc8 covers +// for a response: the 2-byte big-endian address followed by the data field, +// matching how verify_native_crc below reconstructs the same buffer to +// check a response's optional native_crc. +inline std::vector crc_coverage_bytes(const IseledResponse& resp) { + std::vector buf; + buf.reserve(2 + resp.data.size()); + buf.push_back(static_cast(resp.address >> 8)); + buf.push_back(static_cast(resp.address & 0xFFu)); + buf.insert(buf.end(), resp.data.begin(), resp.data.end()); + return buf; +} + +// ── Errors ──────────────────────────────────────────────────────────────────── + +enum class IseledErrc : int { + native_crc_mismatch = 1, // response carried a native_crc that does not match its own address+data +}; + +inline const std::error_category& iseled_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.iseled"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case IseledErrc::native_crc_mismatch: + return "rcp/iseled: response native CRC does not match its address+data"; + default: + return "rcp/iseled: unknown error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(IseledErrc e) noexcept { + return {static_cast(e), iseled_category()}; +} + +// verify_native_crc is a no-op success when response.native_crc is not +// present (the field is optional per the response shape) and otherwise +// recomputes compute_native_crc8 over crc_coverage_bytes(response) and +// compares. This check is independent of, and has no effect on, the +// separate general RCP-level E2E CRC that rcp/e2e.hpp applies around the +// whole ACF message — see header comment. +inline std::error_code verify_native_crc(const IseledResponse& response) noexcept { + if (!response.native_crc.has_value()) return {}; + const uint8_t expected = compute_native_crc8(crc_coverage_bytes(response)); + if (expected != *response.native_crc) return make_error_code(IseledErrc::native_crc_mismatch); + return {}; +} + +// ── Trigger signals ─────────────────────────────────────────────────────────── +// One TransferComplete/NativeCrcError pair per IseledEndpoint instance, built +// on rcp/endpoint.hpp's generic TriggerRegistry, same primitive +// rcp/i2c.hpp's and rcp/spi.hpp's transfer-complete signals use. + +enum class IseledSignal : uint8_t { TransferComplete = 0, NativeCrcError = 1 }; + +constexpr endpoint::TriggerRegistry::SignalId iseled_signal_id(IseledSignal sig) noexcept { + return static_cast(sig); +} + +// ── IseledEndpoint ──────────────────────────────────────────────────────────── +// Mirrors rcp::spi::SpiEndpoint's/rcp::i2c::I2cEndpoint's shape: one +// request-dispatch entry point per daisy-chain transaction, recording the +// exact Instruction/Address/Data request and Address/Data/optional-CRC +// response (supplied together by the caller — this header models the +// request/response and trigger-signal shape of one ISELED transaction, not +// an actual daisy-chain driver or its per-device propagation timing). +class IseledEndpoint { +public: + std::error_code transact(IseledRequest request, IseledResponse response) { + last_request_ = std::move(request); + auto ec = verify_native_crc(response); + last_response_ = std::move(response); + if (ec) { + triggers_.notify(iseled_signal_id(IseledSignal::NativeCrcError)); + return ec; + } + triggers_.notify(iseled_signal_id(IseledSignal::TransferComplete)); + return {}; + } + + const IseledRequest& last_request() const noexcept { return last_request_; } + const IseledResponse& last_response() const noexcept { return last_response_; } + endpoint::TriggerRegistry& triggers() noexcept { return triggers_; } + +private: + endpoint::TriggerRegistry triggers_; + IseledRequest last_request_; + IseledResponse last_response_; +}; + +} // namespace iseled +} // namespace rcp + +// Enable std::error_code construction from rcp::iseled::IseledErrc. +namespace std { +template <> +struct is_error_code_enum : true_type {}; +} // namespace std diff --git a/include/rcp/lin.hpp b/include/rcp/lin.hpp new file mode 100644 index 0000000..dc76b28 --- /dev/null +++ b/include/rcp/lin.hpp @@ -0,0 +1,134 @@ +// fusa:req REQ-LINEP-001 +// fusa:req REQ-LINEP-002 +// fusa:req REQ-LINEP-003 +// fusa:req REQ-LINEP-004 + +// LIN commander endpoint (ep_type 0x06) — the OPEN Alliance TC18 Remote +// Control Protocol Specification v0.5.1_RC's raw-byte-pusher model for LIN: +// a request carries the exact bytes to place on the bus and a response +// carries the exact bytes observed back, with no frame-level concept +// modeled by the endpoint itself at all — no checksum selection, no PID +// generation, no schedule tables (extraction §5.10, §6 "significant +// behavioral-scope question"). +// +// ROADMAP.md milestone 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN +// XL), ISELED, MDIO, Wakeup Control (v2.7.0)": LIN is deliberately the +// simplest of this milestone's five endpoint types by design, not by +// omission — the specification puts every bit of LIN frame construction +// (break/sync generation, PID computation, checksum selection between +// classic and enhanced, schedule-table sequencing) on the client-side +// driver, not on the RC-Server endpoint. This is an explicit deviation from +// the pre-replacement `rcp/linbr.hpp` bridge's shape: that stub (now +// DEPRECATE per the Satellite Package Disposition table, left untouched by +// this milestone) was built against the old rcp::Controller/Zone/Command +// model bridging *from* a Zone, which is the inverse direction and had no +// occasion to assume the endpoint understood frame structure one way or the +// other — this header exists to make explicit, in code, that a native +// LIN-commander endpoint carries none of that structure either, so a future +// reader does not carry over any frame-aware assumption from `linbr.hpp`'s +// era. `LinEndpoint::transfer` below mirrors `rcp::i2c::I2cEndpoint:: +// transfer`'s raw byte-stream shape for exactly this reason: both endpoint +// types push/pull opaque bytes and leave interpretation to the caller. +// +// Field names and behavior below implement TC18's *behavior* as described in +// an internal structured extraction of the specification named above; no +// text from that document is reproduced here. The concrete transfer-shape +// and trigger-signal id encoding chosen in this file are this +// implementation's own, same as the equivalent disclaimers in rcp/wire.hpp, +// rcp/regmap.hpp, rcp/endpoint.hpp, and rcp/i2c.hpp. +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace rcp { +namespace lin { + +// ── Errors ──────────────────────────────────────────────────────────────────── +// no_response is this header's only defined failure: the commander pushed +// bytes onto the bus and nothing came back within whatever window the +// caller's driver layer enforces (this header has no clock of its own, same +// disclaimer as every other bus endpoint type in this codebase). + +enum class LinErrc : int { + no_response = 1, +}; + +inline const std::error_category& lin_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.lin"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case LinErrc::no_response: return "rcp/lin: no response observed on the bus"; + default: return "rcp/lin: unknown error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(LinErrc e) noexcept { + return {static_cast(e), lin_category()}; +} + +// ── Trigger signals ─────────────────────────────────────────────────────────── +// One TransferComplete/NoResponse pair per LinEndpoint instance — LIN has no +// pre-configured channel concept in this milestone's scope (a single +// LinEndpoint instance models one commander-mode LIN bus), matching +// rcp/i2c.hpp's own no-channel precedent. Built on rcp/endpoint.hpp's +// generic TriggerRegistry. + +enum class LinSignal : uint8_t { TransferComplete = 0, NoResponse = 1 }; + +constexpr endpoint::TriggerRegistry::SignalId lin_signal_id(LinSignal sig) noexcept { + return static_cast(sig); +} + +// ── LinEndpoint ─────────────────────────────────────────────────────────────── +// Mirrors rcp::i2c::I2cEndpoint's shape deliberately: one request-dispatch +// entry point per incoming LIN transfer, recording the raw bytes pushed and +// the raw bytes observed back, with no interpretation of either as a LIN +// frame (extraction §5.10). `out_bytes` is whatever byte sequence the +// caller's driver constructed (break/sync/PID/data/checksum all already +// assembled by that driver, not by this header); `in_bytes` is this +// implementation's record of whatever came back over the same exchange +// (supplied by the caller — this header models the request/response and +// trigger-signal shape of a LIN commander transfer, not an actual bus). +class LinEndpoint { +public: + std::error_code transfer(std::vector out_bytes, std::vector in_bytes, + bool responded = true) { + last_out_ = std::move(out_bytes); + last_in_ = std::move(in_bytes); + if (!responded) { + triggers_.notify(lin_signal_id(LinSignal::NoResponse)); + return make_error_code(LinErrc::no_response); + } + triggers_.notify(lin_signal_id(LinSignal::TransferComplete)); + return {}; + } + + const std::vector& last_sent() const noexcept { return last_out_; } + const std::vector& last_received() const noexcept { return last_in_; } + endpoint::TriggerRegistry& triggers() noexcept { return triggers_; } + +private: + endpoint::TriggerRegistry triggers_; + std::vector last_out_; + std::vector last_in_; +}; + +} // namespace lin +} // namespace rcp + +// Enable std::error_code construction from rcp::lin::LinErrc. +namespace std { +template <> +struct is_error_code_enum : true_type {}; +} // namespace std diff --git a/include/rcp/mdio.hpp b/include/rcp/mdio.hpp new file mode 100644 index 0000000..0080b07 --- /dev/null +++ b/include/rcp/mdio.hpp @@ -0,0 +1,162 @@ +// fusa:req REQ-MDIO-001 +// fusa:req REQ-MDIO-002 +// fusa:req REQ-MDIO-003 +// fusa:req REQ-MDIO-004 +// fusa:req REQ-MDIO-005 + +// MDIO endpoint (ep_type 0x0D) — the OPEN Alliance TC18 Remote Control +// Protocol Specification v0.5.1_RC's Clause-22/Clause-45-style mode-selected +// PHY register access (extraction §5.13). +// +// ROADMAP.md milestone 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN +// XL), ISELED, MDIO, Wakeup Control (v2.7.0)": two points the roadmap calls +// out explicitly for MDIO. First, this endpoint type is fully specified in +// the source document but is not named in that document's own informative +// "ten interfaces" scope-summary list (extraction §1.2) — this +// implementation treats that as an editorial omission in the summary list, +// not as evidence MDIO is actually out of scope, and builds it anyway. +// Second, MDIO carries essentially no type-specific functional +// configuration beyond rcp/regmap.hpp's already-existing generic/functional +// endpoint config split (v2.1.0) — unlike rcp/gpio.hpp (which layers a +// pin-direction-plus-edge-mask block on top of that split), this header +// defines no MDIO-specific functional-config encode/decode pair at all, +// because the extraction does not call for one; MdioRequest/MdioResponse +// below are the entire type-specific surface. +// +// 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 request/response +// field widths and register-key composition chosen in this file are this +// implementation's own, same as the equivalent disclaimers in rcp/wire.hpp, +// rcp/regmap.hpp, and rcp/endpoint.hpp. +#pragma once + +#include + +#include +#include +#include +#include + +namespace rcp { +namespace mdio { + +// ── Clause selection ───────────────────────────────────────────────────────── +// Clause 22's addressing is a flat 5-bit register address within a PHY; +// Clause 45 adds a device (MMD) address plus a wider 16-bit register address +// within that device (extraction §5.13). + +enum class MdioClause : uint8_t { Clause22 = 0, Clause45 = 1 }; + +constexpr uint8_t kMaxPhyAddress = 0x1Fu; // 5-bit field +constexpr uint8_t kMaxDeviceOrRegField = 0x1Fu; // 5-bit field: Clause22 register addr, or Clause45 MMD device addr + +struct MdioRequest { + MdioClause clause = MdioClause::Clause22; + uint8_t phy_address = 0; // 5-bit PHY address + uint8_t device_or_reg = 0; // Clause22: register address; Clause45: MMD device address + uint16_t register_address = 0; // Clause45 only: 16-bit register within the MMD; unused for Clause22 + bool is_write = false; + uint16_t write_value = 0; // meaningful only when is_write is true +}; + +struct MdioResponse { + uint16_t value = 0; +}; + +// ── Errors ──────────────────────────────────────────────────────────────────── + +enum class MdioErrc : int { + phy_address_out_of_range = 1, + device_or_reg_out_of_range = 2, +}; + +inline const std::error_category& mdio_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.mdio"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case MdioErrc::phy_address_out_of_range: + return "rcp/mdio: PHY address exceeds its 5-bit range"; + case MdioErrc::device_or_reg_out_of_range: + return "rcp/mdio: device/register address exceeds its 5-bit range"; + default: + return "rcp/mdio: unknown error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(MdioErrc e) noexcept { + return {static_cast(e), mdio_category()}; +} + +inline std::error_code validate_request(const MdioRequest& req) noexcept { + if (req.phy_address > kMaxPhyAddress) return make_error_code(MdioErrc::phy_address_out_of_range); + if (req.device_or_reg > kMaxDeviceOrRegField) + return make_error_code(MdioErrc::device_or_reg_out_of_range); + return {}; +} + +// ── Trigger signals ─────────────────────────────────────────────────────────── +// One TransferComplete signal per MdioEndpoint instance, built on +// rcp/endpoint.hpp's generic TriggerRegistry, same primitive every other +// bus-style endpoint type in this codebase uses. + +enum class MdioSignal : uint8_t { TransferComplete = 0 }; + +constexpr endpoint::TriggerRegistry::SignalId mdio_signal_id(MdioSignal sig) noexcept { + return static_cast(sig); +} + +// ── MdioEndpoint ────────────────────────────────────────────────────────────── +// register_key folds (clause, phy_address, device_or_reg[, register_address +// for Clause45 only]) into one lookup key so Clause22's flat address space +// and Clause45's device+register address space never collide with each +// other, even for identical phy_address/device_or_reg bit patterns. +class MdioEndpoint { +public: + std::error_code handle_request(MdioRequest req, MdioResponse& out) { + auto ec = validate_request(req); + if (ec) return ec; + + last_request_ = req; + const uint64_t key = register_key(req); + if (req.is_write) { + registers_[key] = req.write_value; + out.value = req.write_value; + } else { + const auto it = registers_.find(key); + out.value = (it != registers_.end()) ? it->second : uint16_t{0}; + } + triggers_.notify(mdio_signal_id(MdioSignal::TransferComplete)); + return {}; + } + + const MdioRequest& last_request() const noexcept { return last_request_; } + endpoint::TriggerRegistry& triggers() noexcept { return triggers_; } + +private: + static uint64_t register_key(const MdioRequest& req) noexcept { + uint64_t key = (static_cast(req.clause) << 40) | + (static_cast(req.phy_address) << 32) | + (static_cast(req.device_or_reg) << 16); + if (req.clause == MdioClause::Clause45) key |= req.register_address; + return key; + } + + endpoint::TriggerRegistry triggers_; + MdioRequest last_request_; + std::unordered_map registers_; +}; + +} // namespace mdio +} // namespace rcp + +// Enable std::error_code construction from rcp::mdio::MdioErrc. +namespace std { +template <> +struct is_error_code_enum : true_type {}; +} // namespace std diff --git a/include/rcp/version.hpp b/include/rcp/version.hpp index 03c05b7..ba8982b 100644 --- a/include/rcp/version.hpp +++ b/include/rcp/version.hpp @@ -6,6 +6,6 @@ namespace rcp { // Semantic version of the cpp-RCP implementation (matches the latest git tag). -constexpr std::string_view kVersion = "2.6.0"; +constexpr std::string_view kVersion = "2.7.0"; } // namespace rcp diff --git a/include/rcp/wakeup.hpp b/include/rcp/wakeup.hpp new file mode 100644 index 0000000..3803bcc --- /dev/null +++ b/include/rcp/wakeup.hpp @@ -0,0 +1,149 @@ +// fusa:req REQ-WAKEUP-001 +// fusa:req REQ-WAKEUP-002 +// fusa:req REQ-WAKEUP-003 +// fusa:req REQ-WAKEUP-004 +// fusa:req REQ-WAKEUP-005 + +// 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). +// +// 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/sequencer.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/sequencer.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. +// +// 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. +// +// 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 +// rcp/wire.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, and rcp/sequencer.hpp. +#pragma once + +#include +#include +#include + +namespace rcp { +namespace wakeup { + +// ── SleepCMD — a fixed opcode, not a RequestTypeOpcode (see header comment) ── + +constexpr uint8_t kSleepCmd = 0xA5; + +enum class WakeupErrc : int { + not_sleep_cmd = 1, // decode_sleep_cmd was given a byte other than kSleepCmd +}; + +inline const std::error_category& wakeup_category() noexcept { + struct Cat : std::error_category { + 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"; + } + } + }; + static Cat instance; + return instance; +} + +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); + return {}; +} + +// ── Wake-source pins ────────────────────────────────────────────────────────── + +using WakeSourceMask = uint32_t; +constexpr uint8_t kMaxWakeSourcePins = 32; + +// ── 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. +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. + std::error_code handle_sleep_cmd(uint8_t request_byte) noexcept { + auto ec = decode_sleep_cmd(request_byte); + if (ec) return ec; + asleep_ = true; + wake_handshake_pending_ = false; // entering Sleep clears any handshake left over from a prior cycle + return {}; + } + + 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); + 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. + 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; } + +private: + bool asleep_ = false; + bool wake_handshake_pending_ = false; + WakeSourceMask wake_source_pins_ = 0; +}; + +} // namespace wakeup +} // namespace rcp + +// Enable std::error_code construction from rcp::wakeup::WakeupErrc. +namespace std { +template <> +struct is_error_code_enum : true_type {}; +} // namespace std diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 937ad45..fa5ee12 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -62,6 +62,26 @@ add_executable(test_e2e test_e2e.cpp) target_link_libraries(test_e2e PRIVATE rcp::rcp Catch2::Catch2WithMain) add_test(NAME rcp_e2e COMMAND test_e2e) +add_executable(test_lin test_lin.cpp) +target_link_libraries(test_lin PRIVATE rcp::rcp Catch2::Catch2WithMain) +add_test(NAME rcp_lin COMMAND test_lin) + +add_executable(test_can test_can.cpp) +target_link_libraries(test_can PRIVATE rcp::rcp Catch2::Catch2WithMain) +add_test(NAME rcp_can COMMAND test_can) + +add_executable(test_iseled test_iseled.cpp) +target_link_libraries(test_iseled PRIVATE rcp::rcp Catch2::Catch2WithMain) +add_test(NAME rcp_iseled COMMAND test_iseled) + +add_executable(test_mdio test_mdio.cpp) +target_link_libraries(test_mdio PRIVATE rcp::rcp Catch2::Catch2WithMain) +add_test(NAME rcp_mdio COMMAND test_mdio) + +add_executable(test_wakeup test_wakeup.cpp) +target_link_libraries(test_wakeup PRIVATE rcp::rcp Catch2::Catch2WithMain) +add_test(NAME rcp_wakeup COMMAND test_wakeup) + add_executable(test_prioqueue test_prioqueue.cpp) target_link_libraries(test_prioqueue PRIVATE rcp::rcp Catch2::Catch2WithMain) add_test(NAME rcp_prioqueue COMMAND test_prioqueue) diff --git a/tests/test_can.cpp b/tests/test_can.cpp new file mode 100644 index 0000000..0d65f06 --- /dev/null +++ b/tests/test_can.cpp @@ -0,0 +1,185 @@ +// fusa:test REQ-CANEP-001 +// fusa:test REQ-CANEP-002 +// fusa:test REQ-CANEP-003 +// fusa:test REQ-CANEP-004 +// fusa:test REQ-CANEP-005 +// fusa:test REQ-CANEP-006 +// fusa:test REQ-CANEP-007 + +// Tests for rcp/can.hpp — the CAN controller endpoint type (ROADMAP.md +// milestone 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN XL), +// ISELED, MDIO, Wakeup Control", v2.7.0). + +#include +#include +#include + +using namespace rcp::can; + +// ── ep_type id ──────────────────────────────────────────────────────────────── + +TEST_CASE("CAN's ep_type id is 0x0B", "[can][REQ-CANEP-001]") { + REQUIRE(rcp::endpoint::kEndpointTypeCan == 0x0B); +} + +// ── Identifier range validation ─────────────────────────────────────────────── + +TEST_CASE("validate_identifier accepts in-range standard and extended ids", "[can][REQ-CANEP-002]") { + REQUIRE_FALSE(validate_identifier({0x7FF, false})); + REQUIRE_FALSE(validate_identifier({0x1FFFFFFF, true})); +} + +TEST_CASE("validate_identifier rejects an out-of-range id for its own width", "[can][REQ-CANEP-002]") { + auto ec = validate_identifier({0x800, false}); // 12 bits, too wide for an 11-bit standard id + REQUIRE(ec == make_error_code(CanErrc::identifier_out_of_range)); + + ec = validate_identifier({0x20000000, true}); // 30 bits, too wide for a 29-bit extended id + REQUIRE(ec == make_error_code(CanErrc::identifier_out_of_range)); +} + +// ── Frame-format payload ceilings, data frames only ────────────────────────── + +TEST_CASE("validate_frame enforces each FrameFormat's own payload ceiling", "[can][REQ-CANEP-003]") { + CanDataFrame classical; + classical.id.value = 0x100; + classical.format = FrameFormat::Classical; + classical.data.assign(kMaxClassicalPayload, 0xAA); + REQUIRE_FALSE(validate_frame(classical)); + + classical.data.push_back(0xAA); // 9 bytes, exceeds Classical's 8-byte ceiling + REQUIRE(validate_frame(classical) == make_error_code(CanErrc::payload_exceeds_format_limit)); + + CanDataFrame fd; + fd.id.value = 0x100; + fd.format = FrameFormat::Fd; + fd.data.assign(kMaxFdPayload, 0x55); + REQUIRE_FALSE(validate_frame(fd)); + + fd.data.push_back(0x55); // 65 bytes, exceeds FD's 64-byte ceiling + REQUIRE(validate_frame(fd) == make_error_code(CanErrc::payload_exceeds_format_limit)); +} + +// ── CAN XL: accepted single-AVTPDU limitation ──────────────────────────────── + +TEST_CASE("validate_frame accepts a CAN XL payload within the single-AVTPDU bound", "[can][REQ-CANEP-004]") { + CanDataFrame xl; + xl.id.value = 0x100; + xl.format = FrameFormat::Xl; + xl.data.assign(kMaxXlPayloadSingleAvtpdu, 0x11); + REQUIRE_FALSE(validate_frame(xl)); +} + +TEST_CASE("validate_frame reports xl_payload_exceeds_single_avtpdu_bound between the accepted " + "bound and the specification's own ceiling", + "[can][REQ-CANEP-004]") { + CanDataFrame xl; + xl.id.value = 0x100; + xl.format = FrameFormat::Xl; + xl.data.assign(kMaxXlPayloadSingleAvtpdu + 1, 0x11); + REQUIRE(kMaxXlPayloadSingleAvtpdu + 1 <= kMaxXlPayloadSpec); + REQUIRE(validate_frame(xl) == make_error_code(CanErrc::xl_payload_exceeds_single_avtpdu_bound)); +} + +TEST_CASE("validate_frame reports payload_exceeds_format_limit beyond the specification's own " + "2054-byte CAN XL ceiling", + "[can][REQ-CANEP-004]") { + CanDataFrame xl; + xl.id.value = 0x100; + xl.format = FrameFormat::Xl; + xl.data.assign(kMaxXlPayloadSpec + 1, 0x11); + REQUIRE(validate_frame(xl) == make_error_code(CanErrc::payload_exceeds_format_limit)); +} + +// ── Per-phase bit-timing register sets ─────────────────────────────────────── + +TEST_CASE("CanEndpoint::configure_bit_timing stores arbitration/fd_data/xl_data independently", + "[can][REQ-CANEP-005]") { + CanEndpoint ep; + CanBitTimingConfig cfg; + cfg.arbitration.prescaler = 4; + cfg.fd_data.prescaler = 1; + cfg.xl_data.prescaler = 1; + cfg.arbitration.phase_seg1 = 10; + cfg.fd_data.phase_seg1 = 3; + cfg.xl_data.phase_seg1 = 2; + + REQUIRE_FALSE(ep.configure_bit_timing(cfg)); + REQUIRE(ep.bit_timing().arbitration.prescaler == 4); + REQUIRE(ep.bit_timing().fd_data.prescaler == 1); + REQUIRE(ep.bit_timing().xl_data.prescaler == 1); + REQUIRE(ep.bit_timing().arbitration.phase_seg1 == 10); + REQUIRE(ep.bit_timing().fd_data.phase_seg1 == 3); + REQUIRE(ep.bit_timing().xl_data.phase_seg1 == 2); +} + +// ── CAN-XL-specific acceptance/receive filters ─────────────────────────────── + +TEST_CASE("CanEndpoint::receive accepts everything when no filters are configured", + "[can][REQ-CANEP-006]") { + CanEndpoint ep; + CanDataFrame f; + f.id.value = 0x123; + f.format = FrameFormat::Classical; + REQUIRE(ep.receive(f)); + REQUIRE(ep.last_received().id.value == 0x123); +} + +TEST_CASE("CanEndpoint::receive drops a non-XL frame that matches no general acceptance filter", + "[can][REQ-CANEP-006]") { + CanEndpoint ep; + ep.set_acceptance_filters({CanAcceptanceFilter{0x200, 0x7FF, false}}); + + CanDataFrame f; + f.id.value = 0x100; + f.format = FrameFormat::Classical; + REQUIRE_FALSE(ep.receive(f)); +} + +TEST_CASE("CanEndpoint::receive accepts a frame matching a general acceptance filter's masked id", + "[can][REQ-CANEP-006]") { + CanEndpoint ep; + ep.set_acceptance_filters({CanAcceptanceFilter{0x200, 0x700, false}}); // top 3 id bits must match 0x200's + + CanDataFrame f; + f.id.value = 0x21F; // top 3 bits (0x200) match; low bits are don't-care + f.format = FrameFormat::Classical; + REQUIRE(ep.receive(f)); +} + +TEST_CASE("CanEndpoint::receive matches an XL frame against xl_receive_filters, not the general " + "acceptance bank, once xl_receive_filters is configured", + "[can][REQ-CANEP-006]") { + CanEndpoint ep; + ep.set_acceptance_filters({CanAcceptanceFilter{0x999, 0x7FF, false}}); // would reject 0x100 + ep.set_xl_receive_filters({CanAcceptanceFilter{0x100, 0x7FF, false}}); // accepts 0x100 + + CanDataFrame f; + f.id.value = 0x100; + f.format = FrameFormat::Xl; + REQUIRE(ep.receive(f)); +} + +// ── No trigger-signal table for CAN (extraction §5.11, §7) ────────────────── +// CAN is the one device-facing endpoint type in this codebase with no +// TriggerRegistry at all — there is no triggers() accessor on CanEndpoint to +// call in the first place, so the "no trigger table" property is enforced +// structurally (a caller cannot arm/drain anything), not by a runtime check. +// This test only documents that CanEndpoint's public surface has no +// trigger-related members, by construction: the class compiles without +// exposing one. + +TEST_CASE("CanEndpoint exposes no TriggerRegistry accessor", "[can][REQ-CANEP-007]") { + CanEndpoint ep; + CanDataFrame f; + f.id.value = 0x1; + REQUIRE(ep.transmit(f) == std::error_code{}); + // No ep.triggers() call exists to make here — that is the point. +} + +// ── CanErrc category sanity ─────────────────────────────────────────────────── + +TEST_CASE("CanErrc reports a non-empty message in its own category", "[can][REQ-CANEP-007]") { + auto ec = make_error_code(CanErrc::identifier_out_of_range); + REQUIRE(ec.category() == can_category()); + REQUIRE_FALSE(ec.message().empty()); +} diff --git a/tests/test_iseled.cpp b/tests/test_iseled.cpp new file mode 100644 index 0000000..645b569 --- /dev/null +++ b/tests/test_iseled.cpp @@ -0,0 +1,124 @@ +// fusa:test REQ-ISELED-001 +// fusa:test REQ-ISELED-002 +// fusa:test REQ-ISELED-003 +// fusa:test REQ-ISELED-004 +// fusa:test REQ-ISELED-005 + +// Tests for rcp/iseled.hpp — the ISELED endpoint type (ROADMAP.md milestone +// 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN XL), ISELED, MDIO, +// Wakeup Control", v2.7.0). + +#include +#include +#include + +using namespace rcp::iseled; + +// ── ep_type id ──────────────────────────────────────────────────────────────── + +TEST_CASE("ISELED's ep_type id is 0x0C", "[iseled][REQ-ISELED-001]") { + REQUIRE(rcp::endpoint::kEndpointTypeIseled == 0x0C); +} + +// ── Instruction/Address/Data request, Address/Data(/CRC) response shape ───── + +TEST_CASE("IseledEndpoint::transact records the exact request and response fields", + "[iseled][REQ-ISELED-002]") { + IseledEndpoint ep; + IseledRequest req; + req.instruction = 0x03; + req.address = 0x0102; + req.data = {0xDE, 0xAD}; + + IseledResponse resp; + resp.address = 0x0102; + resp.data = {0xBE, 0xEF}; + // no native_crc — optional field left absent + + REQUIRE_FALSE(ep.transact(req, resp)); + REQUIRE(ep.last_request().instruction == 0x03); + REQUIRE(ep.last_request().address == 0x0102); + REQUIRE(ep.last_request().data == std::vector{0xDE, 0xAD}); + REQUIRE(ep.last_response().address == 0x0102); + REQUIRE(ep.last_response().data == std::vector{0xBE, 0xEF}); + REQUIRE_FALSE(ep.last_response().native_crc.has_value()); +} + +// ── Native CRC: additional to, not a replacement for, the general E2E CRC ─── + +TEST_CASE("compute_native_crc8/verify_native_crc round-trip a correct native CRC", + "[iseled][REQ-ISELED-003]") { + IseledResponse resp; + resp.address = 0x00AB; + resp.data = {0x01, 0x02, 0x03}; + resp.native_crc = compute_native_crc8(crc_coverage_bytes(resp)); + + REQUIRE_FALSE(verify_native_crc(resp)); +} + +TEST_CASE("verify_native_crc reports native_crc_mismatch for a tampered native CRC", + "[iseled][REQ-ISELED-003]") { + IseledResponse resp; + resp.address = 0x00AB; + resp.data = {0x01, 0x02, 0x03}; + resp.native_crc = static_cast(compute_native_crc8(crc_coverage_bytes(resp)) ^ 0xFFu); + + REQUIRE(verify_native_crc(resp) == make_error_code(IseledErrc::native_crc_mismatch)); +} + +TEST_CASE("verify_native_crc succeeds trivially when native_crc is absent (it is optional)", + "[iseled][REQ-ISELED-003]") { + IseledResponse resp; + resp.address = 0x0001; + resp.data = {0xFF}; + REQUIRE_FALSE(verify_native_crc(resp)); +} + +TEST_CASE("IseledEndpoint::transact propagates a native CRC mismatch and fires NativeCrcError", + "[iseled][REQ-ISELED-003]") { + IseledEndpoint ep; + ep.triggers().enable(iseled_signal_id(IseledSignal::TransferComplete)); + ep.triggers().enable(iseled_signal_id(IseledSignal::NativeCrcError)); + + IseledRequest req; + IseledResponse resp; + resp.address = 0x0010; + resp.data = {0x22}; + resp.native_crc = static_cast(compute_native_crc8(crc_coverage_bytes(resp)) ^ 0x01u); + + auto ec = ep.transact(req, resp); + REQUIRE(ec == make_error_code(IseledErrc::native_crc_mismatch)); + + auto drained = ep.triggers().drain(); + REQUIRE(drained.size() == 1); + REQUIRE(drained[0] == iseled_signal_id(IseledSignal::NativeCrcError)); +} + +// ── Trigger signals ─────────────────────────────────────────────────────────── + +TEST_CASE("IseledEndpoint::transact fires only TransferComplete on a clean transaction", + "[iseled][REQ-ISELED-004]") { + IseledEndpoint ep; + ep.triggers().enable(iseled_signal_id(IseledSignal::TransferComplete)); + ep.triggers().enable(iseled_signal_id(IseledSignal::NativeCrcError)); + + IseledRequest req; + IseledResponse resp; + resp.address = 0x0010; + resp.data = {0x22}; + resp.native_crc = compute_native_crc8(crc_coverage_bytes(resp)); + + REQUIRE_FALSE(ep.transact(req, resp)); + + auto drained = ep.triggers().drain(); + REQUIRE(drained.size() == 1); + REQUIRE(drained[0] == iseled_signal_id(IseledSignal::TransferComplete)); +} + +// ── IseledErrc category sanity ──────────────────────────────────────────────── + +TEST_CASE("IseledErrc reports a non-empty message in its own category", "[iseled][REQ-ISELED-005]") { + auto ec = make_error_code(IseledErrc::native_crc_mismatch); + REQUIRE(ec.category() == iseled_category()); + REQUIRE_FALSE(ec.message().empty()); +} diff --git a/tests/test_lin.cpp b/tests/test_lin.cpp new file mode 100644 index 0000000..5aca766 --- /dev/null +++ b/tests/test_lin.cpp @@ -0,0 +1,82 @@ +// fusa:test REQ-LINEP-001 +// fusa:test REQ-LINEP-002 +// fusa:test REQ-LINEP-003 +// fusa:test REQ-LINEP-004 + +// Tests for rcp/lin.hpp — the LIN commander endpoint type (ROADMAP.md +// milestone 51, "Remaining Endpoint Types — LIN, CAN (incl. CAN XL), +// ISELED, MDIO, Wakeup Control", v2.7.0). + +#include +#include +#include + +using namespace rcp::lin; + +// ── ep_type id ──────────────────────────────────────────────────────────────── + +TEST_CASE("LIN's ep_type id is 0x06", "[lin][REQ-LINEP-001]") { + REQUIRE(rcp::endpoint::kEndpointTypeLin == 0x06); +} + +// ── Raw-byte-pusher model: no frame-level concepts ─────────────────────────── + +TEST_CASE("LinEndpoint::transfer records raw bytes exactly, with no frame interpretation", + "[lin][REQ-LINEP-002]") { + LinEndpoint ep; + // The caller's driver has already assembled break/sync/PID/data/checksum + // into one opaque byte stream; this endpoint does not decompose it. + std::vector out_bytes{0x55, 0x21, 0xAA, 0xBB, 0x74}; + std::vector in_bytes{0xAA, 0xBB, 0x74}; + + auto ec = ep.transfer(out_bytes, in_bytes); + REQUIRE_FALSE(ec); + REQUIRE(ep.last_sent() == out_bytes); + REQUIRE(ep.last_received() == in_bytes); +} + +TEST_CASE("LinEndpoint::transfer treats an empty byte stream as valid (no minimum frame length)", + "[lin][REQ-LINEP-002]") { + LinEndpoint ep; + auto ec = ep.transfer({}, {}); + REQUIRE_FALSE(ec); + REQUIRE(ep.last_sent().empty()); + REQUIRE(ep.last_received().empty()); +} + +// ── No-response error path ─────────────────────────────────────────────────── + +TEST_CASE("LinEndpoint::transfer reports no_response and fires NoResponse when responded=false", + "[lin][REQ-LINEP-003]") { + LinEndpoint ep; + ep.triggers().enable(lin_signal_id(LinSignal::TransferComplete)); + ep.triggers().enable(lin_signal_id(LinSignal::NoResponse)); + + auto ec = ep.transfer({0x55, 0x21}, {}, /*responded=*/false); + REQUIRE(ec == make_error_code(LinErrc::no_response)); + + auto drained = ep.triggers().drain(); + REQUIRE(drained.size() == 1); + REQUIRE(drained[0] == lin_signal_id(LinSignal::NoResponse)); +} + +TEST_CASE("LinEndpoint::transfer fires only TransferComplete on a normal responded transfer", + "[lin][REQ-LINEP-003]") { + LinEndpoint ep; + ep.triggers().enable(lin_signal_id(LinSignal::TransferComplete)); + ep.triggers().enable(lin_signal_id(LinSignal::NoResponse)); + + REQUIRE_FALSE(ep.transfer({0x55}, {0xAA})); + + auto drained = ep.triggers().drain(); + REQUIRE(drained.size() == 1); + REQUIRE(drained[0] == lin_signal_id(LinSignal::TransferComplete)); +} + +// ── LinErrc category sanity ─────────────────────────────────────────────────── + +TEST_CASE("LinErrc reports a non-empty message in its own category", "[lin][REQ-LINEP-004]") { + auto ec = make_error_code(LinErrc::no_response); + REQUIRE(ec.category() == lin_category()); + REQUIRE_FALSE(ec.message().empty()); +} diff --git a/tests/test_mdio.cpp b/tests/test_mdio.cpp new file mode 100644 index 0000000..e3289e9 --- /dev/null +++ b/tests/test_mdio.cpp @@ -0,0 +1,139 @@ +// fusa:test REQ-MDIO-001 +// fusa:test REQ-MDIO-002 +// fusa:test REQ-MDIO-003 +// fusa:test REQ-MDIO-004 +// fusa:test REQ-MDIO-005 + +// Tests for rcp/mdio.hpp — the MDIO endpoint type (ROADMAP.md milestone 51, +// "Remaining Endpoint Types — LIN, CAN (incl. CAN XL), ISELED, MDIO, Wakeup +// Control", v2.7.0). + +#include +#include +#include + +using namespace rcp::mdio; + +// ── ep_type id ──────────────────────────────────────────────────────────────── + +TEST_CASE("MDIO's ep_type id is 0x0D", "[mdio][REQ-MDIO-001]") { + REQUIRE(rcp::endpoint::kEndpointTypeMdio == 0x0D); +} + +// ── Address-range validation ────────────────────────────────────────────────── + +TEST_CASE("validate_request rejects an out-of-range PHY address", "[mdio][REQ-MDIO-002]") { + MdioRequest req; + req.phy_address = kMaxPhyAddress + 1; + REQUIRE(validate_request(req) == make_error_code(MdioErrc::phy_address_out_of_range)); +} + +TEST_CASE("validate_request rejects an out-of-range device/register address", "[mdio][REQ-MDIO-002]") { + MdioRequest req; + req.device_or_reg = kMaxDeviceOrRegField + 1; + REQUIRE(validate_request(req) == make_error_code(MdioErrc::device_or_reg_out_of_range)); +} + +TEST_CASE("validate_request accepts every in-range field", "[mdio][REQ-MDIO-002]") { + MdioRequest req; + req.phy_address = kMaxPhyAddress; + req.device_or_reg = kMaxDeviceOrRegField; + REQUIRE_FALSE(validate_request(req)); +} + +// ── Clause 22: flat register write-then-read round-trip ───────────────────── + +TEST_CASE("MdioEndpoint::handle_request Clause22 write then read round-trips the value", + "[mdio][REQ-MDIO-003]") { + MdioEndpoint ep; + MdioRequest write_req; + write_req.clause = MdioClause::Clause22; + write_req.phy_address = 3; + write_req.device_or_reg = 5; // Clause22 register address + write_req.is_write = true; + write_req.write_value = 0xBEEF; + + MdioResponse out; + REQUIRE_FALSE(ep.handle_request(write_req, out)); + REQUIRE(out.value == 0xBEEF); + + MdioRequest read_req = write_req; + read_req.is_write = false; + read_req.write_value = 0; + REQUIRE_FALSE(ep.handle_request(read_req, out)); + REQUIRE(out.value == 0xBEEF); +} + +TEST_CASE("MdioEndpoint::handle_request reads an unwritten register as zero", "[mdio][REQ-MDIO-003]") { + MdioEndpoint ep; + MdioRequest req; + req.clause = MdioClause::Clause22; + req.phy_address = 1; + req.device_or_reg = 1; + req.is_write = false; + + MdioResponse out; + REQUIRE_FALSE(ep.handle_request(req, out)); + REQUIRE(out.value == 0); +} + +// ── Clause 45: device+register addressing distinct from Clause 22 ─────────── + +TEST_CASE("MdioEndpoint::handle_request Clause45 keys on register_address in addition to " + "device_or_reg, distinct from Clause22's flat address space", + "[mdio][REQ-MDIO-004]") { + MdioEndpoint ep; + + MdioRequest c22_write; + c22_write.clause = MdioClause::Clause22; + c22_write.phy_address = 2; + c22_write.device_or_reg = 4; + c22_write.is_write = true; + c22_write.write_value = 0x1111; + MdioResponse out; + REQUIRE_FALSE(ep.handle_request(c22_write, out)); + + // Same phy_address/device_or_reg bit pattern, but Clause45 with a + // distinct register_address — must not read back Clause22's value. + MdioRequest c45_read; + c45_read.clause = MdioClause::Clause45; + c45_read.phy_address = 2; + c45_read.device_or_reg = 4; + c45_read.register_address = 0x0010; + c45_read.is_write = false; + REQUIRE_FALSE(ep.handle_request(c45_read, out)); + REQUIRE(out.value == 0); + + MdioRequest c45_write = c45_read; + c45_write.is_write = true; + c45_write.write_value = 0x2222; + REQUIRE_FALSE(ep.handle_request(c45_write, out)); + REQUIRE(out.value == 0x2222); + + // Clause22's earlier write is unaffected. + MdioRequest c22_read = c22_write; + c22_read.is_write = false; + REQUIRE_FALSE(ep.handle_request(c22_read, out)); + REQUIRE(out.value == 0x1111); +} + +TEST_CASE("MdioEndpoint::handle_request fires TransferComplete on every request", "[mdio][REQ-MDIO-004]") { + MdioEndpoint ep; + ep.triggers().enable(mdio_signal_id(MdioSignal::TransferComplete)); + + MdioRequest req; + MdioResponse out; + REQUIRE_FALSE(ep.handle_request(req, out)); + + auto drained = ep.triggers().drain(); + REQUIRE(drained.size() == 1); + REQUIRE(drained[0] == mdio_signal_id(MdioSignal::TransferComplete)); +} + +// ── MdioErrc category sanity ─────────────────────────────────────────────────── + +TEST_CASE("MdioErrc reports a non-empty message in its own category", "[mdio][REQ-MDIO-005]") { + auto ec = make_error_code(MdioErrc::phy_address_out_of_range); + REQUIRE(ec.category() == mdio_category()); + REQUIRE_FALSE(ec.message().empty()); +} diff --git a/tests/test_wakeup.cpp b/tests/test_wakeup.cpp new file mode 100644 index 0000000..b04426b --- /dev/null +++ b/tests/test_wakeup.cpp @@ -0,0 +1,109 @@ +// fusa:test REQ-WAKEUP-001 +// fusa:test REQ-WAKEUP-002 +// fusa:test REQ-WAKEUP-003 +// fusa:test REQ-WAKEUP-004 +// fusa:test REQ-WAKEUP-005 + +// 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). + +#include +#include +#include + +using namespace rcp::wakeup; + +// ── ep_type id ──────────────────────────────────────────────────────────────── + +TEST_CASE("Wakeup control's ep_type id is 0x01", "[wakeup][REQ-WAKEUP-001]") { + REQUIRE(rcp::endpoint::kEndpointTypeWakeup == 0x01); +} + +// ── SleepCMD: a fixed opcode, not a RequestTypeOpcode ──────────────────────── + +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("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("WakeupEndpoint::handle_sleep_cmd transitions to asleep on the fixed opcode", + "[wakeup][REQ-WAKEUP-002]") { + WakeupEndpoint ep; + REQUIRE_FALSE(ep.is_asleep()); + REQUIRE_FALSE(ep.handle_sleep_cmd(kSleepCmd)); + REQUIRE(ep.is_asleep()); +} + +TEST_CASE("WakeupEndpoint::handle_sleep_cmd rejects a non-SleepCMD byte without changing state", + "[wakeup][REQ-WAKEUP-002]") { + WakeupEndpoint ep; + auto ec = ep.handle_sleep_cmd(0x5A); + REQUIRE(ec == make_error_code(WakeupErrc::not_sleep_cmd)); + 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]") { + WakeupEndpoint ep; + REQUIRE_FALSE(ep.handle_sleep_cmd(kSleepCmd)); + REQUIRE(ep.is_asleep()); + + ep.record_wake_source_event(3); + REQUIRE_FALSE(ep.is_asleep()); + REQUIRE(ep.wake_source_pins() == (WakeSourceMask{1} << 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]") { + 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()); + + 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]") { + WakeupEndpoint ep; + ep.record_wake_source_event(2); + REQUIRE(ep.wakeup_message_pending()); + + REQUIRE_FALSE(ep.handle_sleep_cmd(kSleepCmd)); + 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); + REQUIRE(ec.category() == wakeup_category()); + REQUIRE_FALSE(ec.message().empty()); +}