Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2655,6 +2655,118 @@
"standard": "relay",
"level": "QM",
"asil": "QM"
},
{
"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.",
"standard": "iso26262",
"level": "ASIL-B",
"asil": "ASIL-B"
},
{
"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.",
"standard": "iso26262",
"level": "ASIL-B",
"asil": "ASIL-B"
},
{
"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.",
"standard": "iso26262",
"level": "ASIL-B",
"asil": "ASIL-B"
},
{
"id": "REQ-WIRE-004",
"title": "ACF_ABB carries no timestamp field",
"text": "encode_acf_abb/decode_acf_abb shall round-trip the shared ACF header and payload for an ACF_ABB (0x0E) message, and the decoded AcfMessageInfo::mtv shall always be false for ACF_ABB since it defines no timestamp slot.",
"standard": "iso26262",
"level": "ASIL-B",
"asil": "ASIL-B"
},
{
"id": "REQ-WIRE-005",
"title": "ACF_GBB carries a 64-bit message_timestamp",
"text": "encode_acf_gbb/decode_acf_gbb shall round-trip the shared ACF header, a 64-bit message_timestamp value, and the payload unchanged for an ACF_GBB (0x0D) message.",
"standard": "iso26262",
"level": "ASIL-B",
"asil": "ASIL-B"
},
{
"id": "REQ-WIRE-006",
"title": "Shared byte_message_info fields round-trip",
"text": "AcfMessageInfo encode/decode shall preserve acf_msg_type, acf_msg_length, pad, mtv, byte_bus_id, the evt ack flag and 3-bit sub-opcode, hs, cs, transaction_num, op, rsp, err, ms, and the dual-purpose read_size/segment_num field unchanged through an encode/decode round trip.",
"standard": "iso26262",
"level": "ASIL-B",
"asil": "ASIL-B"
},
{
"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.",
"standard": "iso26262",
"level": "ASIL-B",
"asil": "ASIL-B"
},
{
"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.",
"standard": "iso26262",
"level": "ASIL-B",
"asil": "ASIL-B"
},
{
"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.",
"standard": "iso26262",
"level": "ASIL-B",
"asil": "ASIL-B"
},
{
"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.",
"standard": "iso26262",
"level": "ASIL-B",
"asil": "ASIL-B"
},
{
"id": "REQ-WIRE-011",
"title": "avtp_timestamp validity is explicit, not implied by zero",
"text": "A TSCF header with timestamp_valid false shall be distinguishable from one with a valid, zero-valued avtp_timestamp; decode_tscf_header shall preserve the timestamp_valid flag independently of the avtp_timestamp value.",
"standard": "iso26262",
"level": "ASIL-B",
"asil": "ASIL-B"
},
{
"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.",
"standard": "iso26262",
"level": "ASIL-B",
"asil": "ASIL-B"
},
{
"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.",
"standard": "iso26262",
"level": "ASIL-B",
"asil": "ASIL-B"
},
{
"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.",
"standard": "iso26262",
"level": "ASIL-B",
"asil": "ASIL-B"
}
]
}
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.21)

