From 18e8ea4cd4ca0170533303109aa3e9207790ebd2 Mon Sep 17 00:00:00 2001 From: Matt <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:29:37 -0700 Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20reconcile=20module=20naming=20w?= =?UTF-8?q?ith=20RELAY=20spec=20v1.14=20=C2=A713.7.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RELAY spec v1.14 expanded §13.7.2's standard module-name registry with canonical names for RCP protocol-core concerns (avtp, acf, lifecycle, regmap, discovery, request, fragment, e2e). Comparing this repo's TC18 replacement against go/rust/c-RCP found two real naming deltas (#45): - rcp/wire.hpp bundled AVTPDU header framing together with ACF_ABB/ACF_GBB message-format encoding under the "wire" name, the inverse of the other three implementations' naming. Split into rcp/avtp.hpp (AVTPDU/NTSCF/TSCF framing) and rcp/acf.hpp (ACF_ABB/ACF_GBB message format), matching the registry's separate avtp/acf entries. rcp/legacy_wire.hpp (the pre-replacement 16-byte codec, still used internally by rcp/udp.hpp until v2.13.0) is unchanged. - rcp/sequencer.hpp already covered exactly the registry's `request` entry ("conditional-request taxonomy... and sequencers") in one file, so it is renamed rcp/request.hpp rather than split; SequencerTable keeps its name as the internal sequencer-state sub-concept. rcp/lifecycle.hpp, rcp/regmap.hpp, and rcp/discovery.hpp already matched the registry and needed no change. rcp/fragment.hpp is not created: this repo's ROADMAP.md milestone 52 (v2.8.0) already decided fragmentation no-go for this cycle, so there is no fragmentation logic anywhere in the tree to extract into a module — the issue's delta here was based on a "folded into wire.hpp" possibility that current code rules out. canbr.hpp/linbr.hpp's DEPRECATE-vs-ADAPT question flagged in the issue is a real cross-repo architectural difference, not a naming issue, and is left unresolved here as the issue requested. Pure rename/split, no behavior change: full build + ctest (59/59) pass, and RELAY `relay conform --strict` against the built CLI passes. Addresses #45 Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com> --- .fusa-reqs.json | 50 +- README.md | 3 +- ROADMAP.md | 30 ++ include/rcp/acf.hpp | 300 +++++++++++ include/rcp/adc.hpp | 2 +- include/rcp/avtp.hpp | 255 ++++++++++ include/rcp/can.hpp | 2 +- include/rcp/discovery.hpp | 49 +- include/rcp/e2e.hpp | 80 +-- include/rcp/endpoint.hpp | 4 +- include/rcp/gpio.hpp | 20 +- include/rcp/i2c.hpp | 4 +- include/rcp/iseled.hpp | 2 +- include/rcp/legacy_wire.hpp | 10 +- include/rcp/lifecycle.hpp | 2 +- include/rcp/lin.hpp | 2 +- include/rcp/mdio.hpp | 2 +- include/rcp/pwm.hpp | 2 +- include/rcp/rcp.hpp | 2 +- include/rcp/regmap.hpp | 22 +- include/rcp/{sequencer.hpp => request.hpp} | 145 +++--- include/rcp/spi.hpp | 2 +- include/rcp/uart.hpp | 4 +- include/rcp/udp.hpp | 2 +- include/rcp/wakeup.hpp | 6 +- include/rcp/wire.hpp | 477 ------------------ tests/CMakeLists.txt | 16 +- tests/{test_wire.cpp => test_acf.cpp} | 144 +----- tests/test_avtp.cpp | 128 +++++ tests/test_discovery.cpp | 20 +- tests/test_e2e.cpp | 30 +- tests/test_gpio.cpp | 4 +- tests/test_legacy_wire.cpp | 3 +- .../{test_sequencer.cpp => test_request.cpp} | 172 +++---- 34 files changed, 1077 insertions(+), 919 deletions(-) create mode 100644 include/rcp/acf.hpp create mode 100644 include/rcp/avtp.hpp rename include/rcp/{sequencer.hpp => request.hpp} (85%) delete mode 100644 include/rcp/wire.hpp rename tests/{test_wire.cpp => test_acf.cpp} (61%) create mode 100644 tests/test_avtp.cpp rename tests/{test_sequencer.cpp => test_request.cpp} (82%) diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 306860b..4d4b767 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -2707,7 +2707,7 @@ { "id": "REQ-WIRE-001", "title": "NTSCF header addressing and length fields", - "text": "rcp::wire::encode_ntscf_header/decode_ntscf_header shall round-trip stream_id, sequence_num, and control_data_length unchanged for a Non-Time-Synchronous Control Format AVTPDU header.", + "text": "rcp::avtp::encode_ntscf_header/decode_ntscf_header shall round-trip stream_id, sequence_num, and control_data_length unchanged for a Non-Time-Synchronous Control Format AVTPDU header.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -2715,7 +2715,7 @@ { "id": "REQ-WIRE-002", "title": "TSCF header carries avtp_timestamp", - "text": "rcp::wire::encode_tscf_header/decode_tscf_header shall round-trip stream_id, sequence_num, control_data_length, and the 32-bit avtp_timestamp field unchanged for a Time-Synchronous Control Format AVTPDU header.", + "text": "rcp::avtp::encode_tscf_header/decode_tscf_header shall round-trip stream_id, sequence_num, control_data_length, and the 32-bit avtp_timestamp field unchanged for a Time-Synchronous Control Format AVTPDU header.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -2723,7 +2723,7 @@ { "id": "REQ-WIRE-003", "title": "AVTPDU subtype validation", - "text": "decode_ntscf_header shall reject a buffer whose subtype byte is not the NTSCF subtype, and decode_tscf_header shall reject a buffer whose subtype byte is not the TSCF subtype, both returning WireErrc::bad_subtype.", + "text": "decode_ntscf_header shall reject a buffer whose subtype byte is not the NTSCF subtype, and decode_tscf_header shall reject a buffer whose subtype byte is not the TSCF subtype, both returning AvtpErrc::bad_subtype.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -2755,7 +2755,7 @@ { "id": "REQ-WIRE-007", "title": "StreamId addressing round-trips", - "text": "rcp::wire::StreamId shall round-trip its 48-bit MAC half and 16-bit locally-assigned suffix through to_u64()/from_u64() unchanged, and two StreamId values with different MAC halves shall compare unequal.", + "text": "rcp::avtp::StreamId shall round-trip its 48-bit MAC half and 16-bit locally-assigned suffix through to_u64()/from_u64() unchanged, and two StreamId values with different MAC halves shall compare unequal.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -2763,7 +2763,7 @@ { "id": "REQ-WIRE-008", "title": "byte_bus_id is echoed unchanged on responses", - "text": "rcp::wire::make_response shall always copy byte_bus_id and transaction_num from the request it is built from, unchanged, since byte_bus_id is unique only within the owning stream_id and a response/ack must echo it back exactly.", + "text": "rcp::acf::make_response shall always copy byte_bus_id and transaction_num from the request it is built from, unchanged, since byte_bus_id is unique only within the owning stream_id and a response/ack must echo it back exactly.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -2771,7 +2771,7 @@ { "id": "REQ-WIRE-009", "title": "Standard request kind is unconditional ACF_ABB", - "text": "rcp::wire::make_standard_request shall build a request using acf_msg_type ACF_ABB with rsp clear, representing the mandatory best-effort, unconditional standard request kind.", + "text": "rcp::acf::make_standard_request shall build a request using acf_msg_type ACF_ABB with rsp clear, representing the mandatory best-effort, unconditional standard request kind.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -2779,7 +2779,7 @@ { "id": "REQ-WIRE-010", "title": "Four response semantic types are distinguishable", - "text": "rcp::wire::response_kind_of shall recover Acknowledge, WriteResponse, ReadResponse, or ErrorResponse from a header built by make_response with the corresponding ResponseKind, and the four kinds shall be pairwise distinguishable from the shared header fields alone.", + "text": "rcp::acf::response_kind_of shall recover Acknowledge, WriteResponse, ReadResponse, or ErrorResponse from a header built by make_response with the corresponding ResponseKind, and the four kinds shall be pairwise distinguishable from the shared header fields alone.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -2795,7 +2795,7 @@ { "id": "REQ-WIRE-012", "title": "Timestamp fallback rule: TSCF preferred, then ACF_GBB, then none", - "text": "rcp::wire::effective_timestamp shall return the TSCF avtp_timestamp when timestamp_valid is set, shall fall back to a valid ACF_GBB message_timestamp when no TSCF timestamp is available, and shall return std::nullopt rather than zero when neither source is valid.", + "text": "rcp::acf::effective_timestamp shall return the TSCF avtp_timestamp when timestamp_valid is set, shall fall back to a valid ACF_GBB message_timestamp when no TSCF timestamp is available, and shall return std::nullopt rather than zero when neither source is valid.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -2803,7 +2803,7 @@ { "id": "REQ-WIRE-013", "title": "Decoders reject short buffers", - "text": "decode_ntscf_header, decode_tscf_header, decode_acf_abb, and decode_acf_gbb shall return WireErrc::short_buffer when given fewer bytes than their fixed-size header requires, without reading past the buffer.", + "text": "rcp::avtp::decode_ntscf_header, rcp::avtp::decode_tscf_header, rcp::acf::decode_acf_abb, and rcp::acf::decode_acf_gbb shall return AvtpErrc::short_buffer when given fewer bytes than their fixed-size header requires, without reading past the buffer.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -2811,7 +2811,7 @@ { "id": "REQ-WIRE-014", "title": "ACF message type mismatch is rejected", - "text": "decode_acf_abb shall return WireErrc::bad_acf_msg_type when given a buffer whose acf_msg_type byte is ACF_GBB, and decode_acf_gbb shall return the same error when given a buffer whose acf_msg_type byte is ACF_ABB.", + "text": "decode_acf_abb shall return AcfErrc::bad_acf_msg_type when given a buffer whose acf_msg_type byte is ACF_GBB, and decode_acf_gbb shall return the same error when given a buffer whose acf_msg_type byte is ACF_ABB.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3107,7 +3107,7 @@ { "id": "REQ-GPIO-001", "title": "GPIO payload is a 4-byte big-endian bitmask", - "text": "rcp::gpio::kGpioPayloadLen shall be 4 and kMaxPins shall be 32; encode_gpio_payload/decode_gpio_payload shall round-trip a PinMask value in big-endian byte order, and decode_gpio_payload shall return wire::WireErrc::short_buffer when given fewer than kGpioPayloadLen bytes.", + "text": "rcp::gpio::kGpioPayloadLen shall be 4 and kMaxPins shall be 32; encode_gpio_payload/decode_gpio_payload shall round-trip a PinMask value in big-endian byte order, and decode_gpio_payload shall return avtp::AvtpErrc::short_buffer when given fewer than kGpioPayloadLen bytes.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3147,7 +3147,7 @@ { "id": "REQ-GPIO-006", "title": "GPIO functional config block wiring", - "text": "rcp::gpio::encode_gpio_functional_config/decode_gpio_functional_config shall round-trip a pin-direction mask and per-pin enabled-edge bitmasks through rcp::regmap::EndpointFunctionalConfig::data, and decode_gpio_functional_config shall return wire::WireErrc::short_buffer for an undersized blob.", + "text": "rcp::gpio::encode_gpio_functional_config/decode_gpio_functional_config shall round-trip a pin-direction mask and per-pin enabled-edge bitmasks through rcp::regmap::EndpointFunctionalConfig::data, and decode_gpio_functional_config shall return avtp::AvtpErrc::short_buffer for an undersized blob.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3411,7 +3411,7 @@ { "id": "REQ-SEQ-001", "title": "message_timestamp-repurposing request_type decode/encode (mtv=0)", - "text": "rcp::sequencer::decode_request_type shall return SequencerErrc::timestamp_not_repurposed when mtv is true, shall return SequencerErrc::unknown_request_type when the top byte of message_timestamp is not one of the 8 defined opcodes, and shall otherwise recover the opcode and the remaining 7 parameter bytes exactly as produced by encode_request_type for the same inputs; rcp::sequencer::make_conditional_request shall always build an ACF_GBB AcfMessageInfo with mtv clear.", + "text": "rcp::request::decode_request_type shall return RequestErrc::timestamp_not_repurposed when mtv is true, shall return RequestErrc::unknown_request_type when the top byte of message_timestamp is not one of the 8 defined opcodes, and shall otherwise recover the opcode and the remaining 7 parameter bytes exactly as produced by encode_request_type for the same inputs; rcp::request::make_conditional_request shall always build an ACF_GBB AcfMessageInfo with mtv clear.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3419,7 +3419,7 @@ { "id": "REQ-SEQ-002", "title": "Request categories and execution-priority ordering", - "text": "rcp::sequencer::category_of shall map the 3 cancellation opcodes to RequestCategory::Cancellation, each of the 5 conditional opcodes to its own category, and std::nullopt to RequestCategory::Standard; rcp::sequencer::select_next_due shall pick the candidate with the lowest priority_rank among due candidates and shall break ties within a category by lowest arrival_seq (FIFO), returning std::nullopt for an empty input.", + "text": "rcp::request::category_of shall map the 3 cancellation opcodes to RequestCategory::Cancellation, each of the 5 conditional opcodes to its own category, and std::nullopt to RequestCategory::Standard; rcp::request::select_next_due shall pick the candidate with the lowest priority_rank among due candidates and shall break ties within a category by lowest arrival_seq (FIFO), returning std::nullopt for an empty input.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3427,7 +3427,7 @@ { "id": "REQ-SEQ-003", "title": "Compound/compound-wait optional-feature bundling", - "text": "rcp::sequencer::validate_feature_bundles shall return SequencerErrc::compound_bundle_incomplete whenever FeatureSet::compound or FeatureSet::compound_wait is set unless compound, compound_wait, clear_non_safestate, and sequencer_count >= kMinCompoundSequencers (4) are all simultaneously true, and shall accept every other combination of triggered/chained/timed/clear_single independently of compound support.", + "text": "rcp::request::validate_feature_bundles shall return RequestErrc::compound_bundle_incomplete whenever FeatureSet::compound or FeatureSet::compound_wait is set unless compound, compound_wait, clear_non_safestate, and sequencer_count >= kMinCompoundSequencers (4) are all simultaneously true, and shall accept every other combination of triggered/chained/timed/clear_single independently of compound support.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3435,7 +3435,7 @@ { "id": "REQ-SEQ-004", "title": "Sequencer-state default value and finalization advance rule", - "text": "rcp::sequencer::SequencerTable::ensure_size shall fill newly added slots with kDefaultState (1), never the storage's own zero-initialization default, shall never shrink existing storage, and try_advance shall increment a sequencer only when its current value equals the supplied expected_start, leaving it unchanged (and reporting out_advanced=false, not an error) otherwise; both accessors shall return SequencerErrc::index_out_of_range for an out-of-bounds index.", + "text": "rcp::request::SequencerTable::ensure_size shall fill newly added slots with kDefaultState (1), never the storage's own zero-initialization default, shall never shrink existing storage, and try_advance shall increment a sequencer only when its current value equals the supplied expected_start, leaving it unchanged (and reporting out_advanced=false, not an error) otherwise; both accessors shall return RequestErrc::index_out_of_range for an out-of-bounds index.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3443,7 +3443,7 @@ { "id": "REQ-SEQ-005", "title": "Cancellation kinds and shared cancellation semantics", - "text": "rcp::sequencer::RequestLedger::cancel_single shall cancel a Pending/Started request and cascade cancellation to every transitive chained successor, and shall return SequencerErrc::request_not_found for an unknown transaction or one that is UnderExecution/Finalized/already Canceled; cancel_all shall cancel every Pending/Started record (optionally skipping is_safety records when non_safestate_only is set) without disturbing any UnderExecution or Finalized record; a canceled request's outcome_of shall report SequencerErrc::request_canceled.", + "text": "rcp::request::RequestLedger::cancel_single shall cancel a Pending/Started request and cascade cancellation to every transitive chained successor, and shall return RequestErrc::request_not_found for an unknown transaction or one that is UnderExecution/Finalized/already Canceled; cancel_all shall cancel every Pending/Started record (optionally skipping is_safety records when non_safestate_only is set) without disturbing any UnderExecution or Finalized record; a canceled request's outcome_of shall report RequestErrc::request_canceled.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3451,7 +3451,7 @@ { "id": "REQ-SEQ-006", "title": "Request lifecycle state machine: pending -> started -> under_execution -> finalized", - "text": "rcp::sequencer::RequestLedger shall only permit the single-step forward sequence Pending->Started->UnderExecution->Finalized, returning SequencerErrc::invalid_lifecycle_transition for any other requested transition and SequencerErrc::unknown_transaction for an untracked id; submit shall return SequencerErrc::transaction_num_collision for a transaction_num already tracked; finalize shall drive SequencerTable::try_advance for a Compound/CompoundWait record's sequencer_index/expected_start_state as its side effect.", + "text": "rcp::request::RequestLedger shall only permit the single-step forward sequence Pending->Started->UnderExecution->Finalized, returning RequestErrc::invalid_lifecycle_transition for any other requested transition and RequestErrc::unknown_transaction for an untracked id; submit shall return RequestErrc::transaction_num_collision for a transaction_num already tracked; finalize shall drive SequencerTable::try_advance for a Compound/CompoundWait record's sequencer_index/expected_start_state as its side effect.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3459,7 +3459,7 @@ { "id": "REQ-SEQ-007", "title": "Chained-successor propagation on predecessor finalization", - "text": "rcp::sequencer::RequestLedger::finalize shall, for every record whose chained_predecessor equals the finalized transaction_num, cascade-cancel that successor iff rcp::sequencer::should_execute_chained(successor.cs, errored) is false, and shall leave every such successor untouched when the predecessor finalized with errored=false.", + "text": "rcp::request::RequestLedger::finalize shall, for every record whose chained_predecessor equals the finalized transaction_num, cascade-cancel that successor iff rcp::request::should_execute_chained(successor.cs, errored) is false, and shall leave every such successor untouched when the predecessor finalized with errored=false.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3467,15 +3467,15 @@ { "id": "REQ-SEQ-008", "title": "The cs field's dual meaning (compound-wait vs. chained)", - "text": "rcp::sequencer::compound_wait_check_of shall map cs=true to CompoundWaitCheck::Immediate and cs=false to CompoundWaitCheck::AfterChangeOnly; rcp::sequencer::should_execute_chained shall return true whenever cs is true regardless of predecessor_errored, and shall return true iff predecessor_errored is false when cs is false.", + "text": "rcp::request::compound_wait_check_of shall map cs=true to CompoundWaitCheck::Immediate and cs=false to CompoundWaitCheck::AfterChangeOnly; rcp::request::should_execute_chained shall return true whenever cs is true regardless of predecessor_errored, and shall return true iff predecessor_errored is false when cs is false.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" }, { "id": "REQ-SEQ-009", - "title": "SequencerErrc is a distinct error category with non-empty, distinct messages", - "text": "Every rcp::sequencer::SequencerErrc value shall report rcp::sequencer::sequencer_category() as its category and shall have a non-empty diagnostic message distinct from every other SequencerErrc value's message.", + "title": "RequestErrc is a distinct error category with non-empty, distinct messages", + "text": "Every rcp::request::RequestErrc value shall report rcp::request::request_category() as its category and shall have a non-empty diagnostic message distinct from every other RequestErrc value's message.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3483,7 +3483,7 @@ { "id": "REQ-SEQ-010", "title": "decode_request_type accepts exactly the three 0x8x safety-tagged opcodes", - "text": "rcp::sequencer::is_valid_request_type and decode_request_type shall accept RequestTypeOpcode::CompoundSafety (0x8F), CompoundWaitSafety (0x8B), and TriggeredSafety (0x8E) as valid opcodes, round-tripping their parameter bytes exactly as the eight pre-existing opcodes do, and shall reject every other MSB-set byte value as SequencerErrc::unknown_request_type.", + "text": "rcp::request::is_valid_request_type and decode_request_type shall accept RequestTypeOpcode::CompoundSafety (0x8F), CompoundWaitSafety (0x8B), and TriggeredSafety (0x8E) as valid opcodes, round-tripping their parameter bytes exactly as the eight pre-existing opcodes do, and shall reject every other MSB-set byte value as RequestErrc::unknown_request_type.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3491,7 +3491,7 @@ { "id": "REQ-SEQ-011", "title": "Safety-tagged opcodes share their base opcode's priority category", - "text": "rcp::sequencer::is_safety_variant shall return true for exactly CompoundSafety/CompoundWaitSafety/TriggeredSafety and false for every other RequestTypeOpcode; rcp::sequencer::category_of shall map each safety-tagged opcode to the same RequestCategory as its corresponding base opcode (Compound, CompoundWait, Triggered).", + "text": "rcp::request::is_safety_variant shall return true for exactly CompoundSafety/CompoundWaitSafety/TriggeredSafety and false for every other RequestTypeOpcode; rcp::request::category_of shall map each safety-tagged opcode to the same RequestCategory as its corresponding base opcode (Compound, CompoundWait, Triggered).", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" @@ -3499,7 +3499,7 @@ { "id": "REQ-SEQ-012", "title": "request_record_for derives RequestRecord::is_safety from the opcode", - "text": "rcp::sequencer::request_record_for shall set the returned RequestRecord's is_safety field to is_safety_variant(*type) when type has a value, to false when type is std::nullopt (the standard request kind), and the resulting record shall be excluded from cancellation by RequestLedger::cancel_all(non_safestate_only=true) if and only if is_safety is true.", + "text": "rcp::request::request_record_for shall set the returned RequestRecord's is_safety field to is_safety_variant(*type) when type has a value, to false when type is std::nullopt (the standard request kind), and the resulting record shall be excluded from cancellation by RequestLedger::cancel_all(non_safestate_only=true) if and only if is_safety is true.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B" diff --git a/README.md b/README.md index eac5bd3..7e165fb 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,8 @@ reference. | `` | Mutual TLS transport for zone controller communication | | `` | Zero-copy intra-host command delivery via shared in-process memory | | `` | IEEE 802.1Qbv-aware UDP transport adapter for hard real-time Ethernet | -| `` | TC18 wire codec — IEEE 1722 AVTPDU (NTSCF/TSCF) framing and ACF_ABB/ACF_GBB messages (ROADMAP.md v2.0.0) | +| `` | TC18 wire codec, framing half — IEEE 1722 AVTPDU (NTSCF/TSCF) header framing (ROADMAP.md v2.0.0) | +| `` | TC18 wire codec, message half — ACF_ABB/ACF_GBB message format (ROADMAP.md v2.0.0) | | `` | Pre-replacement 16-byte frame codec, kept only for ``'s internal use until it is rebuilt at v2.13.0 | | `` | Timing-realistic zone controller simulator for SiL/HIL testing | diff --git a/ROADMAP.md b/ROADMAP.md index 53f37b8..332053d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -980,6 +980,36 @@ orthogonal to the protocol replacement. | `relay/relay.hpp` | KEEP AS-IS | Protocol-agnostic `Message`/`Channel`/`Node`/`Caller`/`Errc` types have no dependency on `rcp::` internals; only the RCP-side mapping in `adapt.hpp` needs rework | | Safety/certification tooling (`.fusa*.json`, `fmea.*`, `HARA.md`, `TARA-ANALYSIS.md`, `SAFETY_PLAN.md`, `CYBERSECURITY.md`, `AUDIT_PACK.md`, `tla/`, `cmake/`, `tooling/`) | KEEP AS-IS | Genuinely orthogonal process/tooling scaffolding; regenerated with new requirement IDs at the Certification Refresh milestone (v2.18.0) rather than redesigned | +### Post-hoc naming reconciliation (RELAY spec §13.7.2, issue #45) + +RELAY spec v1.14 expanded §13.7.2's standard module-name registry with +canonical names for the RCP protocol-core concerns this Phase 13 rewrite +builds, prompted by comparing this effort against the equivalent go/rust/c +RCP replacements. Two naming deltas from milestone 44/49 above were +corrected to match, without any behavior change: + +- `rcp/wire.hpp` (milestone 44, v2.0.0) is split into `rcp/avtp.hpp` + (AVTPDU/NTSCF/TSCF header framing) and `rcp/acf.hpp` (ACF_ABB/ACF_GBB + message format) — the registry names these two concerns separately. + Every table/paragraph above that refers to `rcp/wire.hpp` as the then- + current file is describing the shape of the milestone 44/49/50/51 work as + it shipped at the time; the file itself now lives under those two names. + `rcp/legacy_wire.hpp` (the pre-replacement 16-byte codec) is unaffected + and keeps its name. +- `rcp/sequencer.hpp` (milestone 49, v2.5.0) is renamed `rcp/request.hpp` to + match the registry's `request` entry ("conditional-request taxonomy... and + sequencers") — the file's contents already covered exactly that combined + scope, so this was a rename, not a split; `SequencerTable` keeps its name + as the internal sequencer-state sub-concept within the module. +- `rcp/lifecycle.hpp`, `rcp/regmap.hpp`, `rcp/discovery.hpp`, and + `rcp/e2e.hpp` already matched the registry and were left unchanged. +- `rcp/fragment.hpp` is not created: milestone 52 (v2.8.0) above already + decided fragmentation no-go for this cycle, so there is no fragmentation + logic anywhere in this tree to extract into a module. +- `canbr.hpp`/`linbr.hpp`'s DEPRECATE call above (vs. go-RCP's ADAPT) is a + real cross-repo architectural difference, not a naming issue, and is + intentionally left unresolved here — see issue #45. + --- ### Appendix A — Legacy Roadmap (v0.1.0–v1.11, superseded) --- diff --git a/include/rcp/acf.hpp b/include/rcp/acf.hpp new file mode 100644 index 0000000..0ab6014 --- /dev/null +++ b/include/rcp/acf.hpp @@ -0,0 +1,300 @@ +// fusa:req REQ-WIRE-004 +// fusa:req REQ-WIRE-005 +// fusa:req REQ-WIRE-006 +// fusa:req REQ-WIRE-008 +// fusa:req REQ-WIRE-009 +// fusa:req REQ-WIRE-010 +// fusa:req REQ-WIRE-012 +// fusa:req REQ-WIRE-013 +// fusa:req REQ-WIRE-014 + +// TC18 wire codec, message half — the ACF_ABB / ACF_GBB message types (ACF — +// AVTP Control Format) the OPEN Alliance TC18 Remote Control Protocol +// Specification v0.5.1_RC layers on top of rcp/avtp.hpp's AVTPDU framing. +// +// ROADMAP.md milestone 44, "Wire Format Core (v2.0.0)": originally landed +// together with the AVTPDU framing code as a single rcp/wire.hpp; split into +// rcp/avtp.hpp (AVTPDU header framing) and rcp/acf.hpp (this file, +// ACF_ABB/ACF_GBB message format) per RELAY spec §13.7.2's standard +// module-name registry, which names these two concerns separately. +// +// Scope note: this module is a pure wire codec, same as rcp/avtp.hpp — no +// dependency on rcp.hpp's Zone/Command/Controller/Registry model, and no +// dependency on any RC Server lifecycle state, register-map, discovery, or +// endpoint-specific behavior. Conditional request kinds beyond the mandatory +// "standard" kind (v2.5.0, rcp/request.hpp), the E2E CRC safe-point +// mechanism (v2.6.0, rcp/e2e.hpp), and fragmentation (deferred indefinitely, +// v2.8.0) are out of scope here; the fields they need (`cs`, `ms`, +// `read_size_or_segment_num`) already exist below so later milestones can be +// layered on without reshaping this header. +// +// 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-packing +// chosen in this file is this implementation's own encoding of that +// behavior for milestone 44 — full bit-for-bit wire conformance against +// other TC18 implementations is not claimed until v2.6.0 lands, per the +// Phase 13 introduction in ROADMAP.md. +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace rcp { +namespace acf { + +// ── ACF message types ───────────────────────────────────────────────────────── +// The two ACF (AVTP Control Format) message types RCP defines on top of the +// AVTPDU framing in rcp/avtp.hpp (extraction §2.3). + +constexpr uint8_t kAcfMsgTypeGbb = 0x0D; // ACF_GBB — carries a 64-bit message_timestamp slot +constexpr uint8_t kAcfMsgTypeAbb = 0x0E; // ACF_ABB — no timestamp field at all + +// ── Errors ──────────────────────────────────────────────────────────────────── +// short-buffer conditions are reported via avtp::AvtpErrc::short_buffer +// (rcp/avtp.hpp), which this module already depends on, rather than a +// second short_buffer value defined here. + +enum class AcfErrc : int { + bad_acf_msg_type = 1, // acf_msg_type is neither ACF_ABB nor ACF_GBB +}; + +inline const std::error_category& acf_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.acf"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case AcfErrc::bad_acf_msg_type: return "rcp/acf: unrecognized ACF message type"; + default: return "rcp/acf: unknown error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(AcfErrc e) noexcept { + return {static_cast(e), acf_category()}; +} + +// ── ACF shared header ("byte_message_info") ─────────────────────────────────── +// Fields common to both ACF_ABB and ACF_GBB messages (extraction §2.4). Every +// field the roadmap calls out is represented individually below rather than +// left packed in an opaque byte, so later milestones (compound requests, +// safety variants, …) can extend behavior around a given field without +// having to re-derive its bit position from scratch. + +struct AcfMessageInfo { + uint8_t acf_msg_type = kAcfMsgTypeAbb; // ACF_ABB or ACF_GBB + uint16_t acf_msg_length = 0; // quadlets, including this shared header + uint8_t pad = 0; // 0–3 trailing pad bytes added for quadlet alignment + bool mtv = false; // message_timestamp valid (ACF_GBB only) + avtp::ByteBusId byte_bus_id = 0; // target endpoint, unique within stream_id only + bool evt_ack = false; // evt[3] — acknowledge flag + uint8_t evt_op = 0; // evt[2:0] — endpoint-defined sub-opcode + bool hs = false; // reserved for endpoint-specific use (e.g. I2C high-speed) + bool cs = false; // conditional-start; meaning depends on request kind (v2.5.0) + uint8_t transaction_num = 0; // correlates a request with its response/ack + bool op = false; // false = read, true = write (this codec's convention) + bool rsp = false; // set on every response/ack, clear on requests + bool err = false; // set alongside rsp for an error response + bool ms = false; // "more segments" — fragmentation, deferred to v2.8.0 + uint16_t read_size_or_segment_num = 0; // read_size when !ms, segment_num when ms +}; + +constexpr size_t kAcfCommonHeaderLen = 10; +constexpr size_t kAcfGbbTimestampLen = 8; + +namespace detail { +constexpr uint8_t kByte3MtvBit = 0x80; +constexpr uint8_t kByte3PadShift = 5; +constexpr uint8_t kByte3PadMask = 0x03; +constexpr uint8_t kByte5AckBit = 0x80; +constexpr uint8_t kByte5OpShift = 4; +constexpr uint8_t kByte5OpMask = 0x07; +constexpr uint8_t kByte5HsBit = 0x08; +constexpr uint8_t kByte5CsBit = 0x04; +constexpr uint8_t kByte7OpBit = 0x80; +constexpr uint8_t kByte7RspBit = 0x40; +constexpr uint8_t kByte7ErrBit = 0x20; +constexpr uint8_t kByte7MsBit = 0x10; +} // namespace detail + +inline void encode_acf_message_info(const AcfMessageInfo& info, uint8_t* out10) noexcept { + out10[0] = info.acf_msg_type; + avtp::detail::put_u16(&out10[1], info.acf_msg_length); + out10[3] = static_cast( + (info.mtv ? detail::kByte3MtvBit : 0) | + (static_cast(info.pad & detail::kByte3PadMask) << detail::kByte3PadShift)); + out10[4] = info.byte_bus_id; + out10[5] = static_cast( + (info.evt_ack ? detail::kByte5AckBit : 0) | + (static_cast(info.evt_op & detail::kByte5OpMask) << detail::kByte5OpShift) | + (info.hs ? detail::kByte5HsBit : 0) | + (info.cs ? detail::kByte5CsBit : 0)); + out10[6] = info.transaction_num; + out10[7] = static_cast( + (info.op ? detail::kByte7OpBit : 0) | + (info.rsp ? detail::kByte7RspBit : 0) | + (info.err ? detail::kByte7ErrBit : 0) | + (info.ms ? detail::kByte7MsBit : 0)); + avtp::detail::put_u16(&out10[8], info.read_size_or_segment_num); +} + +inline void decode_acf_message_info(const uint8_t* in10, AcfMessageInfo& out) noexcept { + out.acf_msg_type = in10[0]; + out.acf_msg_length = avtp::detail::get_u16(&in10[1]); + out.mtv = (in10[3] & detail::kByte3MtvBit) != 0; + out.pad = static_cast((in10[3] >> detail::kByte3PadShift) & detail::kByte3PadMask); + out.byte_bus_id = in10[4]; + out.evt_ack = (in10[5] & detail::kByte5AckBit) != 0; + out.evt_op = static_cast((in10[5] >> detail::kByte5OpShift) & detail::kByte5OpMask); + out.hs = (in10[5] & detail::kByte5HsBit) != 0; + out.cs = (in10[5] & detail::kByte5CsBit) != 0; + out.transaction_num = in10[6]; + out.op = (in10[7] & detail::kByte7OpBit) != 0; + out.rsp = (in10[7] & detail::kByte7RspBit) != 0; + out.err = (in10[7] & detail::kByte7ErrBit) != 0; + out.ms = (in10[7] & detail::kByte7MsBit) != 0; + out.read_size_or_segment_num = avtp::detail::get_u16(&in10[8]); +} + +// ── ACF_ABB / ACF_GBB message encode / decode ───────────────────────────────── +// ACF_ABB carries no timestamp field at all; ACF_GBB always reserves a +// 64-bit message_timestamp slot immediately after the shared header, +// regardless of whether `mtv` marks it valid (extraction §2.3, §2.7). + +inline std::vector encode_acf_abb(AcfMessageInfo info, + const std::vector& payload) { + info.acf_msg_type = kAcfMsgTypeAbb; + info.mtv = false; + std::vector buf(kAcfCommonHeaderLen + payload.size()); + encode_acf_message_info(info, buf.data()); + std::copy(payload.begin(), payload.end(), buf.begin() + static_cast(kAcfCommonHeaderLen)); + return buf; +} + +inline std::error_code decode_acf_abb(const uint8_t* b, size_t len, + AcfMessageInfo& out_info, + std::vector& out_payload) { + if (len < 1) return avtp::make_error_code(avtp::AvtpErrc::short_buffer); + if (b[0] != kAcfMsgTypeAbb) return make_error_code(AcfErrc::bad_acf_msg_type); + if (len < kAcfCommonHeaderLen) return avtp::make_error_code(avtp::AvtpErrc::short_buffer); + decode_acf_message_info(b, out_info); + out_payload.assign(b + kAcfCommonHeaderLen, b + len); + return {}; +} + +// message_timestamp is the 64-bit ACF_GBB timestamp slot (extraction §2.7, +// §2.9). Its validity is carried by AcfMessageInfo::mtv, not by the value +// itself — an all-zero timestamp is a legitimate valid value. +inline std::vector encode_acf_gbb(AcfMessageInfo info, uint64_t message_timestamp, + const std::vector& payload) { + info.acf_msg_type = kAcfMsgTypeGbb; + std::vector buf(kAcfCommonHeaderLen + kAcfGbbTimestampLen + payload.size()); + encode_acf_message_info(info, buf.data()); + avtp::detail::put_u64(&buf[kAcfCommonHeaderLen], message_timestamp); + std::copy(payload.begin(), payload.end(), + buf.begin() + static_cast(kAcfCommonHeaderLen + kAcfGbbTimestampLen)); + return buf; +} + +inline std::error_code decode_acf_gbb(const uint8_t* b, size_t len, + AcfMessageInfo& out_info, + uint64_t& out_message_timestamp, + std::vector& out_payload) { + if (len < 1) return avtp::make_error_code(avtp::AvtpErrc::short_buffer); + if (b[0] != kAcfMsgTypeGbb) return make_error_code(AcfErrc::bad_acf_msg_type); + if (len < kAcfCommonHeaderLen + kAcfGbbTimestampLen) + return avtp::make_error_code(avtp::AvtpErrc::short_buffer); + decode_acf_message_info(b, out_info); + out_message_timestamp = avtp::detail::get_u64(&b[kAcfCommonHeaderLen]); + out_payload.assign(b + kAcfCommonHeaderLen + kAcfGbbTimestampLen, b + len); + return {}; +} + +// ── Standard request kind & response semantics ──────────────────────────────── +// The mandatory baseline request kind: best-effort, unconditional dispatch +// via ACF_ABB (extraction §2.7). Conditional kinds (compound, triggered, +// timed, chained, …) are a later milestone (v2.5.0, rcp/request.hpp) layered +// on top of the `AcfMessageInfo` fields already defined above; this module +// only wires up the one kind every RC Server must support. + +enum class RequestKind : uint8_t { + Standard = 0, +}; + +inline AcfMessageInfo make_standard_request(avtp::ByteBusId bus_id, uint8_t transaction_num, + bool write, uint16_t read_size) noexcept { + AcfMessageInfo info; + info.acf_msg_type = kAcfMsgTypeAbb; + info.byte_bus_id = bus_id; + info.transaction_num = transaction_num; + info.op = write; + info.rsp = false; + info.err = false; + info.read_size_or_segment_num = write ? 0 : read_size; + return info; +} + +// The four response semantic types a request can produce, mapped onto the +// shared header's evt/op/rsp/err fields (extraction §2.7, §2.8). +enum class ResponseKind : uint8_t { + Acknowledge = 0, // received, no data — evt_ack set, rsp clear + WriteResponse = 1, // completed write — rsp set, op set, err clear + ReadResponse = 2, // completed read, payload carries the data — rsp set, op clear, err clear + ErrorResponse = 3, // failed — rsp set, err set +}; + +inline ResponseKind response_kind_of(const AcfMessageInfo& info) noexcept { + if (!info.rsp) return ResponseKind::Acknowledge; + if (info.err) return ResponseKind::ErrorResponse; + return info.op ? ResponseKind::WriteResponse : ResponseKind::ReadResponse; +} + +// make_response builds the header for a response/ack to `request`. It always +// copies byte_bus_id and transaction_num from the request unchanged — this +// is the mandatory echo-back rule for byte_bus_id (extraction §2.1) — +// callers cannot construct a response with a different byte_bus_id than the +// request it answers through this function. +inline AcfMessageInfo make_response(const AcfMessageInfo& request, ResponseKind kind) noexcept { + AcfMessageInfo resp; + resp.acf_msg_type = request.acf_msg_type; + resp.byte_bus_id = request.byte_bus_id; + resp.transaction_num = request.transaction_num; + resp.evt_ack = (kind == ResponseKind::Acknowledge); + resp.rsp = (kind != ResponseKind::Acknowledge); + resp.err = (kind == ResponseKind::ErrorResponse); + resp.op = (kind == ResponseKind::WriteResponse); + return resp; +} + +// ── Timestamp fallback rules ────────────────────────────────────────────────── +// avtp_timestamp (TSCF-only, 32-bit, rcp/avtp.hpp) and message_timestamp +// (ACF_GBB-only, 64-bit, valid only when `mtv` is set) are two independent, +// optional sources of timing information for the same logical message +// (extraction §2.6, §2.9). effective_timestamp prefers the TSCF timestamp +// when present and valid, falls back to a valid ACF_GBB message_timestamp, +// and returns std::nullopt — rather than silently defaulting to zero — when +// neither source is usable, so callers can distinguish "no timestamp" from +// "timestamp is zero". + +inline std::optional effective_timestamp(const avtp::TscfHeader* tscf, + const AcfMessageInfo* acf_info, + uint64_t message_timestamp) noexcept { + if (tscf != nullptr && tscf->timestamp_valid) + return uint64_t{tscf->avtp_timestamp}; + if (acf_info != nullptr && acf_info->acf_msg_type == kAcfMsgTypeGbb && acf_info->mtv) + return message_timestamp; + return std::nullopt; +} + +} // namespace acf +} // namespace rcp diff --git a/include/rcp/adc.hpp b/include/rcp/adc.hpp index 4525df6..0db46d3 100644 --- a/include/rcp/adc.hpp +++ b/include/rcp/adc.hpp @@ -47,7 +47,7 @@ // text from that document is reproduced here. The concrete averaging // combinator (arithmetic mean at both levels) and cadence-pattern API shape // chosen in this file are this implementation's own, same as the equivalent -// disclaimers in rcp/wire.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, +// disclaimers in rcp/avtp.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, // rcp/gpio.hpp, and rcp/spi.hpp. #pragma once diff --git a/include/rcp/avtp.hpp b/include/rcp/avtp.hpp new file mode 100644 index 0000000..9cfe810 --- /dev/null +++ b/include/rcp/avtp.hpp @@ -0,0 +1,255 @@ +// fusa:req REQ-WIRE-001 +// fusa:req REQ-WIRE-002 +// fusa:req REQ-WIRE-003 +// fusa:req REQ-WIRE-007 +// fusa:req REQ-WIRE-011 +// fusa:req REQ-WIRE-013 + +// TC18 wire codec, framing half — IEEE 1722 AVTPDU framing (NTSCF/TSCF) that +// the OPEN Alliance TC18 Remote Control Protocol Specification v0.5.1_RC +// layers its ACF messages (rcp/acf.hpp) on top of. +// +// ROADMAP.md milestone 44, "Wire Format Core (v2.0.0)": this header (plus +// rcp/acf.hpp) replaces the old bespoke 16-byte frame this file used to +// define (now preserved as-is, unrenamed in behavior, under +// rcp/legacy_wire.hpp for rcp/udp.hpp's benefit until the transport itself +// is rebuilt at v2.13.0). Originally landed as a single rcp/wire.hpp; split +// into rcp/avtp.hpp (this file, AVTPDU header framing) and rcp/acf.hpp +// (ACF_ABB/ACF_GBB message format) per RELAY spec §13.7.2's standard +// module-name registry, which names these two concerns separately. +// +// Scope note: this module is a pure wire codec. It has no dependency on +// rcp.hpp's Zone/Command/Controller/Registry model, and no dependency on any +// RC Server lifecycle state, register-map, discovery, or endpoint-specific +// behavior — those are later Phase 13 milestones (v2.1.0 onward), sequenced +// on top of this header, not into it. Conditional request kinds beyond the +// mandatory "standard" kind (v2.5.0), the E2E CRC safe-point mechanism +// (v2.6.0), and fragmentation (deferred indefinitely, v2.8.0) are likewise +// out of scope here; the fields they need already exist in rcp/acf.hpp so +// later milestones can be layered on without reshaping either header. +// +// Transport-agnostic by design: nothing in this module assumes the AVTPDU +// travels over raw Ethernet. The same framing is meant to sit under a native +// IEEE 1722 Ethernet transport, IEEE 1722-over-UDP/IP (v2.13.0), or a +// CAN(FD/XL) transport — no socket, header, or byte-order assumption here is +// specific to any one of those. +// +// 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-packing +// chosen in this file is this implementation's own encoding of that +// behavior for milestone 44; full bit-for-bit wire conformance against +// other TC18 implementations is not claimed until v2.6.0 lands, per the +// Phase 13 introduction in ROADMAP.md. +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace rcp { +namespace avtp { + +// ── AVTPDU subtype identifiers ──────────────────────────────────────────────── +// From the IEEE 1722 AVTP subtype space — a separate, publicly published +// standard from the confidential TC18 document cited above. NTSCF and TSCF +// are the two control formats TC18 builds its messaging on (extraction §2.2). + +constexpr uint8_t kSubtypeTscf = 0x05; // Time-Synchronous Control Format +constexpr uint8_t kSubtypeNtscf = 0x82; // Non-Time-Synchronous Control Format + +// ── Errors ──────────────────────────────────────────────────────────────────── +// short_buffer is reused by rcp/acf.hpp for its own decode functions (which +// include this header) rather than duplicated there — both modules report +// the same "fewer bytes available than required" condition. + +enum class AvtpErrc : int { + short_buffer = 1, // fewer bytes available than the header/field requires + bad_subtype = 2, // AVTPDU subtype byte is neither NTSCF nor TSCF + length_mismatch = 3, // control_data_length disagrees with buffer size +}; + +inline const std::error_category& avtp_category() noexcept { + struct Cat : std::error_category { + const char* name() const noexcept override { return "rcp.avtp"; } + std::string message(int ev) const override { + switch (static_cast(ev)) { + case AvtpErrc::short_buffer: return "rcp/avtp: buffer too short"; + case AvtpErrc::bad_subtype: return "rcp/avtp: unrecognized AVTPDU subtype"; + case AvtpErrc::length_mismatch: return "rcp/avtp: length field does not match buffer size"; + default: return "rcp/avtp: unknown error"; + } + } + }; + static Cat instance; + return instance; +} + +inline std::error_code make_error_code(AvtpErrc e) noexcept { + return {static_cast(e), avtp_category()}; +} + +// ── Internal helpers ────────────────────────────────────────────────────────── +// Shared big-endian byte-packing helpers. rcp/acf.hpp and other headers +// (e.g. rcp/gpio.hpp) reuse these rather than re-deriving byte order. + +namespace detail { + +inline void put_u16(uint8_t* p, uint16_t v) noexcept { + p[0] = static_cast(v >> 8); + p[1] = static_cast(v & 0xFF); +} +inline void put_u32(uint8_t* p, uint32_t v) noexcept { + p[0] = static_cast(v >> 24); + p[1] = static_cast((v >> 16) & 0xFF); + p[2] = static_cast((v >> 8) & 0xFF); + p[3] = static_cast(v & 0xFF); +} +inline void put_u64(uint8_t* p, uint64_t v) noexcept { + for (int i = 0; i < 8; ++i) + p[i] = static_cast((v >> (56 - 8 * i)) & 0xFF); +} +inline uint16_t get_u16(const uint8_t* p) noexcept { + return static_cast((uint16_t(p[0]) << 8) | p[1]); +} +inline uint32_t get_u32(const uint8_t* p) noexcept { + return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) | + (uint32_t(p[2]) << 8) | uint32_t(p[3]); +} +inline uint64_t get_u64(const uint8_t* p) noexcept { + uint64_t v = 0; + for (int i = 0; i < 8; ++i) + v = (v << 8) | uint64_t(p[i]); + return v; +} + +} // namespace detail + +// ── StreamId ────────────────────────────────────────────────────────────────── +// stream_id addresses the sender of an AVTPDU: the sender's 48-bit MAC +// address plus a 16-bit suffix the sender assigns locally to distinguish its +// own concurrent streams (extraction §2.1). Two different senders' stream_id +// values are only guaranteed distinct because their MAC halves differ; the +// suffix alone is not a global identifier. + +struct StreamId { + std::array mac{}; + uint16_t suffix = 0; + + uint64_t to_u64() const noexcept { + uint64_t v = 0; + for (uint8_t b : mac) v = (v << 8) | uint64_t(b); + return (v << 16) | uint64_t(suffix); + } + + static StreamId from_u64(uint64_t v) noexcept { + StreamId id; + id.suffix = static_cast(v & 0xFFFF); + uint64_t mac_bits = v >> 16; + for (int i = 5; i >= 0; --i) { + id.mac[static_cast(i)] = static_cast(mac_bits & 0xFF); + mac_bits >>= 8; + } + return id; + } +}; + +inline bool operator==(const StreamId& a, const StreamId& b) noexcept { + return a.mac == b.mac && a.suffix == b.suffix; +} +inline bool operator!=(const StreamId& a, const StreamId& b) noexcept { return !(a == b); } + +// ── ByteBusId ───────────────────────────────────────────────────────────────── +// byte_bus_id addresses a target endpoint's request path *within* one +// stream_id. Rule (extraction §2.1): byte_bus_id is unique only within the +// owning stream, never globally — two different clients' streams may reuse +// the same byte_bus_id for unrelated endpoints — and a response or ack MUST +// echo the byte_bus_id of the request it answers, unchanged. rcp/acf.hpp +// enforces the echo half of that rule structurally: see its make_response(), +// which always copies byte_bus_id (and transaction_num) from the request it +// is built from rather than accepting them as free parameters. + +using ByteBusId = uint8_t; + +// ── NTSCF / TSCF AVTPDU headers ─────────────────────────────────────────────── +// +// NTSCF is non-time-synchronous and, per the target protocol, server-only +// outbound; TSCF is time-synchronous and client-only (extraction §2.2). This +// codec does not know which local role is encoding or decoding a given +// frame, so it cannot itself refuse a TSCF frame built by "the server" — +// enforcing that direction rule is the transport/application layer's job +// once it knows its own role; this module only documents it here and +// exposes the two header shapes distinctly enough that misusing one for the +// other's direction is a visible, deliberate choice rather than an accident. + +struct NtscfHeader { + StreamId stream_id{}; + uint16_t sequence_num = 0; // per-stream_id AVTPDU counter + uint16_t control_data_length = 0; // byte length of the ACF message that follows +}; + +struct TscfHeader { + StreamId stream_id{}; + uint16_t sequence_num = 0; + uint16_t control_data_length = 0; + bool timestamp_valid = false; // "tv" — whether avtp_timestamp below is meaningful + uint32_t avtp_timestamp = 0; // 32-bit; TSCF-only (extraction §2.6) +}; + +constexpr size_t kNtscfHeaderLen = 14; // 1 subtype + 1 flags + 2 seq + 8 stream_id + 2 cdl +constexpr size_t kTscfHeaderLen = 18; // kNtscfHeaderLen + 4 avtp_timestamp + +namespace detail { +constexpr uint8_t kFlagStreamValid = 0x80; // "sv" — always set for these control formats +constexpr uint8_t kFlagTimestampValid = 0x40; // "tv" — TSCF only +} // namespace detail + +inline std::vector encode_ntscf_header(const NtscfHeader& h) { + std::vector buf(kNtscfHeaderLen, 0); + buf[0] = kSubtypeNtscf; + buf[1] = detail::kFlagStreamValid; + detail::put_u16(&buf[2], h.sequence_num); + detail::put_u64(&buf[4], h.stream_id.to_u64()); + detail::put_u16(&buf[12], h.control_data_length); + return buf; +} + +inline std::error_code decode_ntscf_header(const uint8_t* b, size_t len, NtscfHeader& out) { + if (len < 1) return make_error_code(AvtpErrc::short_buffer); + if (b[0] != kSubtypeNtscf) return make_error_code(AvtpErrc::bad_subtype); + if (len < kNtscfHeaderLen) return make_error_code(AvtpErrc::short_buffer); + out.sequence_num = detail::get_u16(&b[2]); + out.stream_id = StreamId::from_u64(detail::get_u64(&b[4])); + out.control_data_length = detail::get_u16(&b[12]); + return {}; +} + +inline std::vector encode_tscf_header(const TscfHeader& h) { + std::vector buf(kTscfHeaderLen, 0); + buf[0] = kSubtypeTscf; + buf[1] = static_cast(detail::kFlagStreamValid | + (h.timestamp_valid ? detail::kFlagTimestampValid : 0)); + detail::put_u16(&buf[2], h.sequence_num); + detail::put_u64(&buf[4], h.stream_id.to_u64()); + detail::put_u16(&buf[12], h.control_data_length); + detail::put_u32(&buf[14], h.avtp_timestamp); + return buf; +} + +inline std::error_code decode_tscf_header(const uint8_t* b, size_t len, TscfHeader& out) { + if (len < 1) return make_error_code(AvtpErrc::short_buffer); + if (b[0] != kSubtypeTscf) return make_error_code(AvtpErrc::bad_subtype); + if (len < kTscfHeaderLen) return make_error_code(AvtpErrc::short_buffer); + out.timestamp_valid = (b[1] & detail::kFlagTimestampValid) != 0; + out.sequence_num = detail::get_u16(&b[2]); + out.stream_id = StreamId::from_u64(detail::get_u64(&b[4])); + out.control_data_length = detail::get_u16(&b[12]); + out.avtp_timestamp = detail::get_u32(&b[14]); + return {}; +} + +} // namespace avtp +} // namespace rcp diff --git a/include/rcp/can.hpp b/include/rcp/can.hpp index 3aa6de0..51eb741 100644 --- a/include/rcp/can.hpp +++ b/include/rcp/can.hpp @@ -51,7 +51,7 @@ // 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, +// implementation's own, same as the equivalent disclaimers in rcp/avtp.hpp, // rcp/regmap.hpp, rcp/endpoint.hpp, and rcp/uart.hpp. #pragma once diff --git a/include/rcp/discovery.hpp b/include/rcp/discovery.hpp index ce8bedc..f9d2002 100644 --- a/include/rcp/discovery.hpp +++ b/include/rcp/discovery.hpp @@ -14,7 +14,9 @@ // for configuration (extraction §3.1, §3.5). // // ROADMAP.md milestone 46, "Discovery (v2.2.0)": this header rides directly -// on rcp/wire.hpp's NTSCF/ACF_ABB framing and byte_bus_id addressing (v2.0.0) +// on rcp/avtp.hpp's NTSCF framing and rcp/acf.hpp's ACF_ABB message format +// and byte_bus_id addressing (v2.0.0, split from the original rcp/wire.hpp +// per RELAY spec §13.7.2) // — it needs no changes to that codec — queries rcp/lifecycle.hpp's // ServerState to decide whether an incoming discovery request is eligible to // claim the discovery stream (v2.1.0), and targets rcp/regmap.hpp's EP0 @@ -31,12 +33,13 @@ // text from that document is reproduced here. The concrete claim-state // machine and default timeout chosen in this file are this implementation's // own encoding of that behavior, same as the equivalent disclaimers in -// rcp/wire.hpp, rcp/lifecycle.hpp, and rcp/regmap.hpp. +// rcp/avtp.hpp, rcp/acf.hpp, rcp/lifecycle.hpp, and rcp/regmap.hpp. #pragma once +#include +#include #include #include -#include #include #include @@ -53,7 +56,7 @@ namespace discovery { // — and, within whatever EP0 exposes, register-map address 0: the general // bootstrap fields, magic number first among them (extraction §3.5). -constexpr wire::ByteBusId kDiscoveryByteBusId = static_cast(regmap::kEp0); +constexpr avtp::ByteBusId kDiscoveryByteBusId = static_cast(regmap::kEp0); constexpr uint32_t kDiscoveryRegisterAddress = 0; // A discovery read's default response size: just enough to carry the 32-bit @@ -93,13 +96,13 @@ inline std::error_code make_error_code(DiscoveryErrc e) noexcept { // ── Discovery request framing ──────────────────────────────────────────────── // make_discovery_request builds the ACF_ABB-level header for a discovery // read: unconditional read (op=false), targeting byte_bus_id 0 -// (extraction §3.5). It rides on wire::make_standard_request unchanged — +// (extraction §3.5). It rides on acf::make_standard_request unchanged — // discovery is the mandatory standard request kind addressed at a fixed // endpoint, not a distinct wire-level message shape of its own. -inline wire::AcfMessageInfo make_discovery_request(uint8_t transaction_num, - uint16_t read_size = kDiscoveryDefaultReadSize) noexcept { - return wire::make_standard_request(kDiscoveryByteBusId, transaction_num, - /*write=*/false, read_size); +inline acf::AcfMessageInfo make_discovery_request(uint8_t transaction_num, + uint16_t read_size = kDiscoveryDefaultReadSize) noexcept { + return acf::make_standard_request(kDiscoveryByteBusId, transaction_num, + /*write=*/false, read_size); } // encode_discovery_request wraps make_discovery_request's ACF_ABB message in @@ -107,20 +110,20 @@ inline wire::AcfMessageInfo make_discovery_request(uint8_t transaction_num, // NTSCF-only and broadcastable (extraction §3.5) — this function never // produces a TSCF-headed frame, so there is no way to misuse it into // building the kind of request decode_discovery_request below must drop. -inline std::vector encode_discovery_request(const wire::StreamId& stream_id, +inline std::vector encode_discovery_request(const avtp::StreamId& stream_id, uint16_t sequence_num, uint8_t transaction_num, uint16_t read_size = kDiscoveryDefaultReadSize) { const auto info = make_discovery_request(transaction_num, read_size); - const auto acf = wire::encode_acf_abb(info, {}); + const auto acf_msg = acf::encode_acf_abb(info, {}); - wire::NtscfHeader hdr; + avtp::NtscfHeader hdr; hdr.stream_id = stream_id; hdr.sequence_num = sequence_num; - hdr.control_data_length = static_cast(acf.size()); + hdr.control_data_length = static_cast(acf_msg.size()); - auto out = wire::encode_ntscf_header(hdr); - out.insert(out.end(), acf.begin(), acf.end()); + auto out = avtp::encode_ntscf_header(hdr); + out.insert(out.end(), acf_msg.begin(), acf_msg.end()); return out; } @@ -131,20 +134,20 @@ inline std::vector encode_discovery_request(const wire::StreamId& strea // modeling "drop" as a decode failure the caller cannot accidentally ignore // the way it could ignore a boolean flag on an otherwise-successful decode. inline std::error_code decode_discovery_request(const uint8_t* buf, size_t len, - wire::NtscfHeader& out_hdr, - wire::AcfMessageInfo& out_info, + avtp::NtscfHeader& out_hdr, + acf::AcfMessageInfo& out_info, std::vector& out_payload) { - if (len < 1) return wire::make_error_code(wire::WireErrc::short_buffer); - if (buf[0] == wire::kSubtypeTscf) { + if (len < 1) return avtp::make_error_code(avtp::AvtpErrc::short_buffer); + if (buf[0] == avtp::kSubtypeTscf) { return make_error_code(DiscoveryErrc::tscf_headed_request_dropped); } - auto ec = wire::decode_ntscf_header(buf, len, out_hdr); + auto ec = avtp::decode_ntscf_header(buf, len, out_hdr); if (ec) return ec; - const size_t acf_off = wire::kNtscfHeaderLen; - if (len < acf_off) return wire::make_error_code(wire::WireErrc::short_buffer); - return wire::decode_acf_abb(buf + acf_off, len - acf_off, out_info, out_payload); + const size_t acf_off = avtp::kNtscfHeaderLen; + if (len < acf_off) return avtp::make_error_code(avtp::AvtpErrc::short_buffer); + return acf::decode_acf_abb(buf + acf_off, len - acf_off, out_info, out_payload); } // should_answer_discovery documents, as a single always-true call site diff --git a/include/rcp/e2e.hpp b/include/rcp/e2e.hpp index 1ae12bf..aaeba55 100644 --- a/include/rcp/e2e.hpp +++ b/include/rcp/e2e.hpp @@ -16,7 +16,7 @@ // End-to-end CRC safe points — the OPEN Alliance TC18 Remote Control // Protocol Specification v0.5.1_RC's actual E2E integrity mechanism, plus // the per-request-stream watchdog and safe-state primitives that the -// safety-tagged (0x8x) request variants (rcp/sequencer.hpp) depend on +// safety-tagged (0x8x) request variants (rcp/request.hpp) depend on // (extraction §3.8, §4.4, §4.7, §6 item 4). // // ROADMAP.md milestone 50, "E2E CRC Safe Points & Safety-Request Variants @@ -25,19 +25,20 @@ // — the prior ad-hoc CRC-16/CCITT-FALSE + sequence-counter + replay-window // wrapper around rcp.hpp's Controller is discarded, not adapted. Nothing // else in this tree depended on that old API (only this file's own test -// did), so no legacy shim is needed here, unlike rcp/wire.hpp's +// did), so no legacy shim is needed here, unlike rcp/avtp.hpp's/rcp/acf.hpp's // rcp/legacy_wire.hpp split at v2.0.0. // -// This header rides on top of rcp/wire.hpp's AcfMessageInfo/StreamId -// (v2.0.0), rcp/regmap.hpp's RequestStreamConfig/EndpointGenericConfig +// This header rides on top of rcp/acf.hpp's AcfMessageInfo and +// rcp/avtp.hpp's StreamId (v2.0.0, split from the original rcp/wire.hpp per +// RELAY spec §13.7.2), rcp/regmap.hpp's RequestStreamConfig/EndpointGenericConfig // (v2.1.0, expanded to their full v2.6.0 field set alongside this header), -// and rcp/sequencer.hpp's RequestRecord/RequestLedger/SequencerTable -// (v2.5.0/v2.6.0) without modifying any of their core framing — the CRC -// coverage builder below serializes an already-built AcfMessageInfo rather -// than reaching into rcp/wire.hpp's codec internals, and the -// watchdog-overflow queue behavior reuses -// rcp::sequencer::RequestLedger::cancel_all(non_safestate_only) directly -// rather than reimplementing cancellation. +// and rcp/request.hpp's RequestRecord/RequestLedger/SequencerTable +// (v2.5.0/v2.6.0, renamed from rcp/sequencer.hpp per RELAY spec §13.7.2) +// without modifying any of their core framing — the CRC coverage builder +// below serializes an already-built AcfMessageInfo rather than reaching +// into rcp/acf.hpp's codec internals, and the watchdog-overflow queue +// behavior reuses rcp::request::RequestLedger::cancel_all(non_safestate_only) +// directly rather than reimplementing cancellation. // // Field names and behavior below implement TC18's *behavior* as described // in an internal structured extraction of the specification named above; @@ -46,16 +47,17 @@ // class shapes chosen in this file are this implementation's own encoding // of that behavior — full bit-for-bit conformance against other TC18 // implementations is not claimed, same as the equivalent disclaimers in -// rcp/wire.hpp, rcp/regmap.hpp, and rcp/sequencer.hpp. This header -// provides primitives, not a running scheduler or timer thread — deciding -// *when* to call overflowed()/kick()/should_emit_info_notification() is -// left to the embedding application, same as every other header in this +// rcp/avtp.hpp, rcp/acf.hpp, rcp/regmap.hpp, and rcp/request.hpp. This +// header provides primitives, not a running scheduler or timer thread — +// deciding *when* to call overflowed()/kick()/should_emit_info_notification() +// is left to the embedding application, same as every other header in this // codebase. #pragma once +#include +#include #include -#include -#include +#include #include #include @@ -164,15 +166,15 @@ constexpr uint16_t kCrcLengthAdjustOctets = 4; // +4 octets on an outer AVTPDU // kCrcLengthAdjustQuadlets. Call this before encode_acf_abb/encode_acf_gbb // so the trailing CRC's length is already baked into the header the CRC // itself covers. -inline void apply_acf_length_adjustment(wire::AcfMessageInfo& info) noexcept { +inline void apply_acf_length_adjustment(acf::AcfMessageInfo& info) noexcept { info.acf_msg_length = static_cast(info.acf_msg_length + kCrcLengthAdjustQuadlets); } // apply_frame_length_adjustment mutates an outer AVTPDU header's // control_data_length in place, per kCrcLengthAdjustOctets — the paired // +4 octet adjustment at the frame level. Templated over -// wire::NtscfHeader/wire::TscfHeader since both carry a control_data_length -// field of the same name but rcp/wire.hpp deliberately keeps them as two +// avtp::NtscfHeader/avtp::TscfHeader since both carry a control_data_length +// field of the same name but rcp/avtp.hpp deliberately keeps them as two // independent structs with no common base to adjust through instead. template inline void apply_frame_length_adjustment(Header& hdr) noexcept { @@ -200,19 +202,19 @@ inline void put_u64_be(std::vector& buf, uint64_t v) { // already have apply_acf_length_adjustment() applied if the caller wants // the trailer's length reflected in the coverage — this function only // serializes whatever AcfMessageInfo it is given. -inline std::vector coverage_buffer(const wire::StreamId& stream_id, +inline std::vector coverage_buffer(const avtp::StreamId& stream_id, std::optional avtp_timestamp, - const wire::AcfMessageInfo& info, + const acf::AcfMessageInfo& info, const std::vector& payload) { std::vector buf; - buf.reserve(8 + 4 + wire::kAcfCommonHeaderLen + payload.size()); + buf.reserve(8 + 4 + acf::kAcfCommonHeaderLen + payload.size()); detail::put_u64_be(buf, stream_id.to_u64()); detail::put_u32_be(buf, avtp_timestamp.value_or(0)); // zero-filled stand-in under NTSCF - uint8_t hdr[wire::kAcfCommonHeaderLen]; - wire::encode_acf_message_info(info, hdr); - buf.insert(buf.end(), hdr, hdr + wire::kAcfCommonHeaderLen); + uint8_t hdr[acf::kAcfCommonHeaderLen]; + acf::encode_acf_message_info(info, hdr); + buf.insert(buf.end(), hdr, hdr + acf::kAcfCommonHeaderLen); buf.insert(buf.end(), payload.begin(), payload.end()); return buf; @@ -220,8 +222,8 @@ inline std::vector coverage_buffer(const wire::StreamId& stream_id, // compute_crc is coverage_buffer() + crc32() in one call — the usual way a // caller actually wants this used. -inline uint32_t compute_crc(const wire::StreamId& stream_id, std::optional avtp_timestamp, - const wire::AcfMessageInfo& info, const std::vector& payload) { +inline uint32_t compute_crc(const avtp::StreamId& stream_id, std::optional avtp_timestamp, + const acf::AcfMessageInfo& info, const std::vector& payload) { return crc32(coverage_buffer(stream_id, avtp_timestamp, info, payload)); } @@ -239,8 +241,8 @@ inline void append_crc(std::vector& frame, uint32_t crc) { // and compares it against `received_crc` (as decoded from a frame's // trailing 4 octets). Returns E2eErrc::crc_error — the CRC_ERROR failure // path — on mismatch. -inline std::error_code verify_crc(const wire::StreamId& stream_id, std::optional avtp_timestamp, - const wire::AcfMessageInfo& info, const std::vector& payload, +inline std::error_code verify_crc(const avtp::StreamId& stream_id, std::optional avtp_timestamp, + const acf::AcfMessageInfo& info, const std::vector& payload, uint32_t received_crc) { if (compute_crc(stream_id, avtp_timestamp, info, payload) != received_crc) return make_error_code(E2eErrc::crc_error); @@ -270,7 +272,7 @@ inline bool crc_required(const regmap::EndpointGenericConfig& cfg, MessageRole r // caller's server actually implements the E2E CRC safe-point / safety- // request mechanism — an explicit, testable single call site rather than a // bit a caller might set by hand and forget to gate on real support, -// mirroring rcp::sequencer::implemented_options_bits' equivalent pattern. +// mirroring rcp::request::implemented_options_bits' equivalent pattern. inline uint32_t implemented_options_bit(bool safety_requests_implemented) noexcept { return safety_requests_implemented ? regmap::kOptSafetyRequests : 0; } @@ -328,7 +330,7 @@ class RxSequenceGuard { // ── RxWatchdog — rx_wd_* / rx_ovrflw_safestate_enable (extraction §3.8) ────── // Per-request-stream watchdog and safe-state latch. Deciding *when* to call // kick()/overflowed() (i.e. running a timer loop) is left to the embedding -// application, same as rcp::sequencer::select_next_due's equivalent +// application, same as rcp::request::select_next_due's equivalent // "primitives, not a scheduler" split. class RxWatchdog { public: @@ -378,12 +380,12 @@ class RxWatchdog { // canceled exactly as clear-non-safestate (0x06) would, while safety-tagged // (0x8x) records are left untouched so they can go on to drive the // endpoint through its safe-state sequence. This reuses -// sequencer::RequestLedger::cancel_all directly rather than reimplementing -// cancellation — see rcp/sequencer.hpp's header comment. Returns the count +// request::RequestLedger::cancel_all directly rather than reimplementing +// cancellation — see rcp/request.hpp's header comment. Returns the count // cancel_all() itself returns (0 if the feature is disabled, since nothing // is purged in that case). inline size_t apply_watchdog_overflow(const regmap::RequestStreamConfig& cfg, RxWatchdog& wd, - sequencer::RequestLedger& ledger) noexcept { + request::RequestLedger& ledger) noexcept { if (!cfg.rx_wd_safestate_enable) return 0; wd.enter_safe_state(); return ledger.cancel_all(/*non_safestate_only=*/true); @@ -394,7 +396,7 @@ inline size_t apply_watchdog_overflow(const regmap::RequestStreamConfig& cfg, Rx // be configured, via cfg.rx_ovrflw_safestate_enable, to drive the endpoint // into safe state and purge normal requests the same way. inline size_t apply_queue_overflow(const regmap::RequestStreamConfig& cfg, RxWatchdog& wd, - sequencer::RequestLedger& ledger) noexcept { + request::RequestLedger& ledger) noexcept { if (!cfg.rx_ovrflw_safestate_enable) return 0; wd.enter_safe_state(); return ledger.cancel_all(/*non_safestate_only=*/true); @@ -415,7 +417,7 @@ inline size_t apply_queue_overflow(const regmap::RequestStreamConfig& cfg, RxWat // "safe state" is structural: cfg.rx_safestate_sequencer currently holding // exactly cfg.rx_safe_sequencer_state. inline bool endpoint_in_configured_safe_state(const regmap::RequestStreamConfig& cfg, - const sequencer::SequencerTable& sequencers, + const request::SequencerTable& sequencers, bool force_high_impedance_asserted) noexcept { if (cfg.rx_safety_measure == regmap::RxSafetyMeasure::ForceHighImpedance) return force_high_impedance_asserted; @@ -428,9 +430,9 @@ inline bool endpoint_in_configured_safe_state(const regmap::RequestStreamConfig& // may_execute_now gates dispatch of one decoded request record: a // non-safety record is always eligible as far as this rule is concerned // (subject to whatever other checks the caller already applies elsewhere); -// a safety-tagged (0x8x, sequencer::RequestRecord::is_safety) record is +// a safety-tagged (0x8x, request::RequestRecord::is_safety) record is // only eligible once `endpoint_in_safe_state` is true. -inline bool may_execute_now(const sequencer::RequestRecord& rec, bool endpoint_in_safe_state) noexcept { +inline bool may_execute_now(const request::RequestRecord& rec, bool endpoint_in_safe_state) noexcept { return !rec.is_safety || endpoint_in_safe_state; } diff --git a/include/rcp/endpoint.hpp b/include/rcp/endpoint.hpp index 44c3711..57b339b 100644 --- a/include/rcp/endpoint.hpp +++ b/include/rcp/endpoint.hpp @@ -32,7 +32,7 @@ // transfer-complete/per-CS assert/de-assert signals can both be built on // the same primitive without duplicating the bookkeeping // -// This header has no dependency on rcp/wire.hpp's AcfMessageInfo directly — +// This header has no dependency on rcp/acf.hpp's AcfMessageInfo directly — // it operates on the already-decoded evt[2:0] value a caller extracts from // AcfMessageInfo::evt_op, keeping this scaffolding usable independent of any // particular wire encoding decision. @@ -41,7 +41,7 @@ // an internal structured extraction of the specification named above; no // text from that document is reproduced here. The concrete encodings chosen // in this file are this implementation's own, same as the equivalent -// disclaimers in rcp/wire.hpp, rcp/lifecycle.hpp, rcp/regmap.hpp, and +// disclaimers in rcp/avtp.hpp, rcp/lifecycle.hpp, rcp/regmap.hpp, and // rcp/discovery.hpp. #pragma once diff --git a/include/rcp/gpio.hpp b/include/rcp/gpio.hpp index 1af1a44..2fbda0f 100644 --- a/include/rcp/gpio.hpp +++ b/include/rcp/gpio.hpp @@ -19,7 +19,7 @@ // trigger-signal table (v2.3.0), and is also the vehicle that exercises the // add/subtract saturation rule PWM_OUT reuses at v2.4.0. It rides on // rcp/regmap.hpp's generic/functional endpoint config split (v2.1.0) for -// its functional config block and on rcp/wire.hpp's big-endian field codec +// its functional config block and on rcp/avtp.hpp's big-endian field codec // helpers (v2.0.0) for its 4-byte payload, without changing either header. // // Field names and behavior below implement TC18's *behavior* as described in @@ -27,12 +27,12 @@ // text from that document is reproduced here. The concrete pin-direction bit // convention, functional-config layout, 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, and rcp/endpoint.hpp. +// disclaimers in rcp/avtp.hpp, rcp/regmap.hpp, and rcp/endpoint.hpp. #pragma once +#include #include #include -#include #include #include @@ -108,19 +108,19 @@ inline std::error_code apply_gpio_write(endpoint::WriteSemantics op, GpioState& } // ── Payload codec ───────────────────────────────────────────────────────────── -// Big-endian 4-byte encoding, matching rcp/wire.hpp's own field convention +// Big-endian 4-byte encoding, matching rcp/avtp.hpp's own field convention // (reusing its internal put_u32/get_u32 rather than re-deriving byte order // here). inline std::vector encode_gpio_payload(PinMask mask) { std::vector buf(kGpioPayloadLen); - wire::detail::put_u32(buf.data(), mask); + avtp::detail::put_u32(buf.data(), mask); return buf; } inline std::error_code decode_gpio_payload(const uint8_t* buf, size_t len, PinMask& out) noexcept { - if (len < kGpioPayloadLen) return wire::make_error_code(wire::WireErrc::short_buffer); - out = wire::detail::get_u32(buf); + if (len < kGpioPayloadLen) return avtp::make_error_code(avtp::AvtpErrc::short_buffer); + out = avtp::detail::get_u32(buf); return {}; } @@ -174,7 +174,7 @@ inline regmap::EndpointFunctionalConfig encode_gpio_functional_config(PinMask directions, const std::array& enabled_edge_masks) { regmap::EndpointFunctionalConfig cfg; cfg.data.resize(kGpioFunctionalConfigLen); - wire::detail::put_u32(cfg.data.data(), directions); + avtp::detail::put_u32(cfg.data.data(), directions); std::copy(enabled_edge_masks.begin(), enabled_edge_masks.end(), cfg.data.begin() + static_cast(kGpioPayloadLen)); return cfg; @@ -184,8 +184,8 @@ inline std::error_code decode_gpio_functional_config(const regmap::EndpointFunct PinMask& out_directions, std::array& out_enabled_edge_masks) noexcept { if (cfg.data.size() < kGpioFunctionalConfigLen) - return wire::make_error_code(wire::WireErrc::short_buffer); - out_directions = wire::detail::get_u32(cfg.data.data()); + return avtp::make_error_code(avtp::AvtpErrc::short_buffer); + out_directions = avtp::detail::get_u32(cfg.data.data()); std::copy(cfg.data.begin() + static_cast(kGpioPayloadLen), cfg.data.begin() + static_cast(kGpioFunctionalConfigLen), out_enabled_edge_masks.begin()); diff --git a/include/rcp/i2c.hpp b/include/rcp/i2c.hpp index 06ebea4..2a1719c 100644 --- a/include/rcp/i2c.hpp +++ b/include/rcp/i2c.hpp @@ -23,7 +23,7 @@ // the extraction leaves the mapping of I2C's `i2c_mode` field (device // speed grade — Standard/Fast/Fast-mode-Plus/High-Speed) onto wire values // ambiguous beyond the coarse `AcfMessageInfo::hs` "high-speed requested" -// flag rcp/wire.hpp already reserves for this purpose. This header +// flag rcp/acf.hpp already reserves for this purpose. This header // therefore only decodes that coarse bit (see i2c_mode_of below) and does // not attempt to further distinguish the standard-speed grades from each // other; extending i2c_mode_of to do so must wait for a spec errata pass @@ -33,7 +33,7 @@ // 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, +// 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. #pragma once diff --git a/include/rcp/iseled.hpp b/include/rcp/iseled.hpp index b279b04..f1b537a 100644 --- a/include/rcp/iseled.hpp +++ b/include/rcp/iseled.hpp @@ -38,7 +38,7 @@ // 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, +// equivalent disclaimers in rcp/avtp.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, // and rcp/e2e.hpp. #pragma once diff --git a/include/rcp/legacy_wire.hpp b/include/rcp/legacy_wire.hpp index e3bbfd7..3257424 100644 --- a/include/rcp/legacy_wire.hpp +++ b/include/rcp/legacy_wire.hpp @@ -14,10 +14,12 @@ // Binary frame codec for the pre-replacement Zone/Command/Response/Status // model, used today only as the internal frame format of rcp/udp.hpp. // -// This is the old bespoke 16-byte header that rcp/wire.hpp carried before -// the TC18 protocol-replacement roadmap (ROADMAP.md, "44. Wire Format Core -// (v2.0.0)"). rcp/wire.hpp itself has been rebuilt as the IEEE 1722 -// AVTPDU/ACF codec; this file exists solely so rcp/udp.hpp keeps building +// This is the old bespoke 16-byte header that this module (then named +// rcp/wire.hpp) carried before the TC18 protocol-replacement roadmap +// (ROADMAP.md, "44. Wire Format Core (v2.0.0)"). The new format has been +// built as the IEEE 1722 AVTPDU/ACF codec, in rcp/avtp.hpp and rcp/acf.hpp +// (split per RELAY spec §13.7.2's standard module-name registry); this file +// exists solely so rcp/udp.hpp keeps building // and working against the still-current Zone/Command/Response/Status types // until the Phase 13/14 model replacement (v2.1.0 onward) removes them and // rcp/udp.hpp is rebuilt against the new wire format (v2.13.0, per the diff --git a/include/rcp/lifecycle.hpp b/include/rcp/lifecycle.hpp index f633fa1..ffe0180 100644 --- a/include/rcp/lifecycle.hpp +++ b/include/rcp/lifecycle.hpp @@ -27,7 +27,7 @@ // rules and locking policy chosen in this file are this implementation's // own encoding of that behavior — full bit-for-bit register-map // conformance against other TC18 implementations is a later-milestone -// concern, same as rcp/wire.hpp's disclaimer for the wire codec. +// concern, same as rcp/avtp.hpp's disclaimer for the wire codec. #pragma once #include diff --git a/include/rcp/lin.hpp b/include/rcp/lin.hpp index dc76b28..2fd2d91 100644 --- a/include/rcp/lin.hpp +++ b/include/rcp/lin.hpp @@ -34,7 +34,7 @@ // 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, +// implementation's own, same as the equivalent disclaimers in rcp/avtp.hpp, // rcp/regmap.hpp, rcp/endpoint.hpp, and rcp/i2c.hpp. #pragma once diff --git a/include/rcp/mdio.hpp b/include/rcp/mdio.hpp index 0080b07..c0a54cf 100644 --- a/include/rcp/mdio.hpp +++ b/include/rcp/mdio.hpp @@ -27,7 +27,7 @@ // 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, +// implementation's own, same as the equivalent disclaimers in rcp/avtp.hpp, // rcp/regmap.hpp, and rcp/endpoint.hpp. #pragma once diff --git a/include/rcp/pwm.hpp b/include/rcp/pwm.hpp index f24da1c..c2127c1 100644 --- a/include/rcp/pwm.hpp +++ b/include/rcp/pwm.hpp @@ -34,7 +34,7 @@ // unit (left as an opaque implementation-defined tick count, same as // rcp/regmap.hpp's own field-width disclaimers) and trigger-signal id // scheme chosen in this file are this implementation's own, same as the -// equivalent disclaimers in rcp/wire.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, +// equivalent disclaimers in rcp/avtp.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, // and rcp/gpio.hpp. #pragma once diff --git a/include/rcp/rcp.hpp b/include/rcp/rcp.hpp index 160d6d2..e123ad3 100644 --- a/include/rcp/rcp.hpp +++ b/include/rcp/rcp.hpp @@ -62,7 +62,7 @@ // ROADMAP.md milestone 45 ("RC Server Lifecycle & Register-Map Model, // v2.1.0") for why this file could not simply be deleted at that milestone. // The new stream/endpoint/register-map model that supersedes the types -// below lives in rcp/wire.hpp (v2.0.0), rcp/lifecycle.hpp, and +// below lives in rcp/avtp.hpp/rcp/acf.hpp (v2.0.0), rcp/lifecycle.hpp, and // rcp/regmap.hpp (both v2.1.0); do not build new functionality on top of // Zone/Command/Controller/Registry going forward. #pragma once diff --git a/include/rcp/regmap.hpp b/include/rcp/regmap.hpp index 5fe9f9a..7cc50d6 100644 --- a/include/rcp/regmap.hpp +++ b/include/rcp/regmap.hpp @@ -25,7 +25,7 @@ // to bootstrap discovery (v2.2.0) and every endpoint milestone from v2.3.0 // onward, plus EP0 itself — the RC Server acting as a pseudo-endpoint with // whole-map read/write and root-client arbitration. It rides on top of -// rcp/wire.hpp's StreamId/ByteBusId addressing (v2.0.0) and rcp/lifecycle.hpp's +// rcp/avtp.hpp's StreamId/ByteBusId addressing (v2.0.0) and rcp/lifecycle.hpp's // state machine (also v2.1.0); it does not depend on rcp/rcp.hpp's // pre-replacement Zone/Command/Controller/Registry model at all. // @@ -35,7 +35,7 @@ // magic-number value, and table layout are this implementation's own // encoding of that behavior for milestone 45 — full bit-for-bit register-map // conformance against other TC18 implementations is not claimed, same as -// rcp/wire.hpp's equivalent disclaimer for the wire codec. Endpoint *types* +// rcp/avtp.hpp's equivalent disclaimer for the wire codec. Endpoint *types* // (GPIO, SPI, I2C, ...) and their functional config contents are out of // scope here — this milestone only defines the generic/functional config // split and an opaque byte-blob functional config slot; interpreting that @@ -46,12 +46,12 @@ // and EndpointGenericConfig's per-endpoint CRC-enable toggles are expanded // to their full field set at this milestone, superseding the three // placeholder fields v2.1.0 reserved layout for. The behavior that reads -// and acts on these fields lives in rcp/e2e.hpp and rcp/sequencer.hpp, not +// and acts on these fields lives in rcp/e2e.hpp and rcp/request.hpp, not // here — same config-vs-behavior split as everything else in this header. #pragma once +#include #include -#include #include #include @@ -139,7 +139,7 @@ struct HwPinMapEntry { }; // ── Sequencer-state registers (extraction §3.11, §3.16) ────────────────────── -// Persistent 8-bit values; behavior lives in rcp/sequencer.hpp (v2.5.0). +// Persistent 8-bit values; behavior lives in rcp/request.hpp (v2.5.0). // Declared here, ahead of RequestStreamConfig below, since that struct's // rx_safe_sequencer_state field (v2.6.0) needs the type name already // in scope. @@ -162,7 +162,7 @@ enum class RxSafetyMeasure : uint8_t { // ROADMAP.md milestone 50 (v2.6.0) calls for. Behavior that reads and acts // on these fields lives in rcp/e2e.hpp (watchdog overflow, CRC // enforcement, sequence checking, safe-state gating) and -// rcp/sequencer.hpp (the 0x8x safety-tagged request variants these fields +// rcp/request.hpp (the 0x8x safety-tagged request variants these fields // exist to support) — this struct is durable storage only, same // config-vs-behavior split used throughout this header. // @@ -171,7 +171,7 @@ enum class RxSafetyMeasure : uint8_t { // reserve register-map layout ahead of this milestone's real field list. struct RequestStreamConfig { - wire::StreamId stream_id{}; + avtp::StreamId stream_id{}; uint16_t queue_size = 0; // Watchdog (extraction §3.8). rx_wd_timeout_interval's unit @@ -215,7 +215,7 @@ struct RequestStreamConfig { struct EpIdMappingEntry { EndpointId ep_id = 0; - wire::ByteBusId byte_bus_id = 0; + avtp::ByteBusId byte_bus_id = 0; }; // ── Response / ack queue config (extraction §3.10) ─────────────────────────── @@ -253,8 +253,8 @@ struct TablePointer { // pointer/capacity pairs describe. A real server would serialize this to // and from the wire on demand; this milestone models the data, not the wire // encoding of the whole map (individual field reads/writes ride on the -// wire::ByteBusId-addressed standard request already implemented in -// rcp/wire.hpp). +// avtp::ByteBusId-addressed standard request already implemented in +// rcp/acf.hpp). constexpr uint32_t kRegisterMapMagic = 0x52435030; // "RCP0" — this implementation's own placeholder value @@ -302,7 +302,7 @@ struct RegisterMap { // // "Client" here is identified by an opaque index the embedding transport // assigns per connected stream; this header does not know or care how that -// index maps to a wire::StreamId, only that it is stable for the lifetime of +// index maps to a avtp::StreamId, only that it is stable for the lifetime of // the connection. class Ep0 { public: diff --git a/include/rcp/sequencer.hpp b/include/rcp/request.hpp similarity index 85% rename from include/rcp/sequencer.hpp rename to include/rcp/request.hpp index 7f4935f..f029957 100644 --- a/include/rcp/sequencer.hpp +++ b/include/rcp/request.hpp @@ -17,19 +17,27 @@ // sequencer-state advance/reset, cancellation semantics, and the request // lifecycle state machine an OPEN Alliance TC18 Remote Control Protocol // Specification v0.5.1_RC server needs once it goes beyond the mandatory -// "standard" request kind (extraction §2.4, §2.7, §3.1, §3.11, §3.14). +// "standard" request kind (extraction §2.4, §2.7, §3.1, §3.11, §3.14). This +// is RELAY spec §13.7.2's `request` standard-module-name entry: "conditional- +// request taxonomy (compound, compound-wait, triggered, chained, timed) and +// sequencers" — both halves already lived together in this one file, so the +// module (originally rcp/sequencer.hpp) is renamed rcp/request.hpp to match +// the registry rather than split; SequencerTable stays as the internal name +// for the sequencer-state sub-concept it specifically models. // // ROADMAP.md milestone 49, "Conditional-Request Taxonomy & Sequencers -// (v2.5.0)": this header rides on top of rcp/wire.hpp's existing ACF_GBB -// support (v2.0.0) and rcp/regmap.hpp's existing `sequencer_states` storage -// (v2.1.0) without changing either — the mtv=0 repurposing trick is a new -// decode path over already-implemented fields, and sequencer *behavior* -// (default value, advance-on-finalization) is built on top of regmap.hpp's -// already-allocated storage rather than duplicating it. It also consumes -// rcp/spi.hpp's compound_wait_matches and rcp/i2c.hpp's -// compound_wait_matches_bits as the per-endpoint-type condition-comparison -// rules a real compound-wait dispatch loop would call once it decides a -// compound-wait request is due; this header does not re-implement either. +// (v2.5.0)": this header rides on top of rcp/acf.hpp's existing ACF_GBB +// support (v2.0.0, split from the original rcp/wire.hpp into rcp/avtp.hpp +// and rcp/acf.hpp per RELAY spec §13.7.2) and rcp/regmap.hpp's existing +// `sequencer_states` storage (v2.1.0) without changing either — the mtv=0 +// repurposing trick is a new decode path over already-implemented fields, +// and sequencer *behavior* (default value, advance-on-finalization) is built +// on top of regmap.hpp's already-allocated storage rather than duplicating +// it. It also consumes rcp/spi.hpp's compound_wait_matches and +// rcp/i2c.hpp's compound_wait_matches_bits as the per-endpoint-type +// condition-comparison rules a real compound-wait dispatch loop would call +// once it decides a compound-wait request is due; this header does not +// re-implement either. // // ROADMAP.md milestone 50, "E2E CRC Safe Points & Safety-Request Variants // (v2.6.0)", extends this taxonomy in place rather than superseding it: the @@ -50,16 +58,17 @@ // mapping, the param-byte layout within the repurposed timestamp slot, and // the request-ledger data model chosen in this file are this // implementation's own encoding of that behavior, same as the equivalent -// disclaimers in rcp/wire.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, -// rcp/spi.hpp, and rcp/i2c.hpp. This header models the taxonomy, the -// bundling rules, and the state machine's transitions and effects — it does -// not implement a running scheduler thread; wiring select_next_due()'s -// output into an actual dispatch loop is left to the embedding application, -// same as every other endpoint header in this codebase. +// disclaimers in rcp/avtp.hpp, rcp/acf.hpp, rcp/regmap.hpp, +// rcp/endpoint.hpp, rcp/spi.hpp, and rcp/i2c.hpp. This header models the +// taxonomy, the bundling rules, and the state machine's transitions and +// effects — it does not implement a running scheduler thread; wiring +// select_next_due()'s output into an actual dispatch loop is left to the +// embedding application, same as every other endpoint header in this +// codebase. #pragma once +#include #include -#include #include #include @@ -71,11 +80,11 @@ #include namespace rcp { -namespace sequencer { +namespace request { // ── Errors ──────────────────────────────────────────────────────────────────── -enum class SequencerErrc : int { +enum class RequestErrc : int { timestamp_not_repurposed = 1, // mtv=1: message_timestamp is a real timestamp, not a request_type opcode unknown_request_type = 2, // byte 0 of the repurposed slot is not one of the defined opcodes index_out_of_range = 3, // SequencerTable index >= table size @@ -87,32 +96,32 @@ enum class SequencerErrc : int { compound_bundle_incomplete = 9, // compound/compound-wait claimed without every required companion capability }; -inline const std::error_category& sequencer_category() noexcept { +inline const std::error_category& request_category() noexcept { struct Cat : std::error_category { - const char* name() const noexcept override { return "rcp.sequencer"; } + const char* name() const noexcept override { return "rcp.request"; } std::string message(int ev) const override { - switch (static_cast(ev)) { - case SequencerErrc::timestamp_not_repurposed: - return "rcp/sequencer: mtv=1 — message_timestamp is not a repurposed request_type slot"; - case SequencerErrc::unknown_request_type: - return "rcp/sequencer: unrecognized request_type opcode"; - case SequencerErrc::index_out_of_range: - return "rcp/sequencer: sequencer index out of range"; - case SequencerErrc::unknown_transaction: - return "rcp/sequencer: transaction_num is not tracked by this ledger"; - case SequencerErrc::invalid_lifecycle_transition: - return "rcp/sequencer: requested lifecycle transition is not the next state in sequence"; - case SequencerErrc::transaction_num_collision: - return "rcp/sequencer: transaction_num is already tracked by this ledger"; - case SequencerErrc::request_not_found: - return "rcp/sequencer: REQUEST_NOT_FOUND"; - case SequencerErrc::request_canceled: - return "rcp/sequencer: REQUEST_CANCELED"; - case SequencerErrc::compound_bundle_incomplete: - return "rcp/sequencer: compound support requires compound-wait, clear-non-safestate, " + switch (static_cast(ev)) { + case RequestErrc::timestamp_not_repurposed: + return "rcp/request: mtv=1 — message_timestamp is not a repurposed request_type slot"; + case RequestErrc::unknown_request_type: + return "rcp/request: unrecognized request_type opcode"; + case RequestErrc::index_out_of_range: + return "rcp/request: sequencer index out of range"; + case RequestErrc::unknown_transaction: + return "rcp/request: transaction_num is not tracked by this ledger"; + case RequestErrc::invalid_lifecycle_transition: + return "rcp/request: requested lifecycle transition is not the next state in sequence"; + case RequestErrc::transaction_num_collision: + return "rcp/request: transaction_num is already tracked by this ledger"; + case RequestErrc::request_not_found: + return "rcp/request: REQUEST_NOT_FOUND"; + case RequestErrc::request_canceled: + return "rcp/request: REQUEST_CANCELED"; + case RequestErrc::compound_bundle_incomplete: + return "rcp/request: compound support requires compound-wait, clear-non-safestate, " "and >=4 sequencers together, not compound alone"; default: - return "rcp/sequencer: unknown error"; + return "rcp/request: unknown error"; } } }; @@ -120,18 +129,18 @@ inline const std::error_category& sequencer_category() noexcept { return instance; } -inline std::error_code make_error_code(SequencerErrc e) noexcept { - return {static_cast(e), sequencer_category()}; +inline std::error_code make_error_code(RequestErrc e) noexcept { + return {static_cast(e), request_category()}; } // ── request_type opcode — the message_timestamp-repurposing trick ──────────── // When an ACF_GBB message's `mtv` bit is clear, the 64-bit message_timestamp // slot it would otherwise carry (and which ACF_GBB always reserves space for -// regardless of `mtv`, per rcp/wire.hpp) is repurposed: its first byte +// regardless of `mtv`, per rcp/acf.hpp) is repurposed: its first byte // becomes this opcode, and the remaining 7 bytes carry opcode-specific // parameters (extraction §2.7). The eight opcodes below are every kind this // milestone defines: five conditional request kinds plus three cancellation -// kinds. The mandatory "standard" request kind (rcp::wire::RequestKind) has +// kinds. The mandatory "standard" request kind (rcp::acf::RequestKind) has // no opcode here at all — it is always carried as ACF_ABB, which has no // message_timestamp slot to repurpose in the first place. @@ -206,7 +215,7 @@ constexpr bool is_safety_variant(RequestTypeOpcode type) noexcept { // encode_request_type packs `type` into the top byte and `params` into the // remaining 7 bytes of a 64-bit value, most-significant byte first — the -// same big-endian convention rcp/wire.hpp's put_u64 uses elsewhere in this +// same big-endian convention rcp/avtp.hpp's put_u64 uses elsewhere in this // codebase, kept consistent here even though this file does not call that // (internal, unexported) helper directly. constexpr uint64_t encode_request_type(RequestTypeOpcode type, const std::array& params) noexcept { @@ -226,9 +235,9 @@ constexpr uint64_t encode_request_type(RequestTypeOpcode type, const std::array< inline std::error_code decode_request_type(bool mtv, uint64_t message_timestamp, RequestTypeOpcode& out_type, std::array& out_params) noexcept { - if (mtv) return make_error_code(SequencerErrc::timestamp_not_repurposed); + if (mtv) return make_error_code(RequestErrc::timestamp_not_repurposed); const uint8_t byte0 = static_cast((message_timestamp >> 56) & 0xFF); - if (!is_valid_request_type(byte0)) return make_error_code(SequencerErrc::unknown_request_type); + if (!is_valid_request_type(byte0)) return make_error_code(RequestErrc::unknown_request_type); out_type = static_cast(byte0); for (size_t i = 0; i < 7; ++i) out_params[i] = static_cast((message_timestamp >> (48 - 8 * i)) & 0xFF); @@ -239,13 +248,13 @@ inline std::error_code decode_request_type(bool mtv, uint64_t message_timestamp, // conditional/cancellation request: always ACF_GBB, always mtv=false (the // repurposing trick above requires it), with `cs` set per the caller's // request-kind-specific meaning (see the `cs` section below). Callers pass -// the result to rcp::wire::encode_acf_gbb alongside encode_request_type's +// the result to rcp::acf::encode_acf_gbb alongside encode_request_type's // output as the message_timestamp argument — no new wire encoding is // introduced here, only this semantic layer over the existing one. -inline wire::AcfMessageInfo make_conditional_request(wire::ByteBusId bus_id, uint8_t transaction_num, - bool cs) noexcept { - wire::AcfMessageInfo info; - info.acf_msg_type = wire::kAcfMsgTypeGbb; +inline acf::AcfMessageInfo make_conditional_request(avtp::ByteBusId bus_id, uint8_t transaction_num, + bool cs) noexcept { + acf::AcfMessageInfo info; + info.acf_msg_type = acf::kAcfMsgTypeGbb; info.mtv = false; info.byte_bus_id = bus_id; info.transaction_num = transaction_num; @@ -334,7 +343,7 @@ inline std::optional select_next_due(const std::vector& du } // ── The `cs` field's two request-kind-specific meanings ─────────────────────── -// AcfMessageInfo::cs (rcp/wire.hpp) is a single wire bit whose meaning +// AcfMessageInfo::cs (rcp/acf.hpp) is a single wire bit whose meaning // depends entirely on which conditional request kind it rides on (extraction // §2.4): for compound-wait it selects when the wait condition is first // checked; for chained it selects whether a successor cares about its @@ -388,7 +397,7 @@ inline std::error_code validate_feature_bundles(const FeatureSet& f) noexcept { if (!claims_compound_support) return {}; const bool complete = f.compound && f.compound_wait && f.clear_non_safestate && f.sequencer_count >= kMinCompoundSequencers; - return complete ? std::error_code{} : make_error_code(SequencerErrc::compound_bundle_incomplete); + return complete ? std::error_code{} : make_error_code(RequestErrc::compound_bundle_incomplete); } // implemented_options_bits reports the svr_implemented_options bit(s) @@ -426,7 +435,7 @@ class SequencerTable { size_t size() const noexcept { return states_.size(); } std::error_code state_of(size_t index, regmap::SequencerState& out) const noexcept { - if (index >= states_.size()) return make_error_code(SequencerErrc::index_out_of_range); + if (index >= states_.size()) return make_error_code(RequestErrc::index_out_of_range); out = states_[index]; return {}; } @@ -441,7 +450,7 @@ class SequencerTable { // not match" for a failure of the call itself. std::error_code try_advance(size_t index, regmap::SequencerState expected_start, bool& out_advanced) noexcept { - if (index >= states_.size()) return make_error_code(SequencerErrc::index_out_of_range); + if (index >= states_.size()) return make_error_code(RequestErrc::index_out_of_range); if (states_[index] == expected_start) { states_[index] = static_cast(states_[index] + 1); out_advanced = true; @@ -533,7 +542,7 @@ class RequestLedger { // construct a fresh ledger). std::error_code submit(RequestRecord rec) noexcept { if (find(rec.transaction_num) != nullptr) - return make_error_code(SequencerErrc::transaction_num_collision); + return make_error_code(RequestErrc::transaction_num_collision); rec.state = RequestState::Pending; rec.arrival_seq = next_arrival_seq_++; records_.push_back(std::move(rec)); @@ -575,10 +584,10 @@ class RequestLedger { // executing requests finish", extraction §2.7). std::error_code cancel_single(uint8_t txn) noexcept { auto* rec = find_mut(txn); - if (rec == nullptr) return make_error_code(SequencerErrc::request_not_found); + if (rec == nullptr) return make_error_code(RequestErrc::request_not_found); if (rec->state == RequestState::UnderExecution || rec->state == RequestState::Finalized || rec->state == RequestState::Canceled) - return make_error_code(SequencerErrc::request_not_found); + return make_error_code(RequestErrc::request_not_found); cascade_cancel(txn); return {}; } @@ -609,7 +618,7 @@ class RequestLedger { std::error_code outcome_of(uint8_t txn, std::error_code& out) const noexcept { const auto* rec = find(txn); - if (rec == nullptr) return make_error_code(SequencerErrc::unknown_transaction); + if (rec == nullptr) return make_error_code(RequestErrc::unknown_transaction); out = rec->outcome.value_or(std::error_code{}); return {}; } @@ -625,8 +634,8 @@ class RequestLedger { std::error_code transition(uint8_t txn, RequestState from, RequestState to) noexcept { auto* rec = find_mut(txn); - if (rec == nullptr) return make_error_code(SequencerErrc::unknown_transaction); - if (rec->state != from) return make_error_code(SequencerErrc::invalid_lifecycle_transition); + if (rec == nullptr) return make_error_code(RequestErrc::unknown_transaction); + if (rec->state != from) return make_error_code(RequestErrc::invalid_lifecycle_transition); rec->state = to; return {}; } @@ -642,7 +651,7 @@ class RequestLedger { rec->state == RequestState::Canceled) return; rec->state = RequestState::Canceled; - rec->outcome = make_error_code(SequencerErrc::request_canceled); + rec->outcome = make_error_code(RequestErrc::request_canceled); for (uint8_t succ : rec->chained_successors) cascade_cancel(succ); } @@ -672,11 +681,11 @@ class RequestLedger { size_t next_arrival_seq_ = 0; }; -} // namespace sequencer +} // namespace request } // namespace rcp -// Enable std::error_code construction from rcp::sequencer::SequencerErrc. +// Enable std::error_code construction from rcp::request::RequestErrc. namespace std { template <> -struct is_error_code_enum : true_type {}; +struct is_error_code_enum : true_type {}; } // namespace std diff --git a/include/rcp/spi.hpp b/include/rcp/spi.hpp index a826d28..7f6bcdc 100644 --- a/include/rcp/spi.hpp +++ b/include/rcp/spi.hpp @@ -27,7 +27,7 @@ // text from that document is reproduced here. The concrete channel-config // encoding and trigger-signal id scheme 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/gpio.hpp. +// rcp/avtp.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, and rcp/gpio.hpp. #pragma once #include diff --git a/include/rcp/uart.hpp b/include/rcp/uart.hpp index 770a7ee..5aa6208 100644 --- a/include/rcp/uart.hpp +++ b/include/rcp/uart.hpp @@ -27,7 +27,7 @@ // response can always be carried in a single, unfragmented AVTPDU. This is // a conservative implementation-chosen ceiling, not a value derived from // the wire format's own read_size field width (which is a full uint16_t, -// see rcp/wire.hpp's AcfMessageInfo::read_size_or_segment_num) — a real +// see rcp/acf.hpp's AcfMessageInfo::read_size_or_segment_num) — a real // single-AVTPDU budget depends on MTU and other-header overhead this header // does not model, so kMaxReadSize is deliberately conservative rather than // computed exactly. @@ -36,7 +36,7 @@ // an internal structured extraction of the specification named above; no // 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/wire.hpp, +// 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. #pragma once diff --git a/include/rcp/udp.hpp b/include/rcp/udp.hpp index fcb2dbf..0c965ee 100644 --- a/include/rcp/udp.hpp +++ b/include/rcp/udp.hpp @@ -18,7 +18,7 @@ // // Frame format is defined in rcp/legacy_wire.hpp — the pre-replacement // 16-byte Zone/Command/Response/Status frame, not the TC18 AVTPDU/ACF codec -// now in rcp/wire.hpp. This transport is rebuilt to carry real AVTPDU +// now in rcp/avtp.hpp and rcp/acf.hpp. This transport is rebuilt to carry real AVTPDU // frames at v2.13.0 (ROADMAP.md); until then it deliberately keeps using // the old frame format so it keeps building and working against the // still-current Zone/Command model. diff --git a/include/rcp/wakeup.hpp b/include/rcp/wakeup.hpp index 3803bcc..8e6f523 100644 --- a/include/rcp/wakeup.hpp +++ b/include/rcp/wakeup.hpp @@ -13,10 +13,10 @@ // 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 +// RequestTypeOpcode kinds rcp/request.hpp's decode_request_type decodes // (v2.5.0/v2.6.0) — the two are unrelated request-shape mechanisms that // happen to both repurpose part of a message for a request-kind byte. This -// header has no dependency on rcp/sequencer.hpp and does not call +// header has no dependency on rcp/request.hpp and does not call // decode_request_type anywhere: decode_sleep_cmd below is Wakeup control's // own, separate decode path, and a future reader must not route SleepCMD // through the conditional-request taxonomy or vice versa. @@ -31,7 +31,7 @@ // 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. +// rcp/avtp.hpp, rcp/regmap.hpp, rcp/endpoint.hpp, and rcp/request.hpp. #pragma once #include diff --git a/include/rcp/wire.hpp b/include/rcp/wire.hpp deleted file mode 100644 index 0bf4929..0000000 --- a/include/rcp/wire.hpp +++ /dev/null @@ -1,477 +0,0 @@ -// fusa:req REQ-WIRE-001 -// fusa:req REQ-WIRE-002 -// fusa:req REQ-WIRE-003 -// fusa:req REQ-WIRE-004 -// fusa:req REQ-WIRE-005 -// fusa:req REQ-WIRE-006 -// fusa:req REQ-WIRE-007 -// fusa:req REQ-WIRE-008 -// fusa:req REQ-WIRE-009 -// fusa:req REQ-WIRE-010 -// fusa:req REQ-WIRE-011 -// fusa:req REQ-WIRE-012 -// fusa:req REQ-WIRE-013 -// fusa:req REQ-WIRE-014 - -// TC18 wire codec — IEEE 1722 AVTPDU framing (NTSCF/TSCF) plus the ACF_ABB / -// ACF_GBB message types the OPEN Alliance TC18 Remote Control Protocol -// Specification v0.5.1_RC layers on top of them. -// -// ROADMAP.md milestone 44, "Wire Format Core (v2.0.0)": this header replaces -// the old bespoke 16-byte frame this file used to define (now preserved -// as-is, unrenamed in behavior, under rcp/legacy_wire.hpp for rcp/udp.hpp's -// benefit until the transport itself is rebuilt at v2.13.0). -// -// Scope note: this module is a pure wire codec. It has no dependency on -// rcp.hpp's Zone/Command/Controller/Registry model, and no dependency on any -// RC Server lifecycle state, register-map, discovery, or endpoint-specific -// behavior — those are later Phase 13 milestones (v2.1.0 onward), sequenced -// on top of this header, not into it. Conditional request kinds beyond the -// mandatory "standard" kind (v2.5.0), the E2E CRC safe-point mechanism -// (v2.6.0), and fragmentation (deferred indefinitely, v2.8.0) are likewise -// out of scope here; the fields they need already exist below so later -// milestones can be layered on without reshaping this header. -// -// Transport-agnostic by design: nothing in this module assumes the AVTPDU -// travels over raw Ethernet. The same framing is meant to sit under a native -// IEEE 1722 Ethernet transport, IEEE 1722-over-UDP/IP (v2.13.0), or a -// CAN(FD/XL) transport — no socket, header, or byte-order assumption here is -// specific to any one of those. -// -// 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-packing -// chosen in this file is this implementation's own encoding of that -// behavior for milestone 44 — full bit-for-bit wire conformance against -// other TC18 implementations is not claimed until v2.6.0 lands, per the -// Phase 13 introduction in ROADMAP.md. -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace rcp { -namespace wire { - -// ── AVTPDU subtype identifiers ──────────────────────────────────────────────── -// From the IEEE 1722 AVTP subtype space — a separate, publicly published -// standard from the confidential TC18 document cited above. NTSCF and TSCF -// are the two control formats TC18 builds its messaging on (extraction §2.2). - -constexpr uint8_t kSubtypeTscf = 0x05; // Time-Synchronous Control Format -constexpr uint8_t kSubtypeNtscf = 0x82; // Non-Time-Synchronous Control Format - -// ── ACF message types ───────────────────────────────────────────────────────── -// The two ACF (AVTP Control Format) message types RCP defines on top of the -// AVTPDU framing above (extraction §2.3). - -constexpr uint8_t kAcfMsgTypeGbb = 0x0D; // ACF_GBB — carries a 64-bit message_timestamp slot -constexpr uint8_t kAcfMsgTypeAbb = 0x0E; // ACF_ABB — no timestamp field at all - -// ── Errors ──────────────────────────────────────────────────────────────────── - -enum class WireErrc : int { - short_buffer = 1, // fewer bytes available than the header/field requires - bad_subtype = 2, // AVTPDU subtype byte is neither NTSCF nor TSCF - bad_acf_msg_type = 3, // acf_msg_type is neither ACF_ABB nor ACF_GBB - length_mismatch = 4, // control_data_length / acf_msg_length disagrees with buffer size -}; - -inline const std::error_category& wire_category() noexcept { - struct Cat : std::error_category { - const char* name() const noexcept override { return "rcp.wire"; } - std::string message(int ev) const override { - switch (static_cast(ev)) { - case WireErrc::short_buffer: return "rcp/wire: buffer too short"; - case WireErrc::bad_subtype: return "rcp/wire: unrecognized AVTPDU subtype"; - case WireErrc::bad_acf_msg_type: return "rcp/wire: unrecognized ACF message type"; - case WireErrc::length_mismatch: return "rcp/wire: length field does not match buffer size"; - default: return "rcp/wire: unknown error"; - } - } - }; - static Cat instance; - return instance; -} - -inline std::error_code make_error_code(WireErrc e) noexcept { - return {static_cast(e), wire_category()}; -} - -// ── Internal helpers ────────────────────────────────────────────────────────── - -namespace detail { - -inline void put_u16(uint8_t* p, uint16_t v) noexcept { - p[0] = static_cast(v >> 8); - p[1] = static_cast(v & 0xFF); -} -inline void put_u32(uint8_t* p, uint32_t v) noexcept { - p[0] = static_cast(v >> 24); - p[1] = static_cast((v >> 16) & 0xFF); - p[2] = static_cast((v >> 8) & 0xFF); - p[3] = static_cast(v & 0xFF); -} -inline void put_u64(uint8_t* p, uint64_t v) noexcept { - for (int i = 0; i < 8; ++i) - p[i] = static_cast((v >> (56 - 8 * i)) & 0xFF); -} -inline uint16_t get_u16(const uint8_t* p) noexcept { - return static_cast((uint16_t(p[0]) << 8) | p[1]); -} -inline uint32_t get_u32(const uint8_t* p) noexcept { - return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) | - (uint32_t(p[2]) << 8) | uint32_t(p[3]); -} -inline uint64_t get_u64(const uint8_t* p) noexcept { - uint64_t v = 0; - for (int i = 0; i < 8; ++i) - v = (v << 8) | uint64_t(p[i]); - return v; -} - -} // namespace detail - -// ── StreamId ────────────────────────────────────────────────────────────────── -// stream_id addresses the sender of an AVTPDU: the sender's 48-bit MAC -// address plus a 16-bit suffix the sender assigns locally to distinguish its -// own concurrent streams (extraction §2.1). Two different senders' stream_id -// values are only guaranteed distinct because their MAC halves differ; the -// suffix alone is not a global identifier. - -struct StreamId { - std::array mac{}; - uint16_t suffix = 0; - - uint64_t to_u64() const noexcept { - uint64_t v = 0; - for (uint8_t b : mac) v = (v << 8) | uint64_t(b); - return (v << 16) | uint64_t(suffix); - } - - static StreamId from_u64(uint64_t v) noexcept { - StreamId id; - id.suffix = static_cast(v & 0xFFFF); - uint64_t mac_bits = v >> 16; - for (int i = 5; i >= 0; --i) { - id.mac[static_cast(i)] = static_cast(mac_bits & 0xFF); - mac_bits >>= 8; - } - return id; - } -}; - -inline bool operator==(const StreamId& a, const StreamId& b) noexcept { - return a.mac == b.mac && a.suffix == b.suffix; -} -inline bool operator!=(const StreamId& a, const StreamId& b) noexcept { return !(a == b); } - -// ── ByteBusId ───────────────────────────────────────────────────────────────── -// byte_bus_id addresses a target endpoint's request path *within* one -// stream_id. Rule (extraction §2.1): byte_bus_id is unique only within the -// owning stream, never globally — two different clients' streams may reuse -// the same byte_bus_id for unrelated endpoints — and a response or ack MUST -// echo the byte_bus_id of the request it answers, unchanged. This module -// enforces the echo half of that rule structurally: see make_response() -// below, which always copies byte_bus_id (and transaction_num) from the -// request it is built from rather than accepting them as free parameters. - -using ByteBusId = uint8_t; - -// ── NTSCF / TSCF AVTPDU headers ─────────────────────────────────────────────── -// -// NTSCF is non-time-synchronous and, per the target protocol, server-only -// outbound; TSCF is time-synchronous and client-only (extraction §2.2). This -// codec does not know which local role is encoding or decoding a given -// frame, so it cannot itself refuse a TSCF frame built by "the server" — -// enforcing that direction rule is the transport/application layer's job -// once it knows its own role; this module only documents it here and -// exposes the two header shapes distinctly enough that misusing one for the -// other's direction is a visible, deliberate choice rather than an accident. - -struct NtscfHeader { - StreamId stream_id{}; - uint16_t sequence_num = 0; // per-stream_id AVTPDU counter - uint16_t control_data_length = 0; // byte length of the ACF message that follows -}; - -struct TscfHeader { - StreamId stream_id{}; - uint16_t sequence_num = 0; - uint16_t control_data_length = 0; - bool timestamp_valid = false; // "tv" — whether avtp_timestamp below is meaningful - uint32_t avtp_timestamp = 0; // 32-bit; TSCF-only (extraction §2.6) -}; - -constexpr size_t kNtscfHeaderLen = 14; // 1 subtype + 1 flags + 2 seq + 8 stream_id + 2 cdl -constexpr size_t kTscfHeaderLen = 18; // kNtscfHeaderLen + 4 avtp_timestamp - -namespace detail { -constexpr uint8_t kFlagStreamValid = 0x80; // "sv" — always set for these control formats -constexpr uint8_t kFlagTimestampValid = 0x40; // "tv" — TSCF only -} // namespace detail - -inline std::vector encode_ntscf_header(const NtscfHeader& h) { - std::vector buf(kNtscfHeaderLen, 0); - buf[0] = kSubtypeNtscf; - buf[1] = detail::kFlagStreamValid; - detail::put_u16(&buf[2], h.sequence_num); - detail::put_u64(&buf[4], h.stream_id.to_u64()); - detail::put_u16(&buf[12], h.control_data_length); - return buf; -} - -inline std::error_code decode_ntscf_header(const uint8_t* b, size_t len, NtscfHeader& out) { - if (len < 1) return make_error_code(WireErrc::short_buffer); - if (b[0] != kSubtypeNtscf) return make_error_code(WireErrc::bad_subtype); - if (len < kNtscfHeaderLen) return make_error_code(WireErrc::short_buffer); - out.sequence_num = detail::get_u16(&b[2]); - out.stream_id = StreamId::from_u64(detail::get_u64(&b[4])); - out.control_data_length = detail::get_u16(&b[12]); - return {}; -} - -inline std::vector encode_tscf_header(const TscfHeader& h) { - std::vector buf(kTscfHeaderLen, 0); - buf[0] = kSubtypeTscf; - buf[1] = static_cast(detail::kFlagStreamValid | - (h.timestamp_valid ? detail::kFlagTimestampValid : 0)); - detail::put_u16(&buf[2], h.sequence_num); - detail::put_u64(&buf[4], h.stream_id.to_u64()); - detail::put_u16(&buf[12], h.control_data_length); - detail::put_u32(&buf[14], h.avtp_timestamp); - return buf; -} - -inline std::error_code decode_tscf_header(const uint8_t* b, size_t len, TscfHeader& out) { - if (len < 1) return make_error_code(WireErrc::short_buffer); - if (b[0] != kSubtypeTscf) return make_error_code(WireErrc::bad_subtype); - if (len < kTscfHeaderLen) return make_error_code(WireErrc::short_buffer); - out.timestamp_valid = (b[1] & detail::kFlagTimestampValid) != 0; - out.sequence_num = detail::get_u16(&b[2]); - out.stream_id = StreamId::from_u64(detail::get_u64(&b[4])); - out.control_data_length = detail::get_u16(&b[12]); - out.avtp_timestamp = detail::get_u32(&b[14]); - return {}; -} - -// ── ACF shared header ("byte_message_info") ─────────────────────────────────── -// Fields common to both ACF_ABB and ACF_GBB messages (extraction §2.4). Every -// field the roadmap calls out is represented individually below rather than -// left packed in an opaque byte, so later milestones (compound requests, -// safety variants, …) can extend behavior around a given field without -// having to re-derive its bit position from scratch. - -struct AcfMessageInfo { - uint8_t acf_msg_type = kAcfMsgTypeAbb; // ACF_ABB or ACF_GBB - uint16_t acf_msg_length = 0; // quadlets, including this shared header - uint8_t pad = 0; // 0–3 trailing pad bytes added for quadlet alignment - bool mtv = false; // message_timestamp valid (ACF_GBB only) - ByteBusId byte_bus_id = 0; // target endpoint, unique within stream_id only - bool evt_ack = false; // evt[3] — acknowledge flag - uint8_t evt_op = 0; // evt[2:0] — endpoint-defined sub-opcode - bool hs = false; // reserved for endpoint-specific use (e.g. I2C high-speed) - bool cs = false; // conditional-start; meaning depends on request kind (v2.5.0) - uint8_t transaction_num = 0; // correlates a request with its response/ack - bool op = false; // false = read, true = write (this codec's convention) - bool rsp = false; // set on every response/ack, clear on requests - bool err = false; // set alongside rsp for an error response - bool ms = false; // "more segments" — fragmentation, deferred to v2.8.0 - uint16_t read_size_or_segment_num = 0; // read_size when !ms, segment_num when ms -}; - -constexpr size_t kAcfCommonHeaderLen = 10; -constexpr size_t kAcfGbbTimestampLen = 8; - -namespace detail { -constexpr uint8_t kByte3MtvBit = 0x80; -constexpr uint8_t kByte3PadShift = 5; -constexpr uint8_t kByte3PadMask = 0x03; -constexpr uint8_t kByte5AckBit = 0x80; -constexpr uint8_t kByte5OpShift = 4; -constexpr uint8_t kByte5OpMask = 0x07; -constexpr uint8_t kByte5HsBit = 0x08; -constexpr uint8_t kByte5CsBit = 0x04; -constexpr uint8_t kByte7OpBit = 0x80; -constexpr uint8_t kByte7RspBit = 0x40; -constexpr uint8_t kByte7ErrBit = 0x20; -constexpr uint8_t kByte7MsBit = 0x10; -} // namespace detail - -inline void encode_acf_message_info(const AcfMessageInfo& info, uint8_t* out10) noexcept { - out10[0] = info.acf_msg_type; - detail::put_u16(&out10[1], info.acf_msg_length); - out10[3] = static_cast( - (info.mtv ? detail::kByte3MtvBit : 0) | - (static_cast(info.pad & detail::kByte3PadMask) << detail::kByte3PadShift)); - out10[4] = info.byte_bus_id; - out10[5] = static_cast( - (info.evt_ack ? detail::kByte5AckBit : 0) | - (static_cast(info.evt_op & detail::kByte5OpMask) << detail::kByte5OpShift) | - (info.hs ? detail::kByte5HsBit : 0) | - (info.cs ? detail::kByte5CsBit : 0)); - out10[6] = info.transaction_num; - out10[7] = static_cast( - (info.op ? detail::kByte7OpBit : 0) | - (info.rsp ? detail::kByte7RspBit : 0) | - (info.err ? detail::kByte7ErrBit : 0) | - (info.ms ? detail::kByte7MsBit : 0)); - detail::put_u16(&out10[8], info.read_size_or_segment_num); -} - -inline void decode_acf_message_info(const uint8_t* in10, AcfMessageInfo& out) noexcept { - out.acf_msg_type = in10[0]; - out.acf_msg_length = detail::get_u16(&in10[1]); - out.mtv = (in10[3] & detail::kByte3MtvBit) != 0; - out.pad = static_cast((in10[3] >> detail::kByte3PadShift) & detail::kByte3PadMask); - out.byte_bus_id = in10[4]; - out.evt_ack = (in10[5] & detail::kByte5AckBit) != 0; - out.evt_op = static_cast((in10[5] >> detail::kByte5OpShift) & detail::kByte5OpMask); - out.hs = (in10[5] & detail::kByte5HsBit) != 0; - out.cs = (in10[5] & detail::kByte5CsBit) != 0; - out.transaction_num = in10[6]; - out.op = (in10[7] & detail::kByte7OpBit) != 0; - out.rsp = (in10[7] & detail::kByte7RspBit) != 0; - out.err = (in10[7] & detail::kByte7ErrBit) != 0; - out.ms = (in10[7] & detail::kByte7MsBit) != 0; - out.read_size_or_segment_num = detail::get_u16(&in10[8]); -} - -// ── ACF_ABB / ACF_GBB message encode / decode ───────────────────────────────── -// ACF_ABB carries no timestamp field at all; ACF_GBB always reserves a -// 64-bit message_timestamp slot immediately after the shared header, -// regardless of whether `mtv` marks it valid (extraction §2.3, §2.7). - -inline std::vector encode_acf_abb(AcfMessageInfo info, - const std::vector& payload) { - info.acf_msg_type = kAcfMsgTypeAbb; - info.mtv = false; - std::vector buf(kAcfCommonHeaderLen + payload.size()); - encode_acf_message_info(info, buf.data()); - std::copy(payload.begin(), payload.end(), buf.begin() + static_cast(kAcfCommonHeaderLen)); - return buf; -} - -inline std::error_code decode_acf_abb(const uint8_t* b, size_t len, - AcfMessageInfo& out_info, - std::vector& out_payload) { - if (len < 1) return make_error_code(WireErrc::short_buffer); - if (b[0] != kAcfMsgTypeAbb) return make_error_code(WireErrc::bad_acf_msg_type); - if (len < kAcfCommonHeaderLen) return make_error_code(WireErrc::short_buffer); - decode_acf_message_info(b, out_info); - out_payload.assign(b + kAcfCommonHeaderLen, b + len); - return {}; -} - -// message_timestamp is the 64-bit ACF_GBB timestamp slot (extraction §2.7, -// §2.9). Its validity is carried by AcfMessageInfo::mtv, not by the value -// itself — an all-zero timestamp is a legitimate valid value. -inline std::vector encode_acf_gbb(AcfMessageInfo info, uint64_t message_timestamp, - const std::vector& payload) { - info.acf_msg_type = kAcfMsgTypeGbb; - std::vector buf(kAcfCommonHeaderLen + kAcfGbbTimestampLen + payload.size()); - encode_acf_message_info(info, buf.data()); - detail::put_u64(&buf[kAcfCommonHeaderLen], message_timestamp); - std::copy(payload.begin(), payload.end(), - buf.begin() + static_cast(kAcfCommonHeaderLen + kAcfGbbTimestampLen)); - return buf; -} - -inline std::error_code decode_acf_gbb(const uint8_t* b, size_t len, - AcfMessageInfo& out_info, - uint64_t& out_message_timestamp, - std::vector& out_payload) { - if (len < 1) return make_error_code(WireErrc::short_buffer); - if (b[0] != kAcfMsgTypeGbb) return make_error_code(WireErrc::bad_acf_msg_type); - if (len < kAcfCommonHeaderLen + kAcfGbbTimestampLen) - return make_error_code(WireErrc::short_buffer); - decode_acf_message_info(b, out_info); - out_message_timestamp = detail::get_u64(&b[kAcfCommonHeaderLen]); - out_payload.assign(b + kAcfCommonHeaderLen + kAcfGbbTimestampLen, b + len); - return {}; -} - -// ── Standard request kind & response semantics ──────────────────────────────── -// The mandatory baseline request kind: best-effort, unconditional dispatch -// via ACF_ABB (extraction §2.7). Conditional kinds (compound, triggered, -// timed, chained, …) are a later milestone (v2.5.0) layered on top of the -// `AcfMessageInfo` fields already defined above; this module only wires up -// the one kind every RC Server must support. - -enum class RequestKind : uint8_t { - Standard = 0, -}; - -inline AcfMessageInfo make_standard_request(ByteBusId bus_id, uint8_t transaction_num, - bool write, uint16_t read_size) noexcept { - AcfMessageInfo info; - info.acf_msg_type = kAcfMsgTypeAbb; - info.byte_bus_id = bus_id; - info.transaction_num = transaction_num; - info.op = write; - info.rsp = false; - info.err = false; - info.read_size_or_segment_num = write ? 0 : read_size; - return info; -} - -// The four response semantic types a request can produce, mapped onto the -// shared header's evt/op/rsp/err fields (extraction §2.7, §2.8). -enum class ResponseKind : uint8_t { - Acknowledge = 0, // received, no data — evt_ack set, rsp clear - WriteResponse = 1, // completed write — rsp set, op set, err clear - ReadResponse = 2, // completed read, payload carries the data — rsp set, op clear, err clear - ErrorResponse = 3, // failed — rsp set, err set -}; - -inline ResponseKind response_kind_of(const AcfMessageInfo& info) noexcept { - if (!info.rsp) return ResponseKind::Acknowledge; - if (info.err) return ResponseKind::ErrorResponse; - return info.op ? ResponseKind::WriteResponse : ResponseKind::ReadResponse; -} - -// make_response builds the header for a response/ack to `request`. It always -// copies byte_bus_id and transaction_num from the request unchanged — this -// is the mandatory echo-back rule for byte_bus_id (extraction §2.1) — -// callers cannot construct a response with a different byte_bus_id than the -// request it answers through this function. -inline AcfMessageInfo make_response(const AcfMessageInfo& request, ResponseKind kind) noexcept { - AcfMessageInfo resp; - resp.acf_msg_type = request.acf_msg_type; - resp.byte_bus_id = request.byte_bus_id; - resp.transaction_num = request.transaction_num; - resp.evt_ack = (kind == ResponseKind::Acknowledge); - resp.rsp = (kind != ResponseKind::Acknowledge); - resp.err = (kind == ResponseKind::ErrorResponse); - resp.op = (kind == ResponseKind::WriteResponse); - return resp; -} - -// ── Timestamp fallback rules ────────────────────────────────────────────────── -// avtp_timestamp (TSCF-only, 32-bit) and message_timestamp (ACF_GBB-only, -// 64-bit, valid only when `mtv` is set) are two independent, optional -// sources of timing information for the same logical message (extraction -// §2.6, §2.9). effective_timestamp prefers the TSCF timestamp when present -// and valid, falls back to a valid ACF_GBB message_timestamp, and returns -// std::nullopt — rather than silently defaulting to zero — when neither -// source is usable, so callers can distinguish "no timestamp" from -// "timestamp is zero". - -inline std::optional effective_timestamp(const TscfHeader* tscf, - const AcfMessageInfo* acf_info, - uint64_t message_timestamp) noexcept { - if (tscf != nullptr && tscf->timestamp_valid) - return uint64_t{tscf->avtp_timestamp}; - if (acf_info != nullptr && acf_info->acf_msg_type == kAcfMsgTypeGbb && acf_info->mtv) - return message_timestamp; - return std::nullopt; -} - -} // namespace wire -} // namespace rcp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fa5ee12..bbe4f48 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,9 +6,13 @@ add_executable(test_mock test_mock.cpp) target_link_libraries(test_mock PRIVATE rcp::rcp Catch2::Catch2WithMain) add_test(NAME rcp_mock COMMAND test_mock) -add_executable(test_wire test_wire.cpp) -target_link_libraries(test_wire PRIVATE rcp::rcp Catch2::Catch2WithMain) -add_test(NAME rcp_wire COMMAND test_wire) +add_executable(test_avtp test_avtp.cpp) +target_link_libraries(test_avtp PRIVATE rcp::rcp Catch2::Catch2WithMain) +add_test(NAME rcp_avtp COMMAND test_avtp) + +add_executable(test_acf test_acf.cpp) +target_link_libraries(test_acf PRIVATE rcp::rcp Catch2::Catch2WithMain) +add_test(NAME rcp_acf COMMAND test_acf) add_executable(test_legacy_wire test_legacy_wire.cpp) target_link_libraries(test_legacy_wire PRIVATE rcp::rcp Catch2::Catch2WithMain) @@ -54,9 +58,9 @@ add_executable(test_pwm test_pwm.cpp) target_link_libraries(test_pwm PRIVATE rcp::rcp Catch2::Catch2WithMain) add_test(NAME rcp_pwm COMMAND test_pwm) -add_executable(test_sequencer test_sequencer.cpp) -target_link_libraries(test_sequencer PRIVATE rcp::rcp Catch2::Catch2WithMain) -add_test(NAME rcp_sequencer COMMAND test_sequencer) +add_executable(test_request test_request.cpp) +target_link_libraries(test_request PRIVATE rcp::rcp Catch2::Catch2WithMain) +add_test(NAME rcp_request COMMAND test_request) add_executable(test_e2e test_e2e.cpp) target_link_libraries(test_e2e PRIVATE rcp::rcp Catch2::Catch2WithMain) diff --git a/tests/test_wire.cpp b/tests/test_acf.cpp similarity index 61% rename from tests/test_wire.cpp rename to tests/test_acf.cpp index 7a782ca..3a103f3 100644 --- a/tests/test_wire.cpp +++ b/tests/test_acf.cpp @@ -1,126 +1,28 @@ -// fusa:test REQ-WIRE-001 -// fusa:test REQ-WIRE-002 -// fusa:test REQ-WIRE-003 // fusa:test REQ-WIRE-004 // fusa:test REQ-WIRE-005 // fusa:test REQ-WIRE-006 -// fusa:test REQ-WIRE-007 // fusa:test REQ-WIRE-008 // fusa:test REQ-WIRE-009 // fusa:test REQ-WIRE-010 -// fusa:test REQ-WIRE-011 // fusa:test REQ-WIRE-012 // fusa:test REQ-WIRE-013 // fusa:test REQ-WIRE-014 -// Tests for rcp/wire.hpp — the TC18 AVTPDU/ACF wire codec (ROADMAP.md -// milestone 44, "Wire Format Core", v2.0.0). +// Tests for rcp/acf.hpp — the ACF_ABB/ACF_GBB message-format half of the TC18 +// wire codec (ROADMAP.md milestone 44, "Wire Format Core", v2.0.0; split +// from a single rcp/wire.hpp into rcp/avtp.hpp + rcp/acf.hpp per RELAY spec +// §13.7.2's standard module-name registry). AVTPDU header-framing tests +// live in tests/test_avtp.cpp. #include -#include +#include -using namespace rcp::wire; - -namespace { - -StreamId make_stream_id(uint8_t mac_seed, uint16_t suffix) { - StreamId id; - for (auto& b : id.mac) b = mac_seed++; - id.suffix = suffix; - return id; -} - -} // namespace - -// ── StreamId ─────────────────────────────────────────────────────────────────── - -TEST_CASE("StreamId round-trips through to_u64/from_u64", "[wire][REQ-WIRE-007]") { - auto id = make_stream_id(0x02, 0xBEEF); - auto back = StreamId::from_u64(id.to_u64()); - REQUIRE(back == id); - REQUIRE(back.suffix == 0xBEEF); -} - -TEST_CASE("Distinct MAC halves produce distinct StreamId values", "[wire][REQ-WIRE-007]") { - auto a = make_stream_id(0x02, 0x0001); - auto b = make_stream_id(0x03, 0x0001); - REQUIRE(a != b); -} - -// ── NTSCF header ─────────────────────────────────────────────────────────────── - -TEST_CASE("NTSCF header round-trips stream_id, sequence_num, control_data_length", "[wire][REQ-WIRE-001]") { - NtscfHeader hdr; - hdr.stream_id = make_stream_id(0xAA, 7); - hdr.sequence_num = 4242; - hdr.control_data_length = 128; - - auto buf = encode_ntscf_header(hdr); - REQUIRE(buf.size() == kNtscfHeaderLen); - REQUIRE(buf[0] == kSubtypeNtscf); - - NtscfHeader out; - REQUIRE_FALSE(decode_ntscf_header(buf.data(), buf.size(), out)); - REQUIRE(out.stream_id == hdr.stream_id); - REQUIRE(out.sequence_num == hdr.sequence_num); - REQUIRE(out.control_data_length == hdr.control_data_length); -} - -// ── TSCF header ──────────────────────────────────────────────────────────────── - -TEST_CASE("TSCF header round-trips avtp_timestamp when timestamp_valid is set", "[wire][REQ-WIRE-002][REQ-WIRE-011]") { - TscfHeader hdr; - hdr.stream_id = make_stream_id(0x10, 99); - hdr.sequence_num = 5; - hdr.control_data_length = 64; - hdr.timestamp_valid = true; - hdr.avtp_timestamp = 0xCAFEBABE; - - auto buf = encode_tscf_header(hdr); - REQUIRE(buf.size() == kTscfHeaderLen); - REQUIRE(buf[0] == kSubtypeTscf); - - TscfHeader out; - REQUIRE_FALSE(decode_tscf_header(buf.data(), buf.size(), out)); - REQUIRE(out.timestamp_valid); - REQUIRE(out.avtp_timestamp == hdr.avtp_timestamp); - REQUIRE(out.stream_id == hdr.stream_id); -} - -TEST_CASE("TSCF header marks timestamp invalid distinctly from a zero timestamp", "[wire][REQ-WIRE-011]") { - TscfHeader hdr; - hdr.timestamp_valid = false; - hdr.avtp_timestamp = 0; - - auto buf = encode_tscf_header(hdr); - TscfHeader out; - REQUIRE_FALSE(decode_tscf_header(buf.data(), buf.size(), out)); - REQUIRE_FALSE(out.timestamp_valid); -} - -// ── AVTPDU subtype validation ────────────────────────────────────────────────── - -TEST_CASE("decode_ntscf_header rejects a TSCF-subtype buffer", "[wire][REQ-WIRE-003]") { - TscfHeader tscf; - auto buf = encode_tscf_header(tscf); - NtscfHeader out; - auto ec = decode_ntscf_header(buf.data(), buf.size(), out); - REQUIRE(ec); - REQUIRE(ec == make_error_code(WireErrc::bad_subtype)); -} - -TEST_CASE("decode_tscf_header rejects an NTSCF-subtype buffer", "[wire][REQ-WIRE-003]") { - NtscfHeader ntscf; - auto buf = encode_ntscf_header(ntscf); - TscfHeader out; - auto ec = decode_tscf_header(buf.data(), buf.size(), out); - REQUIRE(ec); - REQUIRE(ec == make_error_code(WireErrc::bad_subtype)); -} +using namespace rcp::acf; +using rcp::avtp::TscfHeader; // ── ACF_ABB ───────────────────────────────────────────────────────────────────── -TEST_CASE("ACF_ABB round-trips the shared header and payload with no timestamp field", "[wire][REQ-WIRE-004][REQ-WIRE-006]") { +TEST_CASE("ACF_ABB round-trips the shared header and payload with no timestamp field", "[acf][REQ-WIRE-004][REQ-WIRE-006]") { AcfMessageInfo info; info.byte_bus_id = 7; info.transaction_num = 42; @@ -162,7 +64,7 @@ TEST_CASE("ACF_ABB round-trips the shared header and payload with no timestamp f // ── ACF_GBB ───────────────────────────────────────────────────────────────────── -TEST_CASE("ACF_GBB round-trips a 64-bit message_timestamp alongside the shared header", "[wire][REQ-WIRE-005][REQ-WIRE-006]") { +TEST_CASE("ACF_GBB round-trips a 64-bit message_timestamp alongside the shared header", "[acf][REQ-WIRE-005][REQ-WIRE-006]") { AcfMessageInfo info; info.byte_bus_id = 3; info.transaction_num = 9; @@ -185,29 +87,25 @@ TEST_CASE("ACF_GBB round-trips a 64-bit message_timestamp alongside the shared h REQUIRE(out_payload == payload); } -TEST_CASE("decode_acf_abb rejects an ACF_GBB-typed buffer and vice versa", "[wire][REQ-WIRE-014]") { +TEST_CASE("decode_acf_abb rejects an ACF_GBB-typed buffer and vice versa", "[acf][REQ-WIRE-014]") { AcfMessageInfo info; auto gbb_frame = encode_acf_gbb(info, 0, {}); AcfMessageInfo out; std::vector out_payload; auto ec = decode_acf_abb(gbb_frame.data(), gbb_frame.size(), out, out_payload); REQUIRE(ec); - REQUIRE(ec == make_error_code(WireErrc::bad_acf_msg_type)); + REQUIRE(ec == make_error_code(AcfErrc::bad_acf_msg_type)); auto abb_frame = encode_acf_abb(info, {}); uint64_t ts = 0; auto ec2 = decode_acf_gbb(abb_frame.data(), abb_frame.size(), out, ts, out_payload); REQUIRE(ec2); - REQUIRE(ec2 == make_error_code(WireErrc::bad_acf_msg_type)); + REQUIRE(ec2 == make_error_code(AcfErrc::bad_acf_msg_type)); } // ── Short-buffer validation ───────────────────────────────────────────────────── -TEST_CASE("Decoders reject buffers shorter than their fixed header", "[wire][REQ-WIRE-013]") { - NtscfHeader ntscf_out; - std::vector too_short(kNtscfHeaderLen - 1, 0); - REQUIRE(decode_ntscf_header(too_short.data(), too_short.size(), ntscf_out)); - +TEST_CASE("Decoders reject buffers shorter than their fixed header", "[acf][REQ-WIRE-013]") { AcfMessageInfo abb_out; std::vector abb_payload; std::vector too_short_acf(kAcfCommonHeaderLen - 1, 0); @@ -216,7 +114,7 @@ TEST_CASE("Decoders reject buffers shorter than their fixed header", "[wire][REQ // ── byte_bus_id echo rule ─────────────────────────────────────────────────────── -TEST_CASE("make_response echoes byte_bus_id and transaction_num from the request unchanged", "[wire][REQ-WIRE-008]") { +TEST_CASE("make_response echoes byte_bus_id and transaction_num from the request unchanged", "[acf][REQ-WIRE-008]") { auto req = make_standard_request(/*bus_id=*/9, /*transaction_num=*/17, /*write=*/false, /*read_size=*/4); auto resp = make_response(req, ResponseKind::ReadResponse); REQUIRE(resp.byte_bus_id == req.byte_bus_id); @@ -225,7 +123,7 @@ TEST_CASE("make_response echoes byte_bus_id and transaction_num from the request // ── Standard request kind ─────────────────────────────────────────────────────── -TEST_CASE("make_standard_request builds an unconditional ACF_ABB read request", "[wire][REQ-WIRE-009]") { +TEST_CASE("make_standard_request builds an unconditional ACF_ABB read request", "[acf][REQ-WIRE-009]") { auto req = make_standard_request(1, 1, /*write=*/false, /*read_size=*/8); REQUIRE(req.acf_msg_type == kAcfMsgTypeAbb); REQUIRE_FALSE(req.op); @@ -233,7 +131,7 @@ TEST_CASE("make_standard_request builds an unconditional ACF_ABB read request", REQUIRE(req.read_size_or_segment_num == 8); } -TEST_CASE("make_standard_request builds an unconditional ACF_ABB write request", "[wire][REQ-WIRE-009]") { +TEST_CASE("make_standard_request builds an unconditional ACF_ABB write request", "[acf][REQ-WIRE-009]") { auto req = make_standard_request(1, 1, /*write=*/true, /*read_size=*/0); REQUIRE(req.acf_msg_type == kAcfMsgTypeAbb); REQUIRE(req.op); @@ -242,7 +140,7 @@ TEST_CASE("make_standard_request builds an unconditional ACF_ABB write request", // ── Four response semantic types ──────────────────────────────────────────────── -TEST_CASE("The four response kinds map onto distinct, recoverable header states", "[wire][REQ-WIRE-010]") { +TEST_CASE("The four response kinds map onto distinct, recoverable header states", "[acf][REQ-WIRE-010]") { auto req = make_standard_request(2, 5, false, 4); auto ack = make_response(req, ResponseKind::Acknowledge); @@ -266,7 +164,7 @@ TEST_CASE("The four response kinds map onto distinct, recoverable header states" // ── Timestamp fallback rules ──────────────────────────────────────────────────── -TEST_CASE("effective_timestamp prefers a valid TSCF avtp_timestamp", "[wire][REQ-WIRE-012]") { +TEST_CASE("effective_timestamp prefers a valid TSCF avtp_timestamp", "[acf][REQ-WIRE-012]") { TscfHeader tscf; tscf.timestamp_valid = true; tscf.avtp_timestamp = 111; @@ -280,7 +178,7 @@ TEST_CASE("effective_timestamp prefers a valid TSCF avtp_timestamp", "[wire][REQ REQUIRE(*ts == 111); } -TEST_CASE("effective_timestamp falls back to a valid ACF_GBB message_timestamp", "[wire][REQ-WIRE-012]") { +TEST_CASE("effective_timestamp falls back to a valid ACF_GBB message_timestamp", "[acf][REQ-WIRE-012]") { AcfMessageInfo gbb_info; gbb_info.acf_msg_type = kAcfMsgTypeGbb; gbb_info.mtv = true; @@ -290,7 +188,7 @@ TEST_CASE("effective_timestamp falls back to a valid ACF_GBB message_timestamp", REQUIRE(*ts == 333); } -TEST_CASE("effective_timestamp is nullopt, not zero, when neither source is valid", "[wire][REQ-WIRE-012]") { +TEST_CASE("effective_timestamp is nullopt, not zero, when neither source is valid", "[acf][REQ-WIRE-012]") { TscfHeader tscf; // timestamp_valid defaults to false AcfMessageInfo abb_info; abb_info.acf_msg_type = kAcfMsgTypeAbb; // ACF_ABB never has a timestamp diff --git a/tests/test_avtp.cpp b/tests/test_avtp.cpp new file mode 100644 index 0000000..0a95e3a --- /dev/null +++ b/tests/test_avtp.cpp @@ -0,0 +1,128 @@ +// fusa:test REQ-WIRE-001 +// fusa:test REQ-WIRE-002 +// fusa:test REQ-WIRE-003 +// fusa:test REQ-WIRE-007 +// fusa:test REQ-WIRE-011 +// fusa:test REQ-WIRE-013 + +// Tests for rcp/avtp.hpp — the TC18 AVTPDU header-framing half of the wire +// codec (ROADMAP.md milestone 44, "Wire Format Core", v2.0.0; split from a +// single rcp/wire.hpp into rcp/avtp.hpp + rcp/acf.hpp per RELAY spec +// §13.7.2's standard module-name registry). ACF_ABB/ACF_GBB message-format +// tests live in tests/test_acf.cpp. + +#include +#include + +using namespace rcp::avtp; + +namespace { + +StreamId make_stream_id(uint8_t mac_seed, uint16_t suffix) { + StreamId id; + for (auto& b : id.mac) b = mac_seed++; + id.suffix = suffix; + return id; +} + +} // namespace + +// ── StreamId ─────────────────────────────────────────────────────────────────── + +TEST_CASE("StreamId round-trips through to_u64/from_u64", "[avtp][REQ-WIRE-007]") { + auto id = make_stream_id(0x02, 0xBEEF); + auto back = StreamId::from_u64(id.to_u64()); + REQUIRE(back == id); + REQUIRE(back.suffix == 0xBEEF); +} + +TEST_CASE("Distinct MAC halves produce distinct StreamId values", "[avtp][REQ-WIRE-007]") { + auto a = make_stream_id(0x02, 0x0001); + auto b = make_stream_id(0x03, 0x0001); + REQUIRE(a != b); +} + +// ── NTSCF header ─────────────────────────────────────────────────────────────── + +TEST_CASE("NTSCF header round-trips stream_id, sequence_num, control_data_length", "[avtp][REQ-WIRE-001]") { + NtscfHeader hdr; + hdr.stream_id = make_stream_id(0xAA, 7); + hdr.sequence_num = 4242; + hdr.control_data_length = 128; + + auto buf = encode_ntscf_header(hdr); + REQUIRE(buf.size() == kNtscfHeaderLen); + REQUIRE(buf[0] == kSubtypeNtscf); + + NtscfHeader out; + REQUIRE_FALSE(decode_ntscf_header(buf.data(), buf.size(), out)); + REQUIRE(out.stream_id == hdr.stream_id); + REQUIRE(out.sequence_num == hdr.sequence_num); + REQUIRE(out.control_data_length == hdr.control_data_length); +} + +// ── TSCF header ──────────────────────────────────────────────────────────────── + +TEST_CASE("TSCF header round-trips avtp_timestamp when timestamp_valid is set", "[avtp][REQ-WIRE-002][REQ-WIRE-011]") { + TscfHeader hdr; + hdr.stream_id = make_stream_id(0x10, 99); + hdr.sequence_num = 5; + hdr.control_data_length = 64; + hdr.timestamp_valid = true; + hdr.avtp_timestamp = 0xCAFEBABE; + + auto buf = encode_tscf_header(hdr); + REQUIRE(buf.size() == kTscfHeaderLen); + REQUIRE(buf[0] == kSubtypeTscf); + + TscfHeader out; + REQUIRE_FALSE(decode_tscf_header(buf.data(), buf.size(), out)); + REQUIRE(out.timestamp_valid); + REQUIRE(out.avtp_timestamp == hdr.avtp_timestamp); + REQUIRE(out.stream_id == hdr.stream_id); +} + +TEST_CASE("TSCF header marks timestamp invalid distinctly from a zero timestamp", "[avtp][REQ-WIRE-011]") { + TscfHeader hdr; + hdr.timestamp_valid = false; + hdr.avtp_timestamp = 0; + + auto buf = encode_tscf_header(hdr); + TscfHeader out; + REQUIRE_FALSE(decode_tscf_header(buf.data(), buf.size(), out)); + REQUIRE_FALSE(out.timestamp_valid); +} + +// ── AVTPDU subtype validation ────────────────────────────────────────────────── + +TEST_CASE("decode_ntscf_header rejects a TSCF-subtype buffer", "[avtp][REQ-WIRE-003]") { + TscfHeader tscf; + auto buf = encode_tscf_header(tscf); + NtscfHeader out; + auto ec = decode_ntscf_header(buf.data(), buf.size(), out); + REQUIRE(ec); + REQUIRE(ec == make_error_code(AvtpErrc::bad_subtype)); +} + +TEST_CASE("decode_tscf_header rejects an NTSCF-subtype buffer", "[avtp][REQ-WIRE-003]") { + NtscfHeader ntscf; + auto buf = encode_ntscf_header(ntscf); + TscfHeader out; + auto ec = decode_tscf_header(buf.data(), buf.size(), out); + REQUIRE(ec); + REQUIRE(ec == make_error_code(AvtpErrc::bad_subtype)); +} + +// ── Short-buffer validation ───────────────────────────────────────────────────── + +TEST_CASE("decode_ntscf_header rejects a buffer shorter than the fixed header", "[avtp][REQ-WIRE-013]") { + NtscfHeader ntscf_out; + std::vector too_short(kNtscfHeaderLen - 1, 0); + REQUIRE(decode_ntscf_header(too_short.data(), too_short.size(), ntscf_out)); +} + +TEST_CASE("decode_tscf_header rejects a buffer shorter than the fixed header", "[avtp][REQ-WIRE-013]") { + TscfHeader tscf_out; + std::vector too_short(kTscfHeaderLen - 1, 0); + REQUIRE(decode_tscf_header(too_short.data(), too_short.size(), tscf_out)); +} diff --git a/tests/test_discovery.cpp b/tests/test_discovery.cpp index 9792104..c5888d6 100644 --- a/tests/test_discovery.cpp +++ b/tests/test_discovery.cpp @@ -49,24 +49,24 @@ TEST_CASE("kDiscoveryRegisterAddress fixes the discovery read at register-map ad // ── NTSCF-only framing; TSCF-headed discovery is dropped ──────────────────────── TEST_CASE("encode_discovery_request always produces an NTSCF-headed frame", "[discovery][REQ-DISC-002]") { - rcp::wire::StreamId sid; + rcp::avtp::StreamId sid; sid.mac = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; sid.suffix = 1; auto frame = encode_discovery_request(sid, /*sequence_num=*/0, /*transaction_num=*/1); REQUIRE_FALSE(frame.empty()); - REQUIRE(frame[0] == rcp::wire::kSubtypeNtscf); + REQUIRE(frame[0] == rcp::avtp::kSubtypeNtscf); } TEST_CASE("decode_discovery_request round-trips an encoded discovery request", "[discovery][REQ-DISC-002]") { - rcp::wire::StreamId sid; + rcp::avtp::StreamId sid; sid.mac = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; sid.suffix = 42; auto frame = encode_discovery_request(sid, /*sequence_num=*/3, /*transaction_num=*/9, /*read_size=*/4); - rcp::wire::NtscfHeader hdr; - rcp::wire::AcfMessageInfo info; + rcp::avtp::NtscfHeader hdr; + rcp::acf::AcfMessageInfo info; std::vector payload; auto ec = decode_discovery_request(frame.data(), frame.size(), hdr, info, payload); @@ -81,19 +81,19 @@ TEST_CASE("decode_discovery_request round-trips an encoded discovery request", " TEST_CASE("A TSCF-headed discovery request is dropped, not decoded", "[discovery][REQ-DISC-002]") { // Build a TSCF frame around the same discovery-shaped ACF_ABB payload — // this must never be treated as a valid discovery request. - rcp::wire::TscfHeader hdr; + rcp::avtp::TscfHeader hdr; hdr.stream_id.mac = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; hdr.stream_id.suffix = 1; auto info = make_discovery_request(/*transaction_num=*/1); - auto acf = rcp::wire::encode_acf_abb(info, {}); + auto acf = rcp::acf::encode_acf_abb(info, {}); hdr.control_data_length = static_cast(acf.size()); - auto frame = rcp::wire::encode_tscf_header(hdr); + auto frame = rcp::avtp::encode_tscf_header(hdr); frame.insert(frame.end(), acf.begin(), acf.end()); - rcp::wire::NtscfHeader out_hdr; - rcp::wire::AcfMessageInfo out_info; + rcp::avtp::NtscfHeader out_hdr; + rcp::acf::AcfMessageInfo out_info; std::vector out_payload; auto ec = decode_discovery_request(frame.data(), frame.size(), out_hdr, out_info, out_payload); diff --git a/tests/test_e2e.cpp b/tests/test_e2e.cpp index 2606daa..4aba577 100644 --- a/tests/test_e2e.cpp +++ b/tests/test_e2e.cpp @@ -24,13 +24,13 @@ using namespace rcp::e2e; using rcp::regmap::EndpointGenericConfig; using rcp::regmap::RequestStreamConfig; using rcp::regmap::RxSafetyMeasure; -using rcp::sequencer::RequestLedger; -using rcp::sequencer::RequestRecord; -using rcp::sequencer::RequestTypeOpcode; -using rcp::sequencer::SequencerTable; -using rcp::sequencer::request_record_for; -using rcp::wire::AcfMessageInfo; -using rcp::wire::StreamId; +using rcp::request::RequestLedger; +using rcp::request::RequestRecord; +using rcp::request::RequestTypeOpcode; +using rcp::request::SequencerTable; +using rcp::request::request_record_for; +using rcp::acf::AcfMessageInfo; +using rcp::avtp::StreamId; namespace { @@ -90,7 +90,7 @@ TEST_CASE("coverage_buffer layout is stream_id + avtp_timestamp + ACF header + p std::vector payload{1, 2, 3}; auto buf = coverage_buffer(sid, uint32_t{0xDEADBEEF}, info, payload); - REQUIRE(buf.size() == 8 + 4 + rcp::wire::kAcfCommonHeaderLen + payload.size()); + REQUIRE(buf.size() == 8 + 4 + rcp::acf::kAcfCommonHeaderLen + payload.size()); // stream_id occupies the first 8 bytes, big-endian. REQUIRE(buf[0] == sid.mac[0]); @@ -132,12 +132,12 @@ TEST_CASE("apply_acf_length_adjustment adds exactly one quadlet", "[e2e][REQ-E2E TEST_CASE("apply_frame_length_adjustment adds exactly four octets to NTSCF and TSCF headers", "[e2e][REQ-E2E-003]") { - rcp::wire::NtscfHeader ntscf; + rcp::avtp::NtscfHeader ntscf; ntscf.control_data_length = 20; apply_frame_length_adjustment(ntscf); REQUIRE(ntscf.control_data_length == 20 + kCrcLengthAdjustOctets); - rcp::wire::TscfHeader tscf; + rcp::avtp::TscfHeader tscf; tscf.control_data_length = 30; apply_frame_length_adjustment(tscf); REQUIRE(tscf.control_data_length == 30 + kCrcLengthAdjustOctets); @@ -286,8 +286,8 @@ TEST_CASE("apply_watchdog_overflow purges normal requests but retains safety-tag REQUIRE(purged == 1); REQUIRE(wd.in_safe_state()); - REQUIRE(ledger.find(1)->state == rcp::sequencer::RequestState::Canceled); - REQUIRE(ledger.find(2)->state == rcp::sequencer::RequestState::Pending); + REQUIRE(ledger.find(1)->state == rcp::request::RequestState::Canceled); + REQUIRE(ledger.find(2)->state == rcp::request::RequestState::Pending); } TEST_CASE("apply_watchdog_overflow purges nothing and does not enter safe state when disabled", @@ -299,7 +299,7 @@ TEST_CASE("apply_watchdog_overflow purges nothing and does not enter safe state RxWatchdog wd; REQUIRE(apply_watchdog_overflow(cfg, wd, ledger) == 0); REQUIRE_FALSE(wd.in_safe_state()); - REQUIRE(ledger.find(1)->state == rcp::sequencer::RequestState::Pending); + REQUIRE(ledger.find(1)->state == rcp::request::RequestState::Pending); } TEST_CASE("apply_queue_overflow implements the same purge-normal/retain-safety rule via a " @@ -315,8 +315,8 @@ TEST_CASE("apply_queue_overflow implements the same purge-normal/retain-safety r RxWatchdog wd; REQUIRE(apply_queue_overflow(cfg, wd, ledger) == 1); REQUIRE(wd.in_safe_state()); - REQUIRE(ledger.find(1)->state == rcp::sequencer::RequestState::Canceled); - REQUIRE(ledger.find(2)->state == rcp::sequencer::RequestState::Pending); + REQUIRE(ledger.find(1)->state == rcp::request::RequestState::Canceled); + REQUIRE(ledger.find(2)->state == rcp::request::RequestState::Pending); } // ── Safe-state gating ───────────────────────────────────────────────────────── diff --git a/tests/test_gpio.cpp b/tests/test_gpio.cpp index 15d90f0..93587d2 100644 --- a/tests/test_gpio.cpp +++ b/tests/test_gpio.cpp @@ -37,7 +37,7 @@ TEST_CASE("decode_gpio_payload rejects a short buffer", "[gpio][REQ-GPIO-001]") std::vector short_buf{0x01, 0x02}; PinMask out = 0; auto ec = decode_gpio_payload(short_buf.data(), short_buf.size(), out); - REQUIRE(ec == rcp::wire::make_error_code(rcp::wire::WireErrc::short_buffer)); + REQUIRE(ec == rcp::avtp::make_error_code(rcp::avtp::AvtpErrc::short_buffer)); } // ── Write semantics: the 6 generic combinators ─────────────────────────────── @@ -164,7 +164,7 @@ TEST_CASE("decode_gpio_functional_config rejects an undersized blob", "[gpio][RE PinMask out_directions = 0; std::array out_edges{}; auto ec = decode_gpio_functional_config(cfg, out_directions, out_edges); - REQUIRE(ec == rcp::wire::make_error_code(rcp::wire::WireErrc::short_buffer)); + REQUIRE(ec == rcp::avtp::make_error_code(rcp::avtp::AvtpErrc::short_buffer)); } // ── GpioEndpoint request-dispatch pattern ──────────────────────────────────── diff --git a/tests/test_legacy_wire.cpp b/tests/test_legacy_wire.cpp index 84be8ca..67530af 100644 --- a/tests/test_legacy_wire.cpp +++ b/tests/test_legacy_wire.cpp @@ -13,7 +13,8 @@ // Tests for rcp/legacy_wire.hpp — the pre-replacement 16-byte frame codec // still used internally by rcp/udp.hpp. See that header's comment for why -// this still exists alongside the new rcp/wire.hpp AVTPDU/ACF codec. +// this still exists alongside the new rcp/avtp.hpp + rcp/acf.hpp AVTPDU/ACF +// codec. #include #include diff --git a/tests/test_sequencer.cpp b/tests/test_request.cpp similarity index 82% rename from tests/test_sequencer.cpp rename to tests/test_request.cpp index 6e43071..d64c192 100644 --- a/tests/test_sequencer.cpp +++ b/tests/test_request.cpp @@ -11,18 +11,20 @@ // fusa:test REQ-SEQ-011 // fusa:test REQ-SEQ-012 -// Tests for rcp/sequencer.hpp — conditional-request taxonomy and +// Tests for rcp/request.hpp — conditional-request taxonomy and // sequencer-state primitives (ROADMAP.md milestone 49, -// "Conditional-Request Taxonomy & Sequencers", v2.5.0). +// "Conditional-Request Taxonomy & Sequencers", v2.5.0; file renamed from +// rcp/sequencer.hpp per RELAY spec §13.7.2's `request` standard-module-name +// entry). #include -#include +#include -using namespace rcp::sequencer; +using namespace rcp::request; // ── message_timestamp-repurposing trick (mtv=0) ────────────────────────────── -TEST_CASE("encode_request_type/decode_request_type round-trip", "[sequencer][REQ-SEQ-001]") { +TEST_CASE("encode_request_type/decode_request_type round-trip", "[request][REQ-SEQ-001]") { std::array params{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77}; const uint64_t ts = encode_request_type(RequestTypeOpcode::Compound, params); @@ -34,42 +36,42 @@ TEST_CASE("encode_request_type/decode_request_type round-trip", "[sequencer][REQ REQUIRE(out_params == params); } -TEST_CASE("decode_request_type rejects mtv=1 as not a repurposed slot", "[sequencer][REQ-SEQ-001]") { +TEST_CASE("decode_request_type rejects mtv=1 as not a repurposed slot", "[request][REQ-SEQ-001]") { RequestTypeOpcode type{}; std::array out_params{}; auto ec = decode_request_type(/*mtv=*/true, 0xFFFFFFFFFFFFFFFFull, type, out_params); - REQUIRE(ec == make_error_code(SequencerErrc::timestamp_not_repurposed)); + REQUIRE(ec == make_error_code(RequestErrc::timestamp_not_repurposed)); } -TEST_CASE("decode_request_type rejects an unrecognized opcode byte", "[sequencer][REQ-SEQ-001]") { +TEST_CASE("decode_request_type rejects an unrecognized opcode byte", "[request][REQ-SEQ-001]") { RequestTypeOpcode type{}; std::array out_params{}; // 0x02 is not one of the 8 defined opcodes. const uint64_t ts = uint64_t{0x02} << 56; auto ec = decode_request_type(/*mtv=*/false, ts, type, out_params); - REQUIRE(ec == make_error_code(SequencerErrc::unknown_request_type)); + REQUIRE(ec == make_error_code(RequestErrc::unknown_request_type)); } -TEST_CASE("make_conditional_request always sets ACF_GBB with mtv clear", "[sequencer][REQ-SEQ-001]") { +TEST_CASE("make_conditional_request always sets ACF_GBB with mtv clear", "[request][REQ-SEQ-001]") { auto info = make_conditional_request(/*bus_id=*/7, /*transaction_num=*/9, /*cs=*/true); - REQUIRE(info.acf_msg_type == rcp::wire::kAcfMsgTypeGbb); + REQUIRE(info.acf_msg_type == rcp::acf::kAcfMsgTypeGbb); REQUIRE_FALSE(info.mtv); REQUIRE(info.byte_bus_id == 7); REQUIRE(info.transaction_num == 9); REQUIRE(info.cs); } -TEST_CASE("make_conditional_request round-trips through wire::encode_acf_gbb/decode_acf_gbb", - "[sequencer][REQ-SEQ-001]") { +TEST_CASE("make_conditional_request round-trips through acf::encode_acf_gbb/decode_acf_gbb", + "[request][REQ-SEQ-001]") { std::array params{1, 2, 3, 4, 5, 6, 7}; auto info = make_conditional_request(3, 42, false); auto ts = encode_request_type(RequestTypeOpcode::Triggered, params); - auto buf = rcp::wire::encode_acf_gbb(info, ts, {}); + auto buf = rcp::acf::encode_acf_gbb(info, ts, {}); - rcp::wire::AcfMessageInfo decoded_info; + rcp::acf::AcfMessageInfo decoded_info; uint64_t decoded_ts = 0; std::vector decoded_payload; - auto ec = rcp::wire::decode_acf_gbb(buf.data(), buf.size(), decoded_info, decoded_ts, decoded_payload); + auto ec = rcp::acf::decode_acf_gbb(buf.data(), buf.size(), decoded_info, decoded_ts, decoded_payload); REQUIRE_FALSE(ec); REQUIRE_FALSE(decoded_info.mtv); @@ -82,7 +84,7 @@ TEST_CASE("make_conditional_request round-trips through wire::encode_acf_gbb/dec // ── Request categories & execution-priority ordering ───────────────────────── -TEST_CASE("category_of maps every opcode to its documented category", "[sequencer][REQ-SEQ-002]") { +TEST_CASE("category_of maps every opcode to its documented category", "[request][REQ-SEQ-002]") { REQUIRE(category_of(std::nullopt) == RequestCategory::Standard); REQUIRE(category_of(RequestTypeOpcode::ClearAll) == RequestCategory::Cancellation); REQUIRE(category_of(RequestTypeOpcode::ClearNonSafestate) == RequestCategory::Cancellation); @@ -98,7 +100,7 @@ TEST_CASE("category_of maps every opcode to its documented category", "[sequence REQUIRE(category_of(RequestTypeOpcode::TriggeredSafety) == RequestCategory::Triggered); } -TEST_CASE("priority_rank orders categories cancellation..standard", "[sequencer][REQ-SEQ-002]") { +TEST_CASE("priority_rank orders categories cancellation..standard", "[request][REQ-SEQ-002]") { REQUIRE(priority_rank(RequestCategory::Cancellation) < priority_rank(RequestCategory::Triggered)); REQUIRE(priority_rank(RequestCategory::Triggered) < priority_rank(RequestCategory::Timed)); REQUIRE(priority_rank(RequestCategory::Timed) < priority_rank(RequestCategory::Compound)); @@ -108,7 +110,7 @@ TEST_CASE("priority_rank orders categories cancellation..standard", "[sequencer] } TEST_CASE("select_next_due picks the highest-priority category regardless of arrival order", - "[sequencer][REQ-SEQ-002]") { + "[request][REQ-SEQ-002]") { std::vector due{ {RequestCategory::Standard, 0}, {RequestCategory::Chained, 1}, @@ -119,7 +121,7 @@ TEST_CASE("select_next_due picks the highest-priority category regardless of arr REQUIRE(*winner == 2); } -TEST_CASE("select_next_due breaks ties within a category by FIFO arrival order", "[sequencer][REQ-SEQ-002]") { +TEST_CASE("select_next_due breaks ties within a category by FIFO arrival order", "[request][REQ-SEQ-002]") { std::vector due{ {RequestCategory::Timed, 5}, {RequestCategory::Timed, 2}, @@ -130,20 +132,20 @@ TEST_CASE("select_next_due breaks ties within a category by FIFO arrival order", REQUIRE(*winner == 1); // arrival_seq 2 is earliest } -TEST_CASE("select_next_due returns nullopt for an empty candidate set", "[sequencer][REQ-SEQ-002]") { +TEST_CASE("select_next_due returns nullopt for an empty candidate set", "[request][REQ-SEQ-002]") { REQUIRE_FALSE(select_next_due({}).has_value()); } // ── Optional-feature bundling ───────────────────────────────────────────────── TEST_CASE("validate_feature_bundles accepts a feature set with nothing conditional enabled", - "[sequencer][REQ-SEQ-003]") { + "[request][REQ-SEQ-003]") { FeatureSet f; REQUIRE_FALSE(validate_feature_bundles(f)); } TEST_CASE("validate_feature_bundles accepts triggered/chained/timed independently of compound", - "[sequencer][REQ-SEQ-003]") { + "[request][REQ-SEQ-003]") { FeatureSet f; f.triggered = true; f.chained = true; @@ -151,38 +153,38 @@ TEST_CASE("validate_feature_bundles accepts triggered/chained/timed independentl REQUIRE_FALSE(validate_feature_bundles(f)); } -TEST_CASE("validate_feature_bundles rejects compound claimed alone", "[sequencer][REQ-SEQ-003]") { +TEST_CASE("validate_feature_bundles rejects compound claimed alone", "[request][REQ-SEQ-003]") { FeatureSet f; f.compound = true; // missing compound_wait, clear_non_safestate, and sequencer_count - REQUIRE(validate_feature_bundles(f) == make_error_code(SequencerErrc::compound_bundle_incomplete)); + REQUIRE(validate_feature_bundles(f) == make_error_code(RequestErrc::compound_bundle_incomplete)); } -TEST_CASE("validate_feature_bundles rejects compound_wait claimed alone", "[sequencer][REQ-SEQ-003]") { +TEST_CASE("validate_feature_bundles rejects compound_wait claimed alone", "[request][REQ-SEQ-003]") { FeatureSet f; f.compound_wait = true; - REQUIRE(validate_feature_bundles(f) == make_error_code(SequencerErrc::compound_bundle_incomplete)); + REQUIRE(validate_feature_bundles(f) == make_error_code(RequestErrc::compound_bundle_incomplete)); } TEST_CASE("validate_feature_bundles rejects compound+compound_wait with fewer than 4 sequencers", - "[sequencer][REQ-SEQ-003]") { + "[request][REQ-SEQ-003]") { FeatureSet f; f.compound = true; f.compound_wait = true; f.clear_non_safestate = true; f.sequencer_count = kMinCompoundSequencers - 1; - REQUIRE(validate_feature_bundles(f) == make_error_code(SequencerErrc::compound_bundle_incomplete)); + REQUIRE(validate_feature_bundles(f) == make_error_code(RequestErrc::compound_bundle_incomplete)); } TEST_CASE("validate_feature_bundles rejects compound+compound_wait without clear_non_safestate", - "[sequencer][REQ-SEQ-003]") { + "[request][REQ-SEQ-003]") { FeatureSet f; f.compound = true; f.compound_wait = true; f.sequencer_count = kMinCompoundSequencers; - REQUIRE(validate_feature_bundles(f) == make_error_code(SequencerErrc::compound_bundle_incomplete)); + REQUIRE(validate_feature_bundles(f) == make_error_code(RequestErrc::compound_bundle_incomplete)); } -TEST_CASE("validate_feature_bundles accepts the complete compound bundle", "[sequencer][REQ-SEQ-003]") { +TEST_CASE("validate_feature_bundles accepts the complete compound bundle", "[request][REQ-SEQ-003]") { FeatureSet f; f.compound = true; f.compound_wait = true; @@ -192,7 +194,7 @@ TEST_CASE("validate_feature_bundles accepts the complete compound bundle", "[seq } TEST_CASE("implemented_options_bits reports kOptConditionalRequests only when something is enabled", - "[sequencer][REQ-SEQ-003]") { + "[request][REQ-SEQ-003]") { FeatureSet none; REQUIRE(implemented_options_bits(none) == 0); @@ -204,7 +206,7 @@ TEST_CASE("implemented_options_bits reports kOptConditionalRequests only when so // ── Sequencer-state registers ───────────────────────────────────────────────── TEST_CASE("SequencerTable::ensure_size fills new slots with kDefaultState (1), not 0", - "[sequencer][REQ-SEQ-004]") { + "[request][REQ-SEQ-004]") { std::vector states; SequencerTable table(states); table.ensure_size(4); @@ -216,7 +218,7 @@ TEST_CASE("SequencerTable::ensure_size fills new slots with kDefaultState (1), n } } -TEST_CASE("SequencerTable::ensure_size never shrinks existing storage", "[sequencer][REQ-SEQ-004]") { +TEST_CASE("SequencerTable::ensure_size never shrinks existing storage", "[request][REQ-SEQ-004]") { std::vector states{5, 5, 5}; SequencerTable table(states); table.ensure_size(1); @@ -224,7 +226,7 @@ TEST_CASE("SequencerTable::ensure_size never shrinks existing storage", "[sequen } TEST_CASE("SequencerTable::try_advance advances only when current state matches expected_start", - "[sequencer][REQ-SEQ-004]") { + "[request][REQ-SEQ-004]") { std::vector states{SequencerTable::kDefaultState}; SequencerTable table(states); @@ -238,7 +240,7 @@ TEST_CASE("SequencerTable::try_advance advances only when current state matches REQUIRE(s == SequencerTable::kDefaultState + 1); } -TEST_CASE("SequencerTable::try_advance leaves a mismatched sequencer untouched", "[sequencer][REQ-SEQ-004]") { +TEST_CASE("SequencerTable::try_advance leaves a mismatched sequencer untouched", "[request][REQ-SEQ-004]") { std::vector states{9}; SequencerTable table(states); @@ -252,17 +254,17 @@ TEST_CASE("SequencerTable::try_advance leaves a mismatched sequencer untouched", REQUIRE(s == 9); } -TEST_CASE("SequencerTable reports index_out_of_range for an out-of-bounds index", "[sequencer][REQ-SEQ-004]") { +TEST_CASE("SequencerTable reports index_out_of_range for an out-of-bounds index", "[request][REQ-SEQ-004]") { std::vector states{1}; SequencerTable table(states); rcp::regmap::SequencerState s = 0; - REQUIRE(table.state_of(5, s) == make_error_code(SequencerErrc::index_out_of_range)); + REQUIRE(table.state_of(5, s) == make_error_code(RequestErrc::index_out_of_range)); bool advanced = false; - REQUIRE(table.try_advance(5, 1, advanced) == make_error_code(SequencerErrc::index_out_of_range)); + REQUIRE(table.try_advance(5, 1, advanced) == make_error_code(RequestErrc::index_out_of_range)); } TEST_CASE("SequencerTable can be constructed directly over a RegisterMap's sequencer_states", - "[sequencer][REQ-SEQ-004]") { + "[request][REQ-SEQ-004]") { rcp::regmap::RegisterMap regs; SequencerTable table(regs); table.ensure_size(4); @@ -272,7 +274,7 @@ TEST_CASE("SequencerTable can be constructed directly over a RegisterMap's seque // ── Cancellation kinds & shared cancellation semantics ──────────────────────── -TEST_CASE("RequestLedger::cancel_single cancels a pending request", "[sequencer][REQ-SEQ-005]") { +TEST_CASE("RequestLedger::cancel_single cancels a pending request", "[request][REQ-SEQ-005]") { RequestLedger ledger; RequestRecord rec; rec.transaction_num = 1; @@ -283,17 +285,17 @@ TEST_CASE("RequestLedger::cancel_single cancels a pending request", "[sequencer] std::error_code outcome; REQUIRE_FALSE(ledger.outcome_of(1, outcome)); - REQUIRE(outcome == make_error_code(SequencerErrc::request_canceled)); + REQUIRE(outcome == make_error_code(RequestErrc::request_canceled)); } TEST_CASE("RequestLedger::cancel_single reports REQUEST_NOT_FOUND for an unknown transaction", - "[sequencer][REQ-SEQ-005]") { + "[request][REQ-SEQ-005]") { RequestLedger ledger; - REQUIRE(ledger.cancel_single(77) == make_error_code(SequencerErrc::request_not_found)); + REQUIRE(ledger.cancel_single(77) == make_error_code(RequestErrc::request_not_found)); } TEST_CASE("RequestLedger::cancel_single leaves an already-executing request to finish", - "[sequencer][REQ-SEQ-005]") { + "[request][REQ-SEQ-005]") { RequestLedger ledger; RequestRecord rec; rec.transaction_num = 1; @@ -301,11 +303,11 @@ TEST_CASE("RequestLedger::cancel_single leaves an already-executing request to f REQUIRE_FALSE(ledger.start(1)); REQUIRE_FALSE(ledger.begin_execution(1)); - REQUIRE(ledger.cancel_single(1) == make_error_code(SequencerErrc::request_not_found)); + REQUIRE(ledger.cancel_single(1) == make_error_code(RequestErrc::request_not_found)); REQUIRE(ledger.find(1)->state == RequestState::UnderExecution); } -TEST_CASE("Cancelling a chained request cancels its successors", "[sequencer][REQ-SEQ-005]") { +TEST_CASE("Cancelling a chained request cancels its successors", "[request][REQ-SEQ-005]") { RequestLedger ledger; RequestRecord a; a.transaction_num = 1; @@ -328,7 +330,7 @@ TEST_CASE("Cancelling a chained request cancels its successors", "[sequencer][RE REQUIRE(ledger.find(3)->state == RequestState::Canceled); } -TEST_CASE("cancel_all(clear-all) cancels every pending/started request", "[sequencer][REQ-SEQ-005]") { +TEST_CASE("cancel_all(clear-all) cancels every pending/started request", "[request][REQ-SEQ-005]") { RequestLedger ledger; for (uint8_t i = 1; i <= 3; ++i) { RequestRecord rec; @@ -339,7 +341,7 @@ TEST_CASE("cancel_all(clear-all) cancels every pending/started request", "[seque for (uint8_t i = 1; i <= 3; ++i) REQUIRE(ledger.find(i)->state == RequestState::Canceled); } -TEST_CASE("cancel_all(clear-non-safestate) skips is_safety records", "[sequencer][REQ-SEQ-005]") { +TEST_CASE("cancel_all(clear-non-safestate) skips is_safety records", "[request][REQ-SEQ-005]") { RequestLedger ledger; RequestRecord normal; normal.transaction_num = 1; @@ -354,7 +356,7 @@ TEST_CASE("cancel_all(clear-non-safestate) skips is_safety records", "[sequencer REQUIRE(ledger.find(2)->state == RequestState::Pending); } -TEST_CASE("cancel_all does not disturb an already-executing request", "[sequencer][REQ-SEQ-005]") { +TEST_CASE("cancel_all does not disturb an already-executing request", "[request][REQ-SEQ-005]") { RequestLedger ledger; RequestRecord rec; rec.transaction_num = 1; @@ -369,7 +371,7 @@ TEST_CASE("cancel_all does not disturb an already-executing request", "[sequence // ── Request lifecycle state machine ─────────────────────────────────────────── TEST_CASE("RequestLedger enforces the forward-only pending->started->under_execution->finalized sequence", - "[sequencer][REQ-SEQ-006]") { + "[request][REQ-SEQ-006]") { RequestLedger ledger; RequestRecord rec; rec.transaction_num = 1; @@ -386,31 +388,31 @@ TEST_CASE("RequestLedger enforces the forward-only pending->started->under_execu REQUIRE(ledger.find(1)->state == RequestState::Finalized); } -TEST_CASE("RequestLedger rejects skipping a lifecycle step", "[sequencer][REQ-SEQ-006]") { +TEST_CASE("RequestLedger rejects skipping a lifecycle step", "[request][REQ-SEQ-006]") { RequestLedger ledger; RequestRecord rec; rec.transaction_num = 1; REQUIRE_FALSE(ledger.submit(rec)); - REQUIRE(ledger.begin_execution(1) == make_error_code(SequencerErrc::invalid_lifecycle_transition)); - REQUIRE(ledger.finalize(1, false) == make_error_code(SequencerErrc::invalid_lifecycle_transition)); + REQUIRE(ledger.begin_execution(1) == make_error_code(RequestErrc::invalid_lifecycle_transition)); + REQUIRE(ledger.finalize(1, false) == make_error_code(RequestErrc::invalid_lifecycle_transition)); } -TEST_CASE("RequestLedger::submit rejects a duplicate transaction_num", "[sequencer][REQ-SEQ-006]") { +TEST_CASE("RequestLedger::submit rejects a duplicate transaction_num", "[request][REQ-SEQ-006]") { RequestLedger ledger; RequestRecord rec; rec.transaction_num = 1; REQUIRE_FALSE(ledger.submit(rec)); - REQUIRE(ledger.submit(rec) == make_error_code(SequencerErrc::transaction_num_collision)); + REQUIRE(ledger.submit(rec) == make_error_code(RequestErrc::transaction_num_collision)); } -TEST_CASE("RequestLedger reports unknown_transaction for an untracked id", "[sequencer][REQ-SEQ-006]") { +TEST_CASE("RequestLedger reports unknown_transaction for an untracked id", "[request][REQ-SEQ-006]") { RequestLedger ledger; - REQUIRE(ledger.start(99) == make_error_code(SequencerErrc::unknown_transaction)); + REQUIRE(ledger.start(99) == make_error_code(RequestErrc::unknown_transaction)); } TEST_CASE("finalize advances a compound request's sequencer on successful finalization", - "[sequencer][REQ-SEQ-006]") { + "[request][REQ-SEQ-006]") { std::vector states{SequencerTable::kDefaultState}; SequencerTable table(states); @@ -431,7 +433,7 @@ TEST_CASE("finalize advances a compound request's sequencer on successful finali } TEST_CASE("finalize does not advance the sequencer when its state no longer matches expected_start", - "[sequencer][REQ-SEQ-006]") { + "[request][REQ-SEQ-006]") { std::vector states{99}; // not the expected start state SequencerTable table(states); @@ -454,7 +456,7 @@ TEST_CASE("finalize does not advance the sequencer when its state no longer matc // ── Chained-successor propagation on predecessor finalization ──────────────── TEST_CASE("finalize with errored=true aborts a chained successor whose cs forbids executing anyway", - "[sequencer][REQ-SEQ-007]") { + "[request][REQ-SEQ-007]") { RequestLedger ledger; RequestRecord predecessor; predecessor.transaction_num = 1; @@ -474,7 +476,7 @@ TEST_CASE("finalize with errored=true aborts a chained successor whose cs forbid } TEST_CASE("finalize with errored=true does not abort a chained successor with cs=execute-regardless", - "[sequencer][REQ-SEQ-007]") { + "[request][REQ-SEQ-007]") { RequestLedger ledger; RequestRecord predecessor; predecessor.transaction_num = 1; @@ -494,7 +496,7 @@ TEST_CASE("finalize with errored=true does not abort a chained successor with cs } TEST_CASE("finalize with errored=false never aborts a chained successor regardless of its cs", - "[sequencer][REQ-SEQ-007]") { + "[request][REQ-SEQ-007]") { RequestLedger ledger; RequestRecord predecessor; predecessor.transaction_num = 1; @@ -515,38 +517,38 @@ TEST_CASE("finalize with errored=false never aborts a chained successor regardle // ── The `cs` field's dual meaning ───────────────────────────────────────────── -TEST_CASE("compound_wait_check_of maps cs to Immediate vs AfterChangeOnly", "[sequencer][REQ-SEQ-008]") { +TEST_CASE("compound_wait_check_of maps cs to Immediate vs AfterChangeOnly", "[request][REQ-SEQ-008]") { REQUIRE(compound_wait_check_of(true) == CompoundWaitCheck::Immediate); REQUIRE(compound_wait_check_of(false) == CompoundWaitCheck::AfterChangeOnly); } TEST_CASE("should_execute_chained: cs=true always executes regardless of predecessor outcome", - "[sequencer][REQ-SEQ-008]") { + "[request][REQ-SEQ-008]") { REQUIRE(should_execute_chained(/*cs=*/true, /*predecessor_errored=*/true)); REQUIRE(should_execute_chained(/*cs=*/true, /*predecessor_errored=*/false)); } TEST_CASE("should_execute_chained: cs=false aborts only when the predecessor errored", - "[sequencer][REQ-SEQ-008]") { + "[request][REQ-SEQ-008]") { REQUIRE_FALSE(should_execute_chained(/*cs=*/false, /*predecessor_errored=*/true)); REQUIRE(should_execute_chained(/*cs=*/false, /*predecessor_errored=*/false)); } -// ── SequencerErrc category sanity ───────────────────────────────────────────── +// ── RequestErrc category sanity ───────────────────────────────────────────── -TEST_CASE("SequencerErrc reports a non-empty message in its own category", "[sequencer][REQ-SEQ-009]") { - auto ec = make_error_code(SequencerErrc::request_canceled); - REQUIRE(ec.category() == sequencer_category()); +TEST_CASE("RequestErrc reports a non-empty message in its own category", "[request][REQ-SEQ-009]") { + auto ec = make_error_code(RequestErrc::request_canceled); + REQUIRE(ec.category() == request_category()); REQUIRE_FALSE(ec.message().empty()); } -TEST_CASE("every SequencerErrc value has a distinct, non-empty message", "[sequencer][REQ-SEQ-009]") { - const SequencerErrc all[] = { - SequencerErrc::timestamp_not_repurposed, SequencerErrc::unknown_request_type, - SequencerErrc::index_out_of_range, SequencerErrc::unknown_transaction, - SequencerErrc::invalid_lifecycle_transition, SequencerErrc::transaction_num_collision, - SequencerErrc::request_not_found, SequencerErrc::request_canceled, - SequencerErrc::compound_bundle_incomplete, +TEST_CASE("every RequestErrc value has a distinct, non-empty message", "[request][REQ-SEQ-009]") { + const RequestErrc all[] = { + RequestErrc::timestamp_not_repurposed, RequestErrc::unknown_request_type, + RequestErrc::index_out_of_range, RequestErrc::unknown_transaction, + RequestErrc::invalid_lifecycle_transition, RequestErrc::transaction_num_collision, + RequestErrc::request_not_found, RequestErrc::request_canceled, + RequestErrc::compound_bundle_incomplete, }; std::vector messages; for (auto e : all) { @@ -561,7 +563,7 @@ TEST_CASE("every SequencerErrc value has a distinct, non-empty message", "[seque // ── Safety-tagged (0x8x) request variants (ROADMAP.md milestone 50, v2.6.0) ── -TEST_CASE("decode_request_type accepts the three 0x8x safety-tagged opcodes", "[sequencer][REQ-SEQ-010]") { +TEST_CASE("decode_request_type accepts the three 0x8x safety-tagged opcodes", "[request][REQ-SEQ-010]") { std::array params{1, 2, 3, 4, 5, 6, 7}; for (auto opcode : {RequestTypeOpcode::CompoundSafety, RequestTypeOpcode::CompoundWaitSafety, RequestTypeOpcode::TriggeredSafety}) { @@ -577,15 +579,15 @@ TEST_CASE("decode_request_type accepts the three 0x8x safety-tagged opcodes", "[ TEST_CASE("decode_request_type rejects an MSB-set byte that is not one of the three defined " "safety opcodes", - "[sequencer][REQ-SEQ-010]") { + "[request][REQ-SEQ-010]") { RequestTypeOpcode type{}; std::array out_params{}; // 0x80 has the MSB set but is not CompoundWaitSafety/TriggeredSafety/CompoundSafety. auto ec = decode_request_type(/*mtv=*/false, uint64_t{0x80} << 56, type, out_params); - REQUIRE(ec == make_error_code(SequencerErrc::unknown_request_type)); + REQUIRE(ec == make_error_code(RequestErrc::unknown_request_type)); } -TEST_CASE("is_safety_variant identifies exactly the three 0x8x opcodes", "[sequencer][REQ-SEQ-011]") { +TEST_CASE("is_safety_variant identifies exactly the three 0x8x opcodes", "[request][REQ-SEQ-011]") { REQUIRE(is_safety_variant(RequestTypeOpcode::CompoundSafety)); REQUIRE(is_safety_variant(RequestTypeOpcode::CompoundWaitSafety)); REQUIRE(is_safety_variant(RequestTypeOpcode::TriggeredSafety)); @@ -598,14 +600,14 @@ TEST_CASE("is_safety_variant identifies exactly the three 0x8x opcodes", "[seque } TEST_CASE("category_of maps each safety-tagged opcode to its base opcode's priority category", - "[sequencer][REQ-SEQ-011]") { + "[request][REQ-SEQ-011]") { REQUIRE(category_of(RequestTypeOpcode::CompoundSafety) == category_of(RequestTypeOpcode::Compound)); REQUIRE(category_of(RequestTypeOpcode::CompoundWaitSafety) == category_of(RequestTypeOpcode::CompoundWait)); REQUIRE(category_of(RequestTypeOpcode::TriggeredSafety) == category_of(RequestTypeOpcode::Triggered)); } -TEST_CASE("request_record_for derives is_safety from the opcode automatically", "[sequencer][REQ-SEQ-012]") { +TEST_CASE("request_record_for derives is_safety from the opcode automatically", "[request][REQ-SEQ-012]") { auto normal = request_record_for(/*transaction_num=*/1, RequestTypeOpcode::Compound, /*cs=*/true); REQUIRE_FALSE(normal.is_safety); REQUIRE(normal.transaction_num == 1); @@ -621,7 +623,7 @@ TEST_CASE("request_record_for derives is_safety from the opcode automatically", } TEST_CASE("request_record_for-built safety records survive cancel_all(non_safestate_only=true)", - "[sequencer][REQ-SEQ-012]") { + "[request][REQ-SEQ-012]") { RequestLedger ledger; REQUIRE_FALSE(ledger.submit(request_record_for(1, RequestTypeOpcode::Triggered, false))); REQUIRE_FALSE(ledger.submit(request_record_for(2, RequestTypeOpcode::TriggeredSafety, false))); From b617815e6b5341aa40268212daebb298ef0bfb96 Mon Sep 17 00:00:00 2001 From: Matt <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:41:07 -0700 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20release=20v2.7.1=20=E2=80=94=20mod?= =?UTF-8?q?ule=20naming=20reconciliation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No behavior change; version bump to mark the rcp/wire.hpp -> rcp/avtp.hpp + rcp/acf.hpp split and rcp/sequencer.hpp -> rcp/request.hpp rename per RELAY spec v1.14 §13.7.2, matching this repo's convention of bumping kVersion/PROJECT_VERSION per merged PR. Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com> --- CMakeLists.txt | 2 +- include/rcp/version.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 229f568..716f2f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.21) project(cpp-rcp - VERSION 2.7.0 + VERSION 2.7.1 DESCRIPTION "Remote Control Protocol for automotive zonal architecture (C++)" HOMEPAGE_URL "https://github.com/SoundMatt/cpp-RCP" LANGUAGES CXX diff --git a/include/rcp/version.hpp b/include/rcp/version.hpp index ba8982b..d9dff9c 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.7.0"; +constexpr std::string_view kVersion = "2.7.1"; } // namespace rcp