diff --git a/.fusa-reqs.json b/.fusa-reqs.json index f32143f..e94e358 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -82,8 +82,8 @@ }, { "id": "REQ-MOCK-001", - "title": "Server starts HW_UNCONFIGURED with a representative seven-endpoint register map", - "text": "rcp::mock::Server shall construct with rcp::lifecycle::ServerLifecycle in state HwUnconfigured and an rcp::regmap::RegisterMap whose endpoint_count, generic_configs, functional_configs, and ep_id_mapping describe exactly seven endpoints: GPIO at endpoint id/byte_bus_id 1, SPI at endpoint id/byte_bus_id 2, I2C at endpoint id/byte_bus_id 3, ADC at endpoint id/byte_bus_id 4, PWM_IN at endpoint id/byte_bus_id 5, LIN at endpoint id/byte_bus_id 6, and CAN at endpoint id/byte_bus_id 7.", + "title": "Server starts HW_UNCONFIGURED with a representative eight-endpoint register map", + "text": "rcp::mock::Server shall construct with rcp::lifecycle::ServerLifecycle in state HwUnconfigured and an rcp::regmap::RegisterMap whose endpoint_count, generic_configs, functional_configs, and ep_id_mapping describe exactly eight endpoints: GPIO at endpoint id/byte_bus_id 1, SPI at endpoint id/byte_bus_id 2, I2C at endpoint id/byte_bus_id 3, ADC at endpoint id/byte_bus_id 4, PWM_IN at endpoint id/byte_bus_id 5, LIN at endpoint id/byte_bus_id 6, CAN at endpoint id/byte_bus_id 7, and UART at endpoint id/byte_bus_id 8.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -250,6 +250,24 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-MOCK-021", + "title": "UART dispatch applies Table 33 Row 2 evt[2:0] classification before touching either queue, routing Plain requests on req.op", + "text": "rcp::mock::Server::dispatch, for a request addressed to the UART endpoint while RcpConfigured, shall decode the request's evt[2:0] field via endpoint::evt_row2_kind_of and drive uart::UartEndpoint::handle_request with it: a Plain (000b) write request shall enqueue the request payload onto the TX queue and be answered as a WriteResponse with an empty byte_msg_payload, a Plain (000b) read request shall drain the RX FIFO (up to the request's read_size, non-blocking) and be answered as a ReadResponse carrying the drained bytes, a Reserved (001b-110b) evt shall be rejected with endpoint::EndpointErrc::reserved_evt_row2 (wire error code UNSUPPORTED_CMD) without touching either queue, and a ConfigWrite (111b) evt shall be rejected with uart::UartErrc::config_write_not_supported (wire error code UNSUPPORTED_CMD) rather than crashing or being treated as a plain TX/RX operation.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.5 Table 33 (\"ADC, PWM_IN, I²C, LIN, CAN, UART, ISELED, MDIO: 000b to 110b reserved – request to be rejected with error code = UNSUPPORTED_CMD\" / \"111b: The byte_msg_payload is not presented to the interface but used to change the configuration of the endpoint (see 12.7.1)\"), TC18.txt L4085-4092; corroborating worked example resolving the evt=000b ambiguity in Table 33's own favor: §13.7.9.3 Figure 34 (\"RC Client sends a standard read request\", ADC endpoint, evt field all-zero), TC18.txt L5590-5607", + "tc18_master_id": "TC18-13.5-001" + }, + { + "id": "REQ-MOCK-022", + "title": "Operational UART requests are rejected before RCP_CONFIGURED, same gating as GPIO/SPI/I2C/ADC/PWM_IN/LIN/CAN", + "text": "rcp::mock::Server::dispatch, for a request addressed to the UART endpoint while the server's lifecycle state is not RcpConfigured, shall return regmap::RegMapErrc::request_rejected and an ErrorResponse without touching the endpoint's live state, the same operational-request gating REQ-MOCK-006/REQ-MOCK-012/REQ-MOCK-014/REQ-MOCK-016/REQ-MOCK-018/REQ-MOCK-020 already apply to GPIO/SPI/I2C/ADC/PWM_IN/LIN/CAN.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B" + }, { "id": "REQ-UDP-001", "title": "Frame round-trips an NTSCF-framed ACF_ABB request", @@ -2863,6 +2881,27 @@ "level": "ASIL-B", "asil": "ASIL-B" }, + { + "id": "REQ-UART-008", + "title": "UartEndpoint::handle_request applies Table 33 Row 2 evt[2:0] classification before routing on req.op to enqueue_tx or handle_read", + "text": "rcp::uart::UartEndpoint::handle_request shall classify its evt_op argument via rcp::endpoint::evt_row2_kind_of (masking to the low 3 bits first) and, for EvtRow2Kind::Plain, shall route on its is_write argument: true delegates to enqueue_tx(tx_bytes) with the TX queue unchanged from that function's own behavior, false delegates to handle_read(read_size, elapsed_ms, uart_timeout_ms, out_data, out_timed_out) unchanged; for EvtRow2Kind::Reserved, shall return endpoint::EndpointErrc::reserved_evt_row2 without invoking enqueue_tx or handle_read and without touching the TX queue or RX FIFO.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.5 Table 33 (\"ADC, PWM_IN, I²C, LIN, CAN, UART, ISELED, MDIO: 000b to 110b reserved – request to be rejected with error code = UNSUPPORTED_CMD\"), TC18.txt L4085-4092; corroborating worked example resolving the evt=000b ambiguity in Table 33's own favor: §13.7.9.3 Figure 34 (\"RC Client sends a standard read request\", ADC endpoint, evt field all-zero), TC18.txt L5590-5607", + "tc18_master_id": "TC18-13.5-001", + "severity": "cybersecurity" + }, + { + "id": "REQ-UART-009", + "title": "UartEndpoint::handle_request reports config-write (evt[2:0]==111b) as not yet supported", + "text": "rcp::uart::UartEndpoint::handle_request, for EvtRow2Kind::ConfigWrite (evt[2:0]==111b), shall return UartErrc::config_write_not_supported without invoking enqueue_tx or handle_read, without touching the TX queue or RX FIFO, and without crashing, rather than silently accepting the request as an ordinary TX/RX operation or silently doing nothing; full §12.7.1 configuration-write handling is deliberately deferred to a follow-up milestone, the same scope decision I2C's I2cErrc::config_write_not_supported, ADC's AdcErrc::config_write_not_supported, PWM_IN's PwmErrc::config_write_not_supported, LIN's LinErrc::config_write_not_supported, and CAN's CanErrc::config_write_not_supported already made. UartErrc::config_write_not_supported shall report a non-empty diagnostic message in its own error_category.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§12.7.1 (\"This can be done by setting evt[2:0] = 111b in request as described above.\"; Figure 19 – \"Configuration request\"; \"The data of the byte_msg_payload from a write request is written into the section of the EP that is addressed by the byte_bus_id.\"), TC18.txt L2844-2865", + "tc18_master_id": "TC18-12.7.1-001" + }, { "id": "REQ-ADC-001", "title": "ADC arithmetic-mean averaging combinator", diff --git a/include/rcp/mock.hpp b/include/rcp/mock.hpp index f1e1cd2..914fcfc 100644 --- a/include/rcp/mock.hpp +++ b/include/rcp/mock.hpp @@ -18,6 +18,8 @@ // fusa:req REQ-MOCK-018 // fusa:req REQ-MOCK-019 // fusa:req REQ-MOCK-020 +// fusa:req REQ-MOCK-021 +// fusa:req REQ-MOCK-022 // In-process RC Server simulator — a small, representative OPEN Alliance // TC18 Remote Control Protocol Specification v0.5.1_RC server built @@ -43,13 +45,16 @@ // mock::Server holds a real rcp::lifecycle::ServerLifecycle (v2.1.0), a // real rcp::regmap::RegisterMap plus rcp::regmap::Ep0 (v2.1.0, including // EP0 whole-map-read and root-client write semantics), and one instance -// each of seven fully-built endpoint types — rcp::gpio::GpioEndpoint and +// each of eight fully-built endpoint types — rcp::gpio::GpioEndpoint and // rcp::spi::SpiEndpoint (both v2.3.0), plus rcp::i2c::I2cEndpoint, -// rcp::adc::AdcEndpoint, rcp::pwm::PwmInEndpoint, rcp::lin::LinEndpoint, and -// rcp::can::CanEndpoint (v2.4.0/post-v2.7.0, wired in by the Table 30/33 -// Row 2 evt[2:0] validation pilot and its ADC, PWM_IN, LIN, and CAN -// follow-ups, in that order) — as its representative endpoint set. -// dispatch() below is the single +// rcp::adc::AdcEndpoint, rcp::pwm::PwmInEndpoint, rcp::lin::LinEndpoint, +// rcp::can::CanEndpoint, and rcp::uart::UartEndpoint (v2.4.0/post-v2.7.0, +// wired in by the Table 30/33 Row 2 evt[2:0] validation pilot and its ADC, +// PWM_IN, LIN, CAN, and UART follow-ups, in that order — UART's is this +// header's FIRST wiring of rcp::uart::UartEndpoint at all, not merely an +// extension of a pre-existing dispatch_uart(); see dispatch_uart's own +// comment for why UART needed its own req.op-branching shape) — as its +// representative endpoint set. dispatch() below is the single // request/response entry point a test drives, decoding the standard // request kind's evt[2:0]/op fields (rcp/acf.hpp, v2.0.0) the same way a // real request-dispatch loop would. Conditional request kinds (v2.5.0), @@ -70,14 +75,15 @@ // in an internal structured extraction of the specification named above; // no text from that document is reproduced here. The concrete endpoint // numbering (GPIO at endpoint id / byte_bus_id 1, SPI at 2, I2C at 3, ADC at -// 4, PWM_IN at 5, LIN at 6, CAN at 7), access-policy choice for operational -// requests (gated on lifecycle state only, not per-endpoint ownership — see -// dispatch()'s own comment), and EP0 partial-read encoding chosen in this -// file are this implementation's own, purely for the purposes of being a -// usable in-process simulator — full bit-for-bit conformance against other -// TC18 implementations is not claimed, same as the equivalent disclaimers -// in rcp/regmap.hpp, rcp/lifecycle.hpp, rcp/gpio.hpp, rcp/spi.hpp, -// rcp/i2c.hpp, rcp/adc.hpp, rcp/pwm.hpp, rcp/lin.hpp, and rcp/can.hpp. +// 4, PWM_IN at 5, LIN at 6, CAN at 7, UART at 8), access-policy choice for +// operational requests (gated on lifecycle state only, not per-endpoint +// ownership — see dispatch()'s own comment), and EP0 partial-read encoding +// chosen in this file are this implementation's own, purely for the +// purposes of being a usable in-process simulator — full bit-for-bit +// conformance against other TC18 implementations is not claimed, same as +// the equivalent disclaimers in rcp/regmap.hpp, rcp/lifecycle.hpp, +// rcp/gpio.hpp, rcp/spi.hpp, rcp/i2c.hpp, rcp/adc.hpp, rcp/pwm.hpp, +// rcp/lin.hpp, rcp/can.hpp, and rcp/uart.hpp. #pragma once #include @@ -92,6 +98,7 @@ #include #include #include +#include #include #include @@ -121,6 +128,7 @@ constexpr EndpointId kAdcEndpointId = 4; constexpr EndpointId kPwmInEndpointId = 5; constexpr EndpointId kLinEndpointId = 6; constexpr EndpointId kCanEndpointId = 7; +constexpr EndpointId kUartEndpointId = 8; constexpr avtp::ByteBusId kGpioByteBusId = static_cast(kGpioEndpointId); constexpr avtp::ByteBusId kSpiByteBusId = static_cast(kSpiEndpointId); constexpr avtp::ByteBusId kI2cByteBusId = static_cast(kI2cEndpointId); @@ -128,6 +136,7 @@ constexpr avtp::ByteBusId kAdcByteBusId = static_cast(kAdcEnd constexpr avtp::ByteBusId kPwmInByteBusId = static_cast(kPwmInEndpointId); constexpr avtp::ByteBusId kLinByteBusId = static_cast(kLinEndpointId); constexpr avtp::ByteBusId kCanByteBusId = static_cast(kCanEndpointId); +constexpr avtp::ByteBusId kUartByteBusId = static_cast(kUartEndpointId); // A discovery-shaped EP0 read only ever answers with the register map's // magic number below — see this header's own scope note above. @@ -178,6 +187,12 @@ inline acf::WireErrorCode wire_error_code_for(const std::error_code& ec) noexcep return acf::WireErrorCode::InvalidParameter; // frame payload exceeds the selected FrameFormat's own ceiling if (ec == make_error_code(can::CanErrc::xl_payload_exceeds_single_avtpdu_bound)) return acf::WireErrorCode::EpError; // internal single-AVTPDU capability bound, not a TC18-defined CAN error code — mirrors i2c::I2cErrc::nack's mapping above + if (ec == make_error_code(uart::UartErrc::config_write_not_supported)) + return acf::WireErrorCode::UnsupportedCmd; // evt[2:0]==111b config-write, not yet implemented by this mock + if (ec == make_error_code(uart::UartErrc::read_size_exceeds_bound)) + return acf::WireErrorCode::InvalidParameter; // requested read_size exceeds kMaxReadSize — client-caused, mirrors avtp::short_buffer's mapping above + if (ec == make_error_code(uart::UartErrc::tx_queue_overflow)) + return acf::WireErrorCode::InvalidParameter; // write payload would overflow the TX queue — client-caused, same rationale as read_size_exceeds_bound above return acf::WireErrorCode::UnsupportedCmd; } @@ -211,6 +226,7 @@ class Server final { pwm::PwmInEndpoint& pwm_in() noexcept { return pwm_in_; } lin::LinEndpoint& lin() noexcept { return lin_; } can::CanEndpoint& can() noexcept { return can_; } + uart::UartEndpoint& uart() noexcept { return uart_; } // set_spi_poci scripts the bytes a subsequent dispatch()/transfer() // call on `channel` reads back as POCI-in data. A real SPI peripheral's @@ -295,6 +311,17 @@ class Server final { // for any test that wants to inspect last_transmitted()/ // last_received() or drive receive()/acceptance filters itself. + // No set_uart_response()-shaped hook here either, same rationale as + // set_can_response's own note directly above: UART's RX side (extraction + // §13.7.8.1) is fed by rx_fill() pushing bytes as if they had arrived + // off the wire, not by scripting a one-shot response value the way + // set_i2c_response/set_lin_response do for their own bus-transfer + // models. uart() above already exposes UartEndpoint directly for any + // test that wants to call rx_fill()/drain_tx() or inspect + // rx_available() itself — see dispatch_uart's own comment for how a + // dispatched read request drains whatever rx_fill() has already put + // there. + // advance_to_rcp_configured is a convenience for tests/simulators that // don't care about exercising ServerLifecycle's intermediate // plausibility-check gating themselves and just want a fully live @@ -335,6 +362,7 @@ class Server final { if (req.byte_bus_id == kPwmInByteBusId) return dispatch_pwm_in(req, req_payload, out_resp, out_resp_payload); if (req.byte_bus_id == kLinByteBusId) return dispatch_lin(req, req_payload, out_resp, out_resp_payload); if (req.byte_bus_id == kCanByteBusId) return dispatch_can(req, req_payload, out_resp, out_resp_payload); + if (req.byte_bus_id == kUartByteBusId) return dispatch_uart(req, req_payload, out_resp, out_resp_payload); return set_error_response(req, make_error_code(regmap::RegMapErrc::invalid_parameter), out_resp, out_resp_payload); @@ -355,9 +383,9 @@ class Server final { static regmap::RegisterMap make_initial_register_map() { regmap::RegisterMap regs; - regs.endpoint_count = 7; - regs.generic_configs.resize(7); - regs.functional_configs.resize(7); + regs.endpoint_count = 8; + regs.generic_configs.resize(8); + regs.functional_configs.resize(8); regs.ep_id_mapping = { {kGpioEndpointId, kGpioByteBusId}, {kSpiEndpointId, kSpiByteBusId}, @@ -366,6 +394,7 @@ class Server final { {kPwmInEndpointId, kPwmInByteBusId}, {kLinEndpointId, kLinByteBusId}, {kCanEndpointId, kCanByteBusId}, + {kUartEndpointId, kUartByteBusId}, }; return regs; } @@ -598,6 +627,65 @@ class Server final { return {}; } + // UART, unlike every other dispatch_*() above, drives a + // request-shape-dependent operation on UartEndpoint: this is the first + // time this file wires UartEndpoint in at all (see this header's own + // top comment). dispatch_gpio above is the precedent this path + // follows for branching on req.op — read (op==false) vs write + // (op==true) select genuinely different UART operations (extraction + // §13.7.8.1's independent TX/RX request storages), not a single + // full-duplex/raw-transfer call the way SPI/I2C/LIN's dispatch paths + // stay op-agnostic. Both branches funnel through the same + // UartEndpoint::handle_request choke point, so Table 33 Row 2's 3-way + // Plain/Reserved/ConfigWrite classification (rcp::endpoint:: + // evt_row2_kind_of) is checked once, before `payload` is ever read as + // TX bytes or `req.read_size_or_segment_num` is ever read as a read + // request's read_size — see handle_request's own doc comment for why + // a Reserved or ConfigWrite evt must never reach either queue. + // + // This mock has no clock of its own (same disclaimer every other + // endpoint type in this file carries), so a dispatched read request is + // always answered synchronously against whatever is currently buffered + // in the RX FIFO: elapsed_ms and uart_timeout_ms are both passed as 0, + // which handle_read's own "elapsed_ms >= uart_timeout_ms" completion + // rule treats as an immediately-elapsed timeout — the read never + // blocks, it simply drains up to read_size bytes right now, the same + // "test scripts the buffer via rx_fill(), this mock does not model + // actual timing" approach set_i2c_response/set_lin_response take for + // their own bus-transfer models (see uart()'s own comment above for + // why there is no set_uart_response() hook instead). A write request's + // successful response carries no payload (out_resp_payload stays + // empty), mirroring dispatch_gpio's write-response shape; a read + // request's successful response carries whatever handle_read drained, + // mirroring dispatch_i2c/dispatch_lin's read-response shape. The + // §13.7.8.3 payload-less-read-only rule ("A read request having a + // byte_msg_payload will be rejected with error code = UNKNOWN_CMD") is + // not enforced here — deliberately out of scope for this evt[2:0] + // classification pass, same as handle_request's own comment already + // flags. + std::error_code dispatch_uart(const acf::AcfMessageInfo& req, const std::vector& payload, + acf::AcfMessageInfo& out_resp, + std::vector& out_resp_payload) noexcept { + if (!operational_requests_allowed()) { + return set_error_response(req, make_error_code(regmap::RegMapErrc::request_rejected), + out_resp, out_resp_payload); + } + std::vector data; + bool timed_out = false; + auto ec = uart_.handle_request(req.evt_op, req.op, payload, req.read_size_or_segment_num, + /*elapsed_ms=*/0, /*uart_timeout_ms=*/0, data, timed_out); + if (ec) return set_error_response(req, ec, out_resp, out_resp_payload); + if (req.op) { + out_resp = acf::make_response(req, req.evt_ack ? acf::ResponseKind::Acknowledge + : acf::ResponseKind::WriteResponse); + } else { + out_resp_payload = std::move(data); + out_resp = acf::make_response(req, req.evt_ack ? acf::ResponseKind::Acknowledge + : acf::ResponseKind::ReadResponse); + } + return {}; + } + lifecycle::ServerLifecycle lifecycle_; regmap::RegisterMap regs_; regmap::Ep0 ep0_; @@ -608,6 +696,7 @@ class Server final { pwm::PwmInEndpoint pwm_in_; lin::LinEndpoint lin_; can::CanEndpoint can_; + uart::UartEndpoint uart_; std::array, spi::kMaxChannels> spi_poci_{}; std::vector i2c_response_{}; bool i2c_acked_ = true; diff --git a/include/rcp/uart.hpp b/include/rcp/uart.hpp index 5aa6208..845ce96 100644 --- a/include/rcp/uart.hpp +++ b/include/rcp/uart.hpp @@ -5,6 +5,8 @@ // fusa:req REQ-UART-005 // fusa:req REQ-UART-006 // fusa:req REQ-UART-007 +// fusa:req REQ-UART-008 +// fusa:req REQ-UART-009 // UART endpoint (ep_type 0x05) — independent TX/RX queues, RX FIFO // fill/drain semantics, read completion on either a configured `read_size` @@ -14,9 +16,26 @@ // ROADMAP.md milestone 48, "Basic Endpoint Types II — I2C, UART, ADC, // PWM_OUT, PWM_IN (v2.4.0)": UART follows the request-dispatch shape // rcp/gpio.hpp and rcp/spi.hpp establish, but — unlike either — has no -// evt[2:0]/channel decode of its own; TX and RX are independent queues -// addressed by separate calls (enqueue_tx/handle_read below) rather than by -// a shared selector field. +// evt[2:0]/channel *selector* of its own the way SPI's evt[2:0] picks a +// channel; TX and RX are independent queues addressed by separate calls +// (enqueue_tx/handle_read below) rather than by a shared selector field. +// +// CORRECTION (Table 30/33 Row 2 evt[2:0] validation, sixth endpoint type +// after I2C, ADC, PWM_IN, LIN, and CAN): the "no evt[2:0] decode of its +// own" claim above described only the *absence of a channel/value +// selector*; it did not mean UART is exempt from Table 33's shared +// Plain/Reserved/ConfigWrite classification. Table 33's own text lists +// UART explicitly, by name, in its second row alongside ADC, PWM_IN, I2C, +// LIN, CAN, ISELED, and MDIO (extraction §13.5, TC18.txt L4085-4092) — the +// exact same row every other endpoint type in that list has already had +// this classification wired in for. handle_request below is that wiring +// for UART, following the exact shape rcp/i2c.hpp's I2cEndpoint:: +// handle_request, rcp/adc.hpp's AdcEndpoint::handle_request, rcp/pwm.hpp's +// PwmInEndpoint::handle_request, rcp/lin.hpp's LinEndpoint::handle_request, +// and rcp/can.hpp's CanEndpoint::handle_request established, adapted for +// UART's own two-entry-point (TX/RX) shape — see handle_request's own +// comment for why that adaptation is needed and how it stays exactly one +// choke point rather than two independent, unvalidated ones. // // ACCEPTED LIMITATION, documented explicitly per the roadmap rather than // left implicit: ROADMAP.md milestone 52 ("Fragmentation — Go/No-Go @@ -37,9 +56,12 @@ // text from that document is reproduced here. The concrete queue-capacity // values and sub-octet padding convention chosen in this file are this // implementation's own, same as the equivalent disclaimers in rcp/avtp.hpp, -// rcp/regmap.hpp, rcp/endpoint.hpp, rcp/gpio.hpp, and rcp/spi.hpp. +// rcp/regmap.hpp, rcp/endpoint.hpp, rcp/gpio.hpp, rcp/spi.hpp, rcp/i2c.hpp, +// rcp/adc.hpp, rcp/pwm.hpp, rcp/lin.hpp, and rcp/can.hpp. #pragma once +#include + #include #include #include @@ -76,6 +98,14 @@ enum class UartErrc : int { rx_fifo_overflow = 2, // rx_fill would push the RX FIFO past kRxFifoCapacity tx_queue_overflow = 3, // enqueue_tx would push the TX queue past kTxQueueCapacity bits_per_frame_out_of_range = 4, // bits_per_frame outside [kMinBitsPerFrame, kMaxBitsPerFrame] + // 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 UartEndpoint::handle_request's + // own comment. Reported explicitly rather than silently accepted as a + // plain TX/RX operation or silently ignored, same as I2C's, ADC's, + // PWM_IN's, LIN's, and CAN's own config_write_not_supported variants. + config_write_not_supported = 5, }; inline const std::error_category& uart_category() noexcept { @@ -91,6 +121,8 @@ inline const std::error_category& uart_category() noexcept { return "rcp/uart: TX queue overflow"; case UartErrc::bits_per_frame_out_of_range: return "rcp/uart: bits_per_frame out of accepted range"; + case UartErrc::config_write_not_supported: + return "rcp/uart: evt[2:0]=111b configuration-write requests are not yet implemented"; default: return "rcp/uart: unknown error"; } @@ -189,6 +221,78 @@ class UartEndpoint { return out; } + // handle_request is UART's single request-decode entry point — the + // piece this header previously had none of, mirroring rcp::i2c:: + // I2cEndpoint::handle_request's shape (this repo's sixth Table 33 Row 2 + // endpoint type after I2C, ADC, PWM_IN, LIN, and CAN) with one + // necessary adaptation: every other Row 2 endpoint type funnels its + // Plain request into a single unified transfer()/transmit()/ + // request_reading() call, but UART's TX and RX are genuinely + // independent operations reached via two different existing entry + // points (enqueue_tx, handle_read — extraction §13.7.8.1's "these two + // processes are independent from each other"). Rather than leave two + // separate, individually-unvalidated request-decode entry points (which + // would let a Reserved or ConfigWrite evt reach either one directly), + // handle_request classifies evt[2:0] via rcp::endpoint:: + // evt_row2_kind_of exactly once and then routes on `is_write` (the + // caller's own req.op — this header has no AcfMessageInfo of its own to + // read it from, same reason every out-parameter below is passed + // explicitly rather than pulled from a wire type): + // - Plain (evt[2:0] == 000b) + is_write: delegates straight to + // enqueue_tx(tx_bytes), unchanged — UART's existing TX-queue model + // already IS this row's correct "plain write request" behavior. + // out_data/out_timed_out are left exactly as the caller passed + // them: a write request produces no read data. + // - Plain (evt[2:0] == 000b) + !is_write: delegates straight to + // handle_read(read_size, elapsed_ms, uart_timeout_ms, out_data, + // out_timed_out), unchanged — UART's existing read-completion rule + // already IS this row's correct "plain read request" behavior. + // - Reserved (evt[2:0] in 001b-110b): returns + // endpoint::EndpointErrc::reserved_evt_row2 without touching either + // queue (neither enqueue_tx nor handle_read/rx_fifo_ is invoked) — + // TC18 requires this be rejected with error code UNSUPPORTED_CMD. + // - ConfigWrite (evt[2:0] == 111b): §12.7.1's configuration-write + // shape targets the UART EP's own functional-config block (Table + // 51 — baud rate, parity, stop bits, uart_timeout itself, ...), not + // a TX/RX operation 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, ADC's, PWM_IN's, LIN's, and CAN's own + // handle_request comments defer for the identical reason); this + // returns UartErrc::config_write_not_supported rather than + // crashing, silently accepting the request as a TX/RX operation, + // or silently doing nothing. + // + // NOT to be confused with §13.7.8.3's own, entirely separate rules — + // "A read request having a byte_msg_payload will be rejected with + // error code = UNKNOWN_CMD" (the payload-less-read-only rule) and the + // read_size-reached-vs-uart_timeout-elapsed race handle_read already + // implements. Both operate only once a request has already been + // classified Plain by the switch above; neither is folded into + // evt[2:0] decoding here, and reading evt[2:0] as if it also gated or + // combined with either would be exactly the kind of invented, + // non-spec-derived encoding this codebase has had to remove elsewhere + // once discovered (e.g. rcp/iseled.hpp's and rcp/mdio.hpp's own header + // comments, and rcp/lin.hpp's/rcp/can.hpp's own handle_request comments + // on the identical class of mistake for their own endpoint types). The + // payload-less-read-only rule itself remains unimplemented by this + // header — called out explicitly here rather than silently conflated + // with Table 33 classification or silently assumed. + std::error_code handle_request(uint8_t evt_op, bool is_write, const std::vector& tx_bytes, + uint16_t read_size, uint32_t elapsed_ms, uint32_t uart_timeout_ms, + std::vector& out_data, bool& out_timed_out) { + switch (endpoint::evt_row2_kind_of(evt_op)) { + case endpoint::EvtRow2Kind::Plain: + if (is_write) return enqueue_tx(tx_bytes); + return handle_read(read_size, elapsed_ms, uart_timeout_ms, out_data, out_timed_out); + case endpoint::EvtRow2Kind::Reserved: + return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); + case endpoint::EvtRow2Kind::ConfigWrite: + return make_error_code(UartErrc::config_write_not_supported); + } + return endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2); // unreachable + } + private: std::deque tx_queue_; std::deque rx_fifo_; diff --git a/tests/test_mock.cpp b/tests/test_mock.cpp index c0771bd..dbcf87c 100644 --- a/tests/test_mock.cpp +++ b/tests/test_mock.cpp @@ -18,6 +18,8 @@ // fusa:test REQ-MOCK-018 // fusa:test REQ-MOCK-019 // fusa:test REQ-MOCK-020 +// fusa:test REQ-MOCK-021 +// fusa:test REQ-MOCK-022 // Tests for rcp/mock.hpp — the in-process RC Server simulator (ROADMAP.md // milestone 56, "Test & Simulation Harness Rebuild", v2.12.0). See @@ -44,14 +46,14 @@ acf::AcfMessageInfo standard_request(avtp::ByteBusId bus_id, bool write, uint8_t // ── Construction / register map ────────────────────────────────────────────── -TEST_CASE("Server starts HW_UNCONFIGURED with a seven-endpoint register map", +TEST_CASE("Server starts HW_UNCONFIGURED with an eight-endpoint register map", "[mock][REQ-MOCK-001]") { mock::Server server; REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); - REQUIRE(server.registers().endpoint_count == 7); - REQUIRE(server.registers().generic_configs.size() == 7); - REQUIRE(server.registers().functional_configs.size() == 7); - REQUIRE(server.registers().ep_id_mapping.size() == 7); + REQUIRE(server.registers().endpoint_count == 8); + REQUIRE(server.registers().generic_configs.size() == 8); + REQUIRE(server.registers().functional_configs.size() == 8); + REQUIRE(server.registers().ep_id_mapping.size() == 8); REQUIRE(server.registers().ep_id_mapping[0].ep_id == mock::kGpioEndpointId); REQUIRE(server.registers().ep_id_mapping[0].byte_bus_id == mock::kGpioByteBusId); REQUIRE(server.registers().ep_id_mapping[1].ep_id == mock::kSpiEndpointId); @@ -66,6 +68,8 @@ TEST_CASE("Server starts HW_UNCONFIGURED with a seven-endpoint register map", REQUIRE(server.registers().ep_id_mapping[5].byte_bus_id == mock::kLinByteBusId); REQUIRE(server.registers().ep_id_mapping[6].ep_id == mock::kCanEndpointId); REQUIRE(server.registers().ep_id_mapping[6].byte_bus_id == mock::kCanByteBusId); + REQUIRE(server.registers().ep_id_mapping[7].ep_id == mock::kUartEndpointId); + REQUIRE(server.registers().ep_id_mapping[7].byte_bus_id == mock::kUartByteBusId); } TEST_CASE("advance_to_rcp_configured drives the lifecycle straight to RCP_CONFIGURED", @@ -628,6 +632,123 @@ TEST_CASE("CAN request is rejected before RCP_CONFIGURED, same operational gatin REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); } +// ── UART ────────────────────────────────────────────────────────────────────── +// Table 30/33 Row 2 evt[2:0] validation, sixth endpoint type after I2C, ADC, +// PWM_IN, LIN, and CAN: UartEndpoint wired into dispatch() at byte_bus_id 8 +// for the FIRST time (this file had zero UART wiring before this pass — see +// mock.hpp's own header comment and dispatch_uart's own comment). Unlike +// every other Row 2 endpoint type's single unified transfer()/transmit() +// call, UART routes a Plain request on req.op — write enqueues onto the TX +// queue, read drains the RX FIFO (see dispatch_uart's own comment for why +// there is no set_uart_response() hook and why elapsed_ms/uart_timeout_ms +// are both hardcoded to 0 instead). + +TEST_CASE("UART plain write request (evt[2:0]==000b) enqueues the payload onto the TX queue and " + "answers WriteResponse with an empty payload", + "[mock][REQ-MOCK-021]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + auto req = standard_request(mock::kUartByteBusId, /*write=*/true, /*evt_op=*/0); + std::vector data{0xDE, 0xAD, 0xBE, 0xEF}; + acf::AcfMessageInfo resp; + std::vector resp_payload; + REQUIRE_FALSE(server.dispatch(0, req, data, resp, resp_payload)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::WriteResponse); + REQUIRE(resp_payload.empty()); + REQUIRE(server.uart().drain_tx() == data); +} + +TEST_CASE("UART plain read request (evt[2:0]==000b) drains the RX FIFO and answers ReadResponse " + "with the drained bytes", + "[mock][REQ-MOCK-021]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + REQUIRE_FALSE(server.uart().rx_fill({0x01, 0x02, 0x03})); + + auto req = standard_request(mock::kUartByteBusId, /*write=*/false, /*evt_op=*/0); + req.read_size_or_segment_num = 3; + acf::AcfMessageInfo resp; + std::vector resp_payload; + REQUIRE_FALSE(server.dispatch(0, req, {}, resp, resp_payload)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ReadResponse); + REQUIRE(resp_payload == std::vector{0x01, 0x02, 0x03}); + REQUIRE(server.uart().rx_available() == 0); +} + +TEST_CASE("UART write request with a reserved evt[2:0] (001b-110b) is rejected with wire error " + "code UNSUPPORTED_CMD and does not touch the TX queue", + "[mock][REQ-MOCK-021]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { + auto req = standard_request(mock::kUartByteBusId, /*write=*/true, evt_op); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {0xAA}, resp, resp_payload); + REQUIRE(ec == endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); + REQUIRE(resp_payload == + std::vector{static_cast(acf::WireErrorCode::UnsupportedCmd)}); + REQUIRE(server.uart().drain_tx().empty()); + } +} + +TEST_CASE("UART read request with a reserved evt[2:0] (001b-110b) is rejected with wire error " + "code UNSUPPORTED_CMD and does not touch the RX FIFO", + "[mock][REQ-MOCK-021]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + REQUIRE_FALSE(server.uart().rx_fill({0x01, 0x02})); + + for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { + auto req = standard_request(mock::kUartByteBusId, /*write=*/false, evt_op); + req.read_size_or_segment_num = 2; + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {}, resp, resp_payload); + REQUIRE(ec == endpoint::make_error_code(endpoint::EndpointErrc::reserved_evt_row2)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); + REQUIRE(resp_payload == + std::vector{static_cast(acf::WireErrorCode::UnsupportedCmd)}); + // A rejected reserved evt must not touch the RX FIFO — the bytes + // scripted above are still fully buffered. + REQUIRE(server.uart().rx_available() == 2); + } +} + +TEST_CASE("UART request with evt[2:0]==111b (config-write) is rejected with wire error code " + "UNSUPPORTED_CMD rather than crashing or being treated as a plain TX/RX operation", + "[mock][REQ-MOCK-021]") { + mock::Server server; + REQUIRE_FALSE(server.advance_to_rcp_configured()); + + auto req = standard_request(mock::kUartByteBusId, /*write=*/true, /*evt_op=*/7); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {0x00, 0xAB}, resp, resp_payload); + REQUIRE(ec == uart::make_error_code(uart::UartErrc::config_write_not_supported)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); + REQUIRE(resp_payload == + std::vector{static_cast(acf::WireErrorCode::UnsupportedCmd)}); + REQUIRE(server.uart().drain_tx().empty()); +} + +TEST_CASE("UART request is rejected before RCP_CONFIGURED, same operational gating as " + "GPIO/SPI/I2C/ADC/PWM_IN/LIN/CAN", + "[mock][REQ-MOCK-022]") { + mock::Server server; + REQUIRE(server.lifecycle().state() == lifecycle::ServerState::HwUnconfigured); + + auto req = standard_request(mock::kUartByteBusId, /*write=*/true, /*evt_op=*/0); + acf::AcfMessageInfo resp; + std::vector resp_payload; + auto ec = server.dispatch(0, req, {0xAA}, resp, resp_payload); + REQUIRE(ec == regmap::make_error_code(regmap::RegMapErrc::request_rejected)); + REQUIRE(acf::response_kind_of(resp) == acf::ResponseKind::ErrorResponse); +} + // ── Unmapped endpoints ──────────────────────────────────────────────────────── TEST_CASE("dispatch to an unmapped byte_bus_id reports invalid_parameter", diff --git a/tests/test_uart.cpp b/tests/test_uart.cpp index 20c0e1f..9113d27 100644 --- a/tests/test_uart.cpp +++ b/tests/test_uart.cpp @@ -5,11 +5,14 @@ // fusa:test REQ-UART-005 // fusa:test REQ-UART-006 // fusa:test REQ-UART-007 +// fusa:test REQ-UART-008 +// fusa:test REQ-UART-009 // Tests for rcp/uart.hpp — the UART endpoint type (ROADMAP.md milestone 48, // "Basic Endpoint Types II — I2C, UART, ADC, PWM_OUT, PWM_IN", v2.4.0). #include +#include #include using namespace rcp::uart; @@ -164,3 +167,103 @@ TEST_CASE("UartErrc reports a non-empty message in its own category", "[uart][RE REQUIRE(ec.category() == uart_category()); REQUIRE_FALSE(ec.message().empty()); } + +// ── Table 33 Row 2 evt[2:0] validation (handle_request) ───────────────────── + +TEST_CASE("UartEndpoint::handle_request delegates a Plain (evt[2:0]==000b) write request to " + "enqueue_tx()", + "[uart][REQ-UART-008]") { + UartEndpoint ep; + std::vector data; + bool timed_out = true; + + auto ec = ep.handle_request(/*evt_op=*/0, /*is_write=*/true, {0x01, 0x02}, /*read_size=*/0, + /*elapsed_ms=*/0, /*uart_timeout_ms=*/0, data, timed_out); + REQUIRE_FALSE(ec); + REQUIRE(ep.drain_tx() == std::vector{0x01, 0x02}); + // A write request produces no read data — out_data/out_timed_out are + // left exactly as the caller passed them. + REQUIRE(data.empty()); + REQUIRE(timed_out); +} + +TEST_CASE("UartEndpoint::handle_request delegates a Plain (evt[2:0]==000b) read request to " + "handle_read()", + "[uart][REQ-UART-008]") { + UartEndpoint ep; + REQUIRE_FALSE(ep.rx_fill({0xAA, 0xBB, 0xCC})); + + std::vector data; + bool timed_out = true; + auto ec = ep.handle_request(/*evt_op=*/0, /*is_write=*/false, /*tx_bytes=*/{}, /*read_size=*/2, + /*elapsed_ms=*/1, /*uart_timeout_ms=*/100, data, timed_out); + REQUIRE_FALSE(ec); + REQUIRE(data == std::vector{0xAA, 0xBB}); + REQUIRE_FALSE(timed_out); + REQUIRE(ep.rx_available() == 1); // remaining byte stays buffered (drain semantics) +} + +TEST_CASE("UartEndpoint::handle_request rejects every reserved evt[2:0] value (001b-110b) for " + "both write and read requests, touching neither queue", + "[uart][REQ-UART-008]") { + for (uint8_t evt_op = 1; evt_op <= 6; ++evt_op) { + UartEndpoint write_ep; + std::vector write_data; + bool write_timed_out = false; + auto write_ec = write_ep.handle_request(evt_op, /*is_write=*/true, {0xAA}, 0, 0, 0, + write_data, write_timed_out); + REQUIRE(write_ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); + REQUIRE(write_ep.drain_tx().empty()); + + UartEndpoint read_ep; + REQUIRE_FALSE(read_ep.rx_fill({0x11, 0x22})); + std::vector read_data; + bool read_timed_out = false; + auto read_ec = read_ep.handle_request(evt_op, /*is_write=*/false, {}, /*read_size=*/2, 0, 0, + read_data, read_timed_out); + REQUIRE(read_ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); + // A rejected reserved evt must not touch the RX FIFO — the bytes + // scripted above are still fully buffered. + REQUIRE(read_ep.rx_available() == 2); + } +} + +TEST_CASE("UartEndpoint::handle_request reports config_write_not_supported for evt[2:0]==111b " + "without crashing or touching either queue", + "[uart][REQ-UART-009]") { + UartEndpoint write_ep; + std::vector write_data; + bool write_timed_out = false; + auto write_ec = + write_ep.handle_request(/*evt_op=*/7, /*is_write=*/true, {0x00, 0xAB}, 0, 0, 0, write_data, write_timed_out); + REQUIRE(write_ec == make_error_code(UartErrc::config_write_not_supported)); + REQUIRE(write_ep.drain_tx().empty()); + + UartEndpoint read_ep; + REQUIRE_FALSE(read_ep.rx_fill({0x11})); + std::vector read_data; + bool read_timed_out = false; + auto read_ec = + read_ep.handle_request(/*evt_op=*/7, /*is_write=*/false, {}, /*read_size=*/1, 0, 0, read_data, read_timed_out); + REQUIRE(read_ec == make_error_code(UartErrc::config_write_not_supported)); + REQUIRE(read_ep.rx_available() == 1); +} + +TEST_CASE("UartEndpoint::handle_request masks evt_op down to 3 bits before classifying", + "[uart][REQ-UART-008]") { + UartEndpoint ep; + std::vector data; + bool timed_out = false; + REQUIRE_FALSE(ep.handle_request(/*evt_op=*/0xF8, /*is_write=*/true, {0x01}, 0, 0, 0, data, + timed_out)); // low 3 bits 000 -> Plain + auto ec = ep.handle_request(/*evt_op=*/0xF9, /*is_write=*/true, {0x01}, 0, 0, 0, data, + timed_out); // low 3 bits 001 -> Reserved + REQUIRE(ec == rcp::endpoint::make_error_code(rcp::endpoint::EndpointErrc::reserved_evt_row2)); +} + +TEST_CASE("UartErrc::config_write_not_supported reports a non-empty message in its own category", + "[uart][REQ-UART-009]") { + auto ec = make_error_code(UartErrc::config_write_not_supported); + REQUIRE(ec.category() == uart_category()); + REQUIRE_FALSE(ec.message().empty()); +}