project(cpp-rcp
VERSION 1.2.0
VERSION 2.0.0
DESCRIPTION "Remote Control Protocol for automotive zonal architecture (C++)"
HOMEPAGE_URL "https://github.com/SoundMatt/cpp-RCP"
LANGUAGES CXX
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,12 @@ reference.

| Header | Description |
|---|---|
| `<rcp/udp.hpp>` | Pure-C++ UDP transport for the RCP protocol |
| `<rcp/udp.hpp>` | Pure-C++ UDP transport for the RCP protocol (still framed via `<rcp/legacy_wire.hpp>` until v2.13.0) |
| `<rcp/tls.hpp>` | Mutual TLS transport for zone controller communication |
| `<rcp/shmem.hpp>` | Zero-copy intra-host command delivery via shared in-process memory |
| `<rcp/tsn.hpp>` | IEEE 802.1Qbv-aware UDP transport adapter for hard real-time Ethernet |
| `<rcp/wire.hpp>` | Binary frame codec shared by the UDP and TLS transports |
| `<rcp/wire.hpp>` | TC18 wire codec — IEEE 1722 AVTPDU (NTSCF/TSCF) framing and ACF_ABB/ACF_GBB messages (ROADMAP.md v2.0.0) |
| `<rcp/legacy_wire.hpp>` | Pre-replacement 16-byte frame codec, kept only for `<rcp/udp.hpp>`'s internal use until it is rebuilt at v2.13.0 |
| `<rcp/sim.hpp>` | Timing-realistic zone controller simulator for SiL/HIL testing |

## Build
Expand Down
18 changes: 18 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,24 @@ and full endpoint coverage is not reached until v2.7.0.

### 44. Wire Format Core (v2.0.0)

**Done (v2.0.0):** `rcp/wire.hpp` is now the IEEE 1722 AVTPDU/ACF codec
described below — NTSCF/TSCF headers, ACF_ABB/ACF_GBB messages, the shared
`AcfMessageInfo` ("byte_message_info") fields, `StreamId`/`byte_bus_id`
addressing with an enforced echo-back rule, the mandatory standard request
kind, the four response semantic types, and `avtp_timestamp`/
`message_timestamp` fallback handling via `effective_timestamp`. It has no
dependency on `rcp.hpp`'s Zone/Command/Controller/Registry model or on any
later-phase lifecycle/discovery/endpoint behavior, and makes no assumption
about the underlying transport. The old 16-byte frame codec this file used
to define is preserved unchanged, under `rcp/legacy_wire.hpp`, purely so
`rcp/udp.hpp` keeps building and working until it is rebuilt against real
AVTPDU framing at v2.13.0 — see the disposition table entry for `wire.hpp`
and the header comment in `legacy_wire.hpp`. New coverage lives in
`tests/test_wire.cpp` (REQ-WIRE-001..014); the prior 16-byte-frame tests
moved to `tests/test_legacy_wire.cpp` unchanged (REQ-UDP-001..012). Full
bit-for-bit wire conformance is not claimed at this milestone — that lands
incrementally through v2.6.0 per the Phase 13 introduction above.

- Replace the bespoke 16-byte `wire.hpp` header entirely with real IEEE 1722
framing: NTSCF (non-time-synchronous, server-only-outbound) and TSCF
(time-synchronous, client-only) AVTPDU headers (extraction §2.2)
Expand Down
230 changes: 230 additions & 0 deletions include/rcp/legacy_wire.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// fusa:req REQ-UDP-001
// fusa:req REQ-UDP-002
// fusa:req REQ-UDP-003
// fusa:req REQ-UDP-004
// fusa:req REQ-UDP-005
// fusa:req REQ-UDP-006
// fusa:req REQ-UDP-007
// fusa:req REQ-UDP-008
// fusa:req REQ-UDP-009
// fusa:req REQ-UDP-010
// fusa:req REQ-UDP-011
// fusa:req REQ-UDP-012

// 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
// 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
// disposition table). Do not build new functionality on top of this file —
// it is a deliberately temporary bridge, not a second wire format going
// forward.
//
// Frame layout (all multi-byte fields big-endian):
// [0] Magic 'R'
// [1] Magic 'C'
// [2] Protocol version (0x01)
// [3] Message type (TypeCommand / TypeResponse / TypeStatus / …)
// [4] Zone (uint8)
// [5:7] CommandType or 0x0000 (uint16)
// [7] Priority, ResponseStatus, or Healthy flag (uint8)
// [8:12] Command/response ID or Status seq (uint32)
// [12:16] Payload length (uint32)
// [16:] Payload (variable)
#pragma once

#include "rcp.hpp"

#include <cstdint>
#include <system_error>

namespace rcp {
namespace legacy_wire {

constexpr uint8_t MagicByte0 = 0x52; // 'R'
constexpr uint8_t MagicByte1 = 0x43; // 'C'
constexpr uint8_t ProtoVer = 0x01;

constexpr uint8_t TypeCommand = 0x01;
constexpr uint8_t TypeResponse = 0x02;
constexpr uint8_t TypeStatus = 0x03;
constexpr uint8_t TypeSubscribe = 0x04;
constexpr uint8_t TypeUnsubscribe = 0x05;

constexpr size_t HeaderLen = 16;
constexpr size_t MaxPayload = 65507 - HeaderLen; // fits in one UDP datagram

// ── Codec errors ──────────────────────────────────────────────────────────────

enum class WireErrc : int {
short_frame = 1,
bad_magic = 2,
bad_version = 3,
};

inline const std::error_category& wire_category() noexcept {
struct Cat : std::error_category {
const char* name() const noexcept override { return "rcp.legacy_wire"; }
std::string message(int ev) const override {
switch (static_cast<WireErrc>(ev)) {
case WireErrc::short_frame: return "rcp/legacy_wire: frame too short";
case WireErrc::bad_magic: return "rcp/legacy_wire: bad magic bytes";
case WireErrc::bad_version: return "rcp/legacy_wire: unsupported protocol version";
default: return "rcp/legacy_wire: unknown error";
}
}
};
static Cat instance;
return instance;
}

inline std::error_code make_error_code(WireErrc e) noexcept {
return {static_cast<int>(e), wire_category()};
}

// ── Internal helpers ──────────────────────────────────────────────────────────

namespace detail {

inline void put_u16(uint8_t* p, uint16_t v) noexcept {
p[0] = static_cast<uint8_t>(v >> 8);
p[1] = static_cast<uint8_t>(v & 0xFF);
}
inline void put_u32(uint8_t* p, uint32_t v) noexcept {
p[0] = static_cast<uint8_t>(v >> 24);
p[1] = static_cast<uint8_t>((v >> 16) & 0xFF);
p[2] = static_cast<uint8_t>((v >> 8) & 0xFF);
p[3] = static_cast<uint8_t>(v & 0xFF);
}
inline uint16_t get_u16(const uint8_t* p) noexcept {
return static_cast<uint16_t>((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]);
}

} // namespace detail

// ── Header validation ─────────────────────────────────────────────────────────

inline std::error_code validate_header(const uint8_t* b, size_t len) noexcept {
if (len < HeaderLen)
return make_error_code(WireErrc::short_frame);
if (b[0] != MagicByte0 || b[1] != MagicByte1)
return make_error_code(WireErrc::bad_magic);
if (b[2] != ProtoVer)
return make_error_code(WireErrc::bad_version);
return {};
}

// ── Command encode / decode ───────────────────────────────────────────────────

inline std::vector<uint8_t> encode_command(const Command& cmd) {
const size_t n = HeaderLen + cmd.payload.size();
std::vector<uint8_t> buf(n);
buf[0] = MagicByte0;
buf[1] = MagicByte1;
buf[2] = ProtoVer;
buf[3] = TypeCommand;
buf[4] = static_cast<uint8_t>(cmd.zone);
detail::put_u16(&buf[5], static_cast<uint16_t>(cmd.type));
buf[7] = static_cast<uint8_t>(cmd.priority);
detail::put_u32(&buf[8], cmd.id);
detail::put_u32(&buf[12], static_cast<uint32_t>(cmd.payload.size()));
std::copy(cmd.payload.begin(), cmd.payload.end(), buf.begin() + HeaderLen);
return buf;
}

inline std::error_code decode_command(const uint8_t* b, size_t len, Command& out) {
if (auto ec = validate_header(b, len)) return ec;
uint32_t body_len = detail::get_u32(&b[12]);
if (len < HeaderLen + body_len)
return make_error_code(WireErrc::short_frame);
out.zone = static_cast<Zone>(b[4]);
out.type = static_cast<CommandType>(detail::get_u16(&b[5]));
out.priority = static_cast<Priority>(b[7]);
out.id = detail::get_u32(&b[8]);
out.payload.assign(b + HeaderLen, b + HeaderLen + body_len);
return {};
}

// ── Response encode / decode ──────────────────────────────────────────────────

inline std::vector<uint8_t> encode_response(const Response& resp) {
const size_t n = HeaderLen + resp.payload.size();
std::vector<uint8_t> buf(n);
buf[0] = MagicByte0;
buf[1] = MagicByte1;
buf[2] = ProtoVer;
buf[3] = TypeResponse;
buf[4] = static_cast<uint8_t>(resp.zone);
detail::put_u16(&buf[5], 0);
buf[7] = static_cast<uint8_t>(resp.status);
detail::put_u32(&buf[8], resp.command_id);
detail::put_u32(&buf[12], static_cast<uint32_t>(resp.payload.size()));
std::copy(resp.payload.begin(), resp.payload.end(), buf.begin() + HeaderLen);
return buf;
}

inline std::error_code decode_response(const uint8_t* b, size_t len, Response& out) {
if (auto ec = validate_header(b, len)) return ec;
uint32_t body_len = detail::get_u32(&b[12]);
if (len < HeaderLen + body_len)
return make_error_code(WireErrc::short_frame);
out.zone = static_cast<Zone>(b[4]);
out.status = static_cast<ResponseStatus>(b[7]);
out.command_id = detail::get_u32(&b[8]);
out.payload.assign(b + HeaderLen, b + HeaderLen + body_len);
return {};
}

// ── Status encode / decode ────────────────────────────────────────────────────

inline std::vector<uint8_t> encode_status(const Status& st) {
const size_t n = HeaderLen + st.payload.size();
std::vector<uint8_t> buf(n);
buf[0] = MagicByte0;
buf[1] = MagicByte1;
buf[2] = ProtoVer;
buf[3] = TypeStatus;
buf[4] = static_cast<uint8_t>(st.zone);
detail::put_u16(&buf[5], 0);
buf[7] = st.healthy ? 1u : 0u;
detail::put_u32(&buf[8], st.seq);
detail::put_u32(&buf[12], static_cast<uint32_t>(st.payload.size()));
std::copy(st.payload.begin(), st.payload.end(), buf.begin() + HeaderLen);
return buf;
}

inline std::error_code decode_status(const uint8_t* b, size_t len, Status& out) {
if (auto ec = validate_header(b, len)) return ec;
uint32_t body_len = detail::get_u32(&b[12]);
if (len < HeaderLen + body_len)
return make_error_code(WireErrc::short_frame);
out.zone = static_cast<Zone>(b[4]);
out.healthy = (b[7] == 1);
out.seq = detail::get_u32(&b[8]);
out.payload.assign(b + HeaderLen, b + HeaderLen + body_len);
return {};
}

// ── Control frame (subscribe / unsubscribe) ───────────────────────────────────

inline std::vector<uint8_t> encode_control(uint8_t msg_type, Zone zone) {
std::vector<uint8_t> buf(HeaderLen, 0);
buf[0] = MagicByte0;
buf[1] = MagicByte1;
buf[2] = ProtoVer;
buf[3] = msg_type;
buf[4] = static_cast<uint8_t>(zone);
return buf;
}

} // namespace legacy_wire
} // namespace rcp
Loading
Loading