diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 9306e1b..df71483 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -128,7 +128,7 @@ jobs:
with:
submodules: true
- run: cmake -S . -B $GITHUB_WORKSPACE/build -DCMAKE_BUILD_TYPE=Debug -DNO_STATIC_ANALYSIS=ON
- - run: cd $GITHUB_WORKSPACE/build && make -j"$(nproc)" udp_echo udp_time_pub
+ - run: cd $GITHUB_WORKSPACE/build && make -j"$(nproc)" example_echo example_time_pub
- run: python3 tools/ci_example_smoke.py --build-dir $GITHUB_WORKSPACE/build
test_model_montecarlo:
diff --git a/.gitmodules b/.gitmodules
index a6b0297..f8eeca5 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,7 @@
[submodule "lib/unity"]
path = lib/unity
url = https://github.com/ThrowTheSwitch/Unity
+[submodule "lib/libcanard"]
+ path = lib/libcanard
+ url = https://github.com/OpenCyphal/libcanard
+ branch = experimental
diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
index 0cbec32..616297b 100644
--- a/.idea/dictionaries/project.xml
+++ b/.idea/dictionaries/project.xml
@@ -21,6 +21,7 @@
cyphal
cyudp
deinit
+ destructions
dgrams
dscp
enroute
diff --git a/CMakeLists.txt b/CMakeLists.txt
index aaa60d7..158ccdc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -54,6 +54,13 @@ if (NOT NO_STATIC_ANALYSIS)
${CMAKE_SOURCE_DIR}/tests/*/*.[ch]pp
${CMAKE_SOURCE_DIR}/examples/*.[ch]
${CMAKE_SOURCE_DIR}/cy_udp_posix/*.[ch]
+ ${CMAKE_SOURCE_DIR}/cy_udp_posix/tests/*.[ch]
+ ${CMAKE_SOURCE_DIR}/cy_can/tests/*.[ch]
+ ${CMAKE_SOURCE_DIR}/cy_udp_posix/tests/*/*.[ch]
+ ${CMAKE_SOURCE_DIR}/cy_udp_posix/tests/*/*.[ch]pp
+ ${CMAKE_SOURCE_DIR}/cy_can/*.[ch]
+ ${CMAKE_SOURCE_DIR}/cy_can/tests/*/*.[ch]
+ ${CMAKE_SOURCE_DIR}/cy_can/tests/*/*.[ch]pp
)
message(STATUS "Using clang-format: ${clang_format}; files to format: ${format_files}")
add_custom_target(format COMMAND ${clang_format} -i -fallback-style=none -style=file --verbose ${format_files})
@@ -105,5 +112,6 @@ endif ()
# SUBDIRECTORIES
add_subdirectory(cy_udp_posix)
+add_subdirectory(cy_can)
add_subdirectory(examples)
add_subdirectory(tests)
diff --git a/README.md b/README.md
index a207d06..e514e0e 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,10 @@ On an embedded system, one may also prefer to use [`o1heap`](https://github.com/
management, but this is not a hard dependency -- any allocator will work. O1Heap is the recommended choice for embedded
platforms due to its hard determinism and low fragmentation.
+Pick one of the transport/platform glue layers suitable for your application: `cy_can`, `cy_udp_posix`, etc.
+The integration instructions are identical: simply copy the C files and add them to your build system.
+These components may be moved into dedicated repositories in the future.
+
🌐 A live demo of the distributed consensus algorithm can be found at .
For a detailed design overview, refer to `model/`.
@@ -49,8 +53,9 @@ The specifics of setting up a local node depend on the platform and transport us
unlike the rest of the API, which is entirely platform- and transport-agnostic.
```c++
-#include // platform- and transport-agnostic Cyphal API
-#include // thin low-level glue specific to Cyphal/UDP on POSIX systems; choose one for your setup
+#include // platform- and transport-agnostic Cyphal API
+#include // thin low-level glue specific to Cyphal/UDP on POSIX systems; choose one for your setup
+#include // thin low-level glue specific to Cyphal/CAN on SocketCAN; choose one for your setup
int main(void)
{
@@ -58,6 +63,10 @@ int main(void)
// Here we're using Cyphal/UDP on POSIX as an example.
cy_platform_t* platform = cy_udp_posix_new();
if (platform == NULL) { ... }
+
+ // If you need Cyphal/CAN on SocketCAN instead, just replace the above with:
+ cy_platform_t* platform = cy_can_socketcan_new(1, (const char*[]){"can0"}, 1000); // 1 iface, 1000 frames TX queue
+ if (platform == NULL) { ... }
// Set up the local Cyphal node instance.
// Every node needs a home, which should be unique across the network.
@@ -150,7 +159,7 @@ Do not destroy unwanted futures right away because that cancels the associated o
cy_future_callback_set(future, cy_future_destroy); // Will destroy itself when done, no need to keep the reference.
```
-The examples folder contains a simple publisher example `main_udp_time_pub.c`.
+The examples folder contains a simple publisher example `example_time_pub.c`.
### 📩 Subscribe to topics and receive messages
@@ -255,7 +264,7 @@ for (size_t i = 0; i < subs.count; i++) {
It is also possible to monitor subscriber liveness and alert the application via its callback when messages cease to
arrive; see the API docs for details.
-The examples folder contains a simple subscriber example `main_udp_echo.c`.
+The examples folder contains a simple subscriber example `example_echo.c`.
### 🔄 RPC & streaming
diff --git a/cy/cy.c b/cy/cy.c
index 010f9d0..07b7e9c 100644
--- a/cy/cy.c
+++ b/cy/cy.c
@@ -837,7 +837,8 @@ static cy_tree_t* reader_cavl_factory(void* const user)
return NULL;
}
r->handle->subject_id = ctx->subject_id;
- r->handle->extent = ctx->extent; // In case the platform layer didn't set it.
+ // Some platforms may revive a tombstoned incumbent whose extent is already larger than requested.
+ r->handle->extent = larger(r->handle->extent, ctx->extent);
}
return (cy_tree_t*)r;
}
diff --git a/cy/cy_platform.h b/cy/cy_platform.h
index 461a9b0..c901b4a 100644
--- a/cy/cy_platform.h
+++ b/cy/cy_platform.h
@@ -73,7 +73,7 @@ typedef struct cy_subject_writer_t
typedef struct cy_subject_reader_t
{
uint32_t subject_id;
- size_t extent;
+ size_t extent; // Platform must either set this to the initial value or zero at construction time.
} cy_subject_reader_t;
/// Abstracts away the specifics of the transport (UDP, serial, CAN, etc) and the platform where Cy is running
@@ -170,6 +170,7 @@ typedef struct cy_platform_vtable_t
/// The cy_on_message() callback will be invoked from this function.
/// This is the only platform function that is allowed to block.
/// May return additional error codes, which will be forwarded to the application as-is.
+ /// This can only be invoked from within cy_spin*() functions, and is never invoked from cy_on_message().
cy_err_t (*spin)(cy_platform_t*, cy_us_t deadline);
// === MISC ===
diff --git a/cy_can/CMakeLists.txt b/cy_can/CMakeLists.txt
new file mode 100644
index 0000000..00a3db1
--- /dev/null
+++ b/cy_can/CMakeLists.txt
@@ -0,0 +1,38 @@
+# Copyright (c) Pavel Kirienko
+
+cmake_minimum_required(VERSION 3.24)
+project(cy_can C)
+
+include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/lib")
+
+# Libcanard static library.
+add_library(canard STATIC "${CMAKE_SOURCE_DIR}/lib/libcanard/libcanard/canard.c")
+target_compile_options(canard PRIVATE -Wno-cast-align)
+target_include_directories(canard SYSTEM INTERFACE ${CMAKE_SOURCE_DIR}/lib/libcanard/libcanard)
+set_target_properties(
+ canard
+ PROPERTIES
+ COMPILE_WARNING_AS_ERROR OFF
+ C_STANDARD_REQUIRED ON
+ C_EXTENSIONS OFF
+ C_CLANG_TIDY ""
+ CXX_CLANG_TIDY ""
+ C_CPPCHECK ""
+ CXX_CPPCHECK ""
+)
+
+# Platform-agnostic CAN transport glue static library.
+# Consumers link cy/cy.c themselves; this target only contains the transport/platform layer.
+add_library(cy_can STATIC cy_can.c)
+target_link_libraries(cy_can PUBLIC canard)
+target_include_directories(cy_can PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/cy)
+set_target_properties(cy_can PROPERTIES COMPILE_WARNING_AS_ERROR ON C_STANDARD_REQUIRED ON C_EXTENSIONS OFF)
+
+# Linux SocketCAN backend for cy_can. Includes cy_can and canard, making it self-contained except for cy.c.
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
+ add_library(cy_can_socketcan STATIC cy_can_socketcan.c)
+ target_link_libraries(cy_can_socketcan PUBLIC cy_can)
+ set_target_properties(cy_can_socketcan PROPERTIES COMPILE_WARNING_AS_ERROR ON C_STANDARD_REQUIRED ON C_EXTENSIONS OFF)
+endif ()
+
+add_subdirectory(tests)
diff --git a/cy_can/cy_can.c b/cy_can/cy_can.c
new file mode 100644
index 0000000..ea220b1
--- /dev/null
+++ b/cy_can/cy_can.c
@@ -0,0 +1,855 @@
+// ____ ______ __ __
+// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / /
+// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ /
+// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / /
+// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/
+// /_/ /____/_/
+//
+// Copyright (c) Pavel Kirienko
+
+#include "cy_can.h"
+#include
+
+#define RAPIDHASH_COMPACT
+#include
+
+#include
+#include
+
+#if __STDC_VERSION__ < 201112L
+#define static_assert(x, ...) typedef char _sa_gl(_sa_, __LINE__)[(x) ? 1 : -1]
+#define _sa_gl(a, b) _sa_gl2(a, b)
+#define _sa_gl2(a, b) a##b
+#endif
+
+/// The Cy session-layer header is fixed at 24 bytes. Must agree with cy.c.
+#define HEADER_BYTES 24U
+
+/// Service-ID reserved for v1.1 unicast transfers over Cyphal/CAN.
+#define UNICAST_SERVICE_ID 511U
+
+/// Default extent for incoming unicast messages; will grow as needed.
+#define UNICAST_EXTENT_INITIAL (HEADER_BYTES + 1024U)
+
+typedef struct
+{
+ cy_platform_t base; // Must be first (upcast pattern).
+ uint_least8_t iface_count;
+
+ canard_t canard;
+
+ canard_subscription_t unicast_sub; // Service-ID 511 for unicast.
+
+ // Per-remote unicast transfer-ID counters (one per possible CAN node-ID).
+ uint_least8_t unicast_tid[CANARD_NODE_ID_CAPACITY];
+
+ struct subject_reader_t* tombstone_head; // Deferred destruction list to avoid reentrancy issues.
+ struct subject_reader_t* tombstone_tail;
+
+ uint64_t oom_count; // cy_can-level OOM (message wrapper allocation failures, etc.).
+
+ const cy_can_vtable_t* vtable;
+ void* user;
+} cy_can_t;
+
+typedef struct subject_writer_t
+{
+ cy_subject_writer_t base;
+ uint_least8_t next_tid_13b;
+ uint_least8_t next_tid_16b;
+} subject_writer_t;
+
+typedef struct subject_reader_t
+{
+ cy_subject_reader_t base;
+ canard_subscription_t sub_16b;
+ cy_can_t* owner;
+ struct subject_reader_t* prev_tombstone;
+ struct subject_reader_t* next_tombstone;
+} subject_reader_t;
+
+// The runtime type is determined as pinned=subject_id<=CY_SUBJECT_ID_PINNED_MAX.
+typedef struct subject_reader_pinned_t
+{
+ subject_reader_t base;
+ canard_subscription_t sub_13b;
+ uint64_t phony_tag;
+ uint_least8_t phony_header[HEADER_BYTES]; // Precomputed for 13-bit reception.
+} subject_reader_pinned_t;
+
+static subject_reader_pinned_t* as_pinned(subject_reader_t* const self)
+{
+ return (self->base.subject_id <= CY_SUBJECT_ID_PINNED_MAX) ? (subject_reader_pinned_t*)self : NULL;
+}
+
+// =====================================================================================================================
+// MEMORY BRIDGE
+
+static void* can_mem_alloc(const canard_mem_t mem, const size_t size)
+{
+ cy_can_t* const self = (cy_can_t*)mem.context;
+ return self->vtable->realloc(self->user, NULL, size);
+}
+
+static void can_mem_free(const canard_mem_t mem, const size_t size, void* const ptr)
+{
+ cy_can_t* const self = (cy_can_t*)mem.context;
+ (void)size;
+ if (ptr != NULL) {
+ self->vtable->realloc(self->user, ptr, 0);
+ }
+}
+
+static const canard_mem_vtable_t can_mem_vtable = { .free = can_mem_free, .alloc = can_mem_alloc };
+
+static canard_mem_t make_mem(cy_can_t* const self)
+{
+ return (canard_mem_t){ .vtable = &can_mem_vtable, .context = self };
+}
+
+static canard_mem_set_t make_mem_set(cy_can_t* const self)
+{
+ const canard_mem_t m = make_mem(self);
+ return (canard_mem_set_t){ .tx_transfer = m, .tx_frame = m, .rx_session = m, .rx_payload = m, .rx_filters = m };
+}
+
+// =====================================================================================================================
+// SERIALIZATION HELPERS (little-endian, must match cy.c)
+
+static void le_serialize_u32(uint_least8_t* const ptr, const uint32_t value)
+{
+ for (size_t i = 0; i < 4; i++) {
+ ptr[i] = (uint_least8_t)((value >> (i * 8U)) & 0xFFU);
+ }
+}
+
+static void le_serialize_u64(uint_least8_t* const ptr, const uint64_t value)
+{
+ for (size_t i = 0; i < 8; i++) {
+ ptr[i] = (uint_least8_t)((value >> (i * 8U)) & 0xFFU);
+ }
+}
+
+/// Portable decimal conversion for subject-ID -> topic name hash. Buffer must be at least 11 bytes.
+static size_t uint_to_decimal(uint32_t value, char* const buf)
+{
+ char tmp[11];
+ size_t len = 0;
+ do {
+ tmp[len++] = (char)('0' + (char)(value % 10U));
+ value /= 10U;
+ } while (value > 0);
+ for (size_t i = 0; i < len; i++) {
+ buf[i] = tmp[len - 1U - i];
+ }
+ return len;
+}
+
+// =====================================================================================================================
+// MESSAGE BUFFER
+//
+// The logical message is seg0[] (flex array, inline data) concatenated with seg1 (optional external data),
+// with `skip` bytes removed from the front.
+//
+// - 16-bit single-frame: seg0 = [payload copy], seg1 = NULL. origin = NULL.
+// - 16-bit multi-frame: seg0 = [] (empty), seg1 = view. origin = canard buffer.
+// - 13-bit single-frame: seg0 = [phony hdr + payload], seg1 = NULL. origin = NULL.
+// - 13-bit multi-frame: seg0 = [phony hdr], seg1 = view. origin = canard buffer.
+
+typedef struct
+{
+ cy_message_t base;
+ cy_can_t* owner;
+ void* origin; // Multi-frame payload buffer from canard (NULL for single-frame). Freed on destroy.
+ const void* seg1; // Second segment: points into origin (NULL if single-segment).
+ size_t seg1_len;
+ size_t seg0_len; // Length of inline data in seg0[].
+ size_t skip;
+ uint_least8_t seg0[]; // Flex array: inline data (phony header and/or single-frame payload).
+} can_message_t;
+
+static void v_message_skip(cy_message_t* const base, const size_t offset)
+{
+ can_message_t* const self = (can_message_t*)base;
+ const size_t total = self->seg0_len + self->seg1_len;
+ const size_t avail = (total > self->skip) ? (total - self->skip) : 0;
+ self->skip += (offset < avail) ? offset : avail;
+}
+
+static size_t v_message_read(const cy_message_t* const base, const size_t offset, const size_t size, void* const dest)
+{
+ const can_message_t* const self = (const can_message_t*)base;
+ const size_t total = self->seg0_len + self->seg1_len;
+ const size_t start = self->skip + offset;
+ if (start >= total) {
+ return 0;
+ }
+ const size_t avail = total - start;
+ const size_t to_read = (size < avail) ? size : avail;
+ size_t done = 0;
+ size_t pos = start;
+ char* out = (char*)dest;
+ if (pos < self->seg0_len) {
+ const size_t rem = self->seg0_len - pos;
+ const size_t n = ((to_read - done) < rem) ? (to_read - done) : rem;
+ (void)memcpy(out, self->seg0 + pos, n);
+ done += n;
+ out += n;
+ pos += n;
+ }
+ if ((done < to_read) && (self->seg1 != NULL)) {
+ const size_t n = to_read - done;
+ (void)memcpy(out, (const char*)self->seg1 + (pos - self->seg0_len), n);
+ done += n;
+ }
+ return done;
+}
+
+static size_t v_message_size(const cy_message_t* const base)
+{
+ const can_message_t* const self = (const can_message_t*)base;
+ const size_t total = self->seg0_len + self->seg1_len;
+ return (total > self->skip) ? (total - self->skip) : 0;
+}
+
+static void v_message_destroy(cy_message_t* const base)
+{
+ can_message_t* const self = (can_message_t*)base;
+ if (self->origin != NULL) {
+ self->owner->vtable->realloc(self->owner->user, self->origin, 0);
+ }
+ self->owner->vtable->realloc(self->owner->user, self, 0);
+}
+
+static const cy_message_vtable_t message_vtable = { .skip = v_message_skip,
+ .read = v_message_read,
+ .size = v_message_size,
+ .destroy = v_message_destroy };
+
+/// Allocate a can_message_t with `inline_extra` bytes for the flex array seg0[].
+static can_message_t* make_message(cy_can_t* const owner, const size_t inline_extra)
+{
+ can_message_t* const msg =
+ (can_message_t*)owner->vtable->realloc(owner->user, NULL, sizeof(can_message_t) + inline_extra);
+ if (msg != NULL) {
+ (void)memset(msg, 0, sizeof(*msg));
+ msg->base = CY_MESSAGE_INIT(&message_vtable);
+ msg->owner = owner;
+ }
+ return msg;
+}
+
+// =====================================================================================================================
+// cy_bytes_t -> canard_bytes_chain_t ALIASING
+
+static_assert(offsetof(canard_bytes_chain_t, bytes.size) == offsetof(cy_bytes_t, size), "");
+static_assert(offsetof(canard_bytes_chain_t, bytes.data) == offsetof(cy_bytes_t, data), "");
+static_assert(offsetof(canard_bytes_chain_t, next) == offsetof(cy_bytes_t, next), "");
+
+static canard_bytes_chain_t cy_bytes_to_canard(const cy_bytes_t message)
+{
+ return (canard_bytes_chain_t){ .bytes = { .size = message.size, .data = message.data },
+ .next = (const canard_bytes_chain_t*)message.next };
+}
+
+// =====================================================================================================================
+// CANARD VTABLE CALLBACKS
+
+static canard_us_t v_canard_now(const canard_t* const self)
+{
+ const cy_can_t* const owner = (const cy_can_t*)self->user_context;
+ return owner->vtable->now(owner->user);
+}
+
+static bool v_canard_tx(canard_t* const self,
+ void* const user_context,
+ const canard_us_t deadline,
+ const uint_least8_t iface_index,
+ const bool fd,
+ const uint32_t extended_can_id,
+ const canard_bytes_t can_data)
+{
+ cy_can_t* const owner = (cy_can_t*)self->user_context;
+ const uint_least8_t len = (uint_least8_t)can_data.size;
+ (void)user_context;
+ (void)deadline;
+ assert(iface_index < owner->iface_count);
+ if (fd && (owner->vtable->tx_fd != NULL)) {
+ return owner->vtable->tx_fd(owner->user, iface_index, extended_can_id, can_data.data, len);
+ }
+ return owner->vtable->tx_classic(owner->user, iface_index, extended_can_id, can_data.data, len);
+}
+
+static bool v_canard_filter(canard_t* const self, const size_t filter_count, const canard_filter_t* const filters)
+{
+ cy_can_t* const owner = (cy_can_t*)self->user_context;
+ assert((owner != NULL) && (owner->vtable != NULL) && (owner->vtable->filter != NULL));
+ return owner->vtable->filter(owner->user, filter_count, filters);
+}
+
+static const canard_vtable_t canard_vtbl_no_filter = { .now = v_canard_now, .tx = v_canard_tx, .filter = NULL };
+static const canard_vtable_t canard_vtbl_filter = { .now = v_canard_now, .tx = v_canard_tx, .filter = v_canard_filter };
+
+// =====================================================================================================================
+// SUBSCRIPTION CALLBACKS
+
+static void deliver(cy_can_t* const owner,
+ const uint32_t* const subject_id,
+ const uint_least8_t source_node_id,
+ const canard_prio_t priority,
+ const canard_us_t timestamp,
+ can_message_t* const msg)
+{
+ cy_lane_t lane;
+ (void)memset(&lane, 0, sizeof(lane));
+ lane.id = source_node_id;
+ lane.prio = (cy_prio_t)priority;
+ lane.ctx.state[0] = (unsigned char)source_node_id;
+ const cy_message_ts_t mts = { .timestamp = timestamp, .content = &msg->base };
+ cy_on_message(&owner->base, lane, subject_id, mts);
+}
+
+/// 16-bit subscription callback. Payload already contains the Cy session header.
+static void v_on_msg_16b(canard_subscription_t* const self,
+ const canard_us_t timestamp,
+ const canard_prio_t priority,
+ const uint_least8_t source_node_id,
+ const uint_least8_t transfer_id,
+ const canard_payload_t payload)
+{
+ (void)transfer_id;
+ subject_reader_t* const reader = (subject_reader_t*)self->user_context;
+ cy_can_t* const owner = reader->owner;
+ const bool multiframe = (payload.origin.data != NULL);
+
+ can_message_t* const msg = make_message(owner, multiframe ? 0 : payload.view.size);
+ if (msg == NULL) {
+ if (multiframe) {
+ owner->vtable->realloc(owner->user, payload.origin.data, 0);
+ }
+ owner->oom_count++;
+ return;
+ }
+ if (multiframe) {
+ msg->seg1 = payload.view.data;
+ msg->seg1_len = payload.view.size;
+ msg->origin = payload.origin.data;
+ } else {
+ (void)memcpy(msg->seg0, payload.view.data, payload.view.size);
+ msg->seg0_len = payload.view.size;
+ }
+ const uint32_t sid = reader->base.subject_id;
+ deliver(owner, &sid, source_node_id, priority, timestamp, msg);
+}
+
+/// 13-bit subscription callback. Must prepend a precomputed phony Cy session header.
+static void v_on_msg_13b(canard_subscription_t* const self,
+ const canard_us_t timestamp,
+ const canard_prio_t priority,
+ const uint_least8_t source_node_id,
+ const uint_least8_t transfer_id,
+ const canard_payload_t payload)
+{
+ (void)transfer_id;
+ subject_reader_t* const reader = (subject_reader_t*)self->user_context;
+ assert(reader != NULL);
+ cy_can_t* const owner = reader->owner;
+ subject_reader_pinned_t* const pinned = as_pinned(reader);
+ assert((pinned != NULL) && (owner != NULL));
+ const bool multiframe = (payload.origin.data != NULL);
+
+ const size_t inline_size = HEADER_BYTES + (multiframe ? 0 : payload.view.size);
+ can_message_t* const msg = make_message(owner, inline_size);
+ if (msg == NULL) {
+ if (multiframe) {
+ owner->vtable->realloc(owner->user, payload.origin.data, 0);
+ }
+ owner->oom_count++;
+ return;
+ }
+ // Stamp precomputed phony header with incrementing tag.
+ pinned->phony_tag++;
+ (void)memcpy(msg->seg0, pinned->phony_header, HEADER_BYTES);
+ le_serialize_u64(msg->seg0 + 16, pinned->phony_tag);
+ msg->seg0_len = HEADER_BYTES;
+
+ if (multiframe) {
+ msg->seg1 = payload.view.data;
+ msg->seg1_len = payload.view.size;
+ msg->origin = payload.origin.data;
+ } else {
+ if (payload.view.size > 0) {
+ (void)memcpy(msg->seg0 + HEADER_BYTES, payload.view.data, payload.view.size);
+ }
+ msg->seg0_len += payload.view.size;
+ }
+ deliver(owner, &reader->base.subject_id, source_node_id, priority, timestamp, msg);
+}
+
+/// Service-ID 511 subscription callback for unicast transfers.
+static void v_on_msg_unicast(canard_subscription_t* const self,
+ const canard_us_t timestamp,
+ const canard_prio_t priority,
+ const uint_least8_t source_node_id,
+ const uint_least8_t transfer_id,
+ const canard_payload_t payload)
+{
+ (void)transfer_id;
+ cy_can_t* const owner = (cy_can_t*)self->user_context;
+ const bool multiframe = (payload.origin.data != NULL);
+
+ can_message_t* const msg = make_message(owner, multiframe ? 0 : payload.view.size);
+ if (msg == NULL) {
+ if (multiframe) {
+ owner->vtable->realloc(owner->user, payload.origin.data, 0);
+ }
+ owner->oom_count++;
+ return;
+ }
+ if (multiframe) {
+ msg->seg1 = payload.view.data;
+ msg->seg1_len = payload.view.size;
+ msg->origin = payload.origin.data;
+ } else {
+ (void)memcpy(msg->seg0, payload.view.data, payload.view.size);
+ msg->seg0_len = payload.view.size;
+ }
+ deliver(owner, NULL, source_node_id, priority, timestamp, msg);
+}
+
+static const canard_subscription_vtable_t sub_vtable_16b = { .on_message = v_on_msg_16b };
+static const canard_subscription_vtable_t sub_vtable_13b = { .on_message = v_on_msg_13b };
+static const canard_subscription_vtable_t sub_vtable_unicast = { .on_message = v_on_msg_unicast };
+
+// =====================================================================================================================
+// SUBJECT WRITER
+
+static cy_subject_writer_t* v_subject_writer_new(cy_platform_t* const base, const uint32_t subject_id)
+{
+ cy_can_t* const owner = (cy_can_t*)base;
+ subject_writer_t* const self = (subject_writer_t*)owner->vtable->realloc(owner->user, NULL, sizeof(*self));
+ if (self != NULL) {
+ (void)memset(self, 0, sizeof(*self));
+ self->base.subject_id = subject_id;
+ }
+ CY_TRACE(owner->base.cy, "CAN writer S%08jx", (uintmax_t)subject_id);
+ return (cy_subject_writer_t*)self;
+}
+
+static void v_subject_writer_destroy(cy_platform_t* const platform, cy_subject_writer_t* const base)
+{
+ cy_can_t* const owner = (cy_can_t*)platform;
+ subject_writer_t* const self = (subject_writer_t*)base;
+ owner->vtable->realloc(owner->user, self, 0);
+}
+
+static cy_err_t v_subject_writer_send(cy_platform_t* const platform,
+ cy_subject_writer_t* const base,
+ const cy_us_t deadline,
+ const cy_prio_t priority,
+ const cy_bytes_t message)
+{
+ cy_can_t* const owner = (cy_can_t*)platform;
+ subject_writer_t* const self = (subject_writer_t*)base;
+ const uint32_t sid = base->subject_id;
+ const uint_least8_t ibm = (uint_least8_t)((1U << owner->iface_count) - 1U);
+
+ assert((message.data != NULL) && (message.size >= HEADER_BYTES));
+ const bool pinned = (sid <= CY_SUBJECT_ID_PINNED_MAX);
+ const bool best_effort = (((const uint_least8_t*)message.data)[0] == 0); // header_msg_be
+ const uint64_t e_oom = owner->canard.err.oom;
+ const uint64_t e_cap = owner->canard.err.tx_capacity;
+
+ bool ok = false;
+ if (pinned && best_effort) { // V1.0 PATH: 13-bit subject-ID, strip the 24-byte Cy header.
+ cy_bytes_t stripped = message;
+ stripped.data = (const char*)stripped.data + HEADER_BYTES;
+ stripped.size -= HEADER_BYTES;
+ if ((stripped.size == 0) && (stripped.next != NULL)) {
+ stripped = *stripped.next;
+ }
+ ok = canard_publish_13b(&owner->canard,
+ deadline,
+ ibm,
+ (canard_prio_t)priority,
+ (uint16_t)sid,
+ self->next_tid_13b++,
+ cy_bytes_to_canard(stripped),
+ NULL);
+ } else { // V1.1 PATH: 16-bit subject-ID, full message including Cy header.
+ ok = canard_publish_16b(&owner->canard,
+ deadline,
+ ibm,
+ (canard_prio_t)priority,
+ (uint16_t)sid,
+ self->next_tid_16b++,
+ cy_bytes_to_canard(message),
+ NULL);
+ }
+ if (ok) {
+ return CY_OK;
+ }
+ if (owner->canard.err.oom > e_oom) {
+ return CY_ERR_MEMORY;
+ }
+ if (owner->canard.err.tx_capacity > e_cap) {
+ return CY_ERR_CAPACITY;
+ }
+ return CY_ERR_ARGUMENT;
+}
+
+// =====================================================================================================================
+// SUBJECT READER
+
+static void build_phony_header(subject_reader_pinned_t* const self, const uint32_t subject_id)
+{
+ (void)memset(self->phony_header, 0, HEADER_BYTES);
+ self->phony_header[3] = 0xFFU; // lage = -1 (int8_t)
+ le_serialize_u32(&self->phony_header[4], (uint32_t)(UINT32_MAX - subject_id));
+ char decimal[11];
+ const size_t dlen = uint_to_decimal(subject_id, decimal);
+ le_serialize_u64(&self->phony_header[8], rapidhash(decimal, dlen));
+}
+
+static void reader_set_extent(subject_reader_t* const self, const size_t extent)
+{
+ self->base.extent = extent;
+ self->sub_16b.extent = extent;
+ subject_reader_pinned_t* const pinned = as_pinned(self);
+ if (pinned != NULL) {
+ pinned->sub_13b.extent = (extent > HEADER_BYTES) ? (extent - HEADER_BYTES) : 0;
+ }
+}
+
+static void tombstone_remove(cy_can_t* const owner, subject_reader_t* const self)
+{
+ assert((owner != NULL) && (self != NULL));
+ if (self->prev_tombstone != NULL) {
+ self->prev_tombstone->next_tombstone = self->next_tombstone;
+ } else {
+ owner->tombstone_head = self->next_tombstone;
+ }
+ if (self->next_tombstone != NULL) {
+ self->next_tombstone->prev_tombstone = self->prev_tombstone;
+ } else {
+ owner->tombstone_tail = self->prev_tombstone;
+ }
+ self->prev_tombstone = NULL;
+ self->next_tombstone = NULL;
+}
+
+static void tombstone_enqueue(cy_can_t* const owner, subject_reader_t* const self)
+{
+ assert((owner != NULL) && (self != NULL));
+ self->prev_tombstone = owner->tombstone_tail;
+ self->next_tombstone = NULL;
+ if (owner->tombstone_tail != NULL) {
+ owner->tombstone_tail->next_tombstone = self;
+ } else {
+ owner->tombstone_head = self;
+ }
+ owner->tombstone_tail = self;
+}
+
+static subject_reader_t* tombstone_pop(cy_can_t* const owner)
+{
+ subject_reader_t* const out = owner->tombstone_head;
+ if (out != NULL) {
+ tombstone_remove(owner, out);
+ }
+ return out;
+}
+
+static subject_reader_t* reader_try_revive(cy_can_t* const owner, const uint32_t subject_id, const size_t extent)
+{
+ canard_subscription_t* const incumbent =
+ canard_find_subscription(&owner->canard, canard_kind_message_16b, (uint16_t)subject_id);
+ if (incumbent == NULL) {
+ return NULL;
+ }
+ subject_reader_t* const self = (subject_reader_t*)incumbent->user_context;
+ assert((self != NULL) && (self->owner == owner) && (self->base.subject_id == subject_id));
+ if (as_pinned(self) != NULL) {
+ canard_subscription_t* const incumbent_13b =
+ canard_find_subscription(&owner->canard, canard_kind_message_13b, (uint16_t)subject_id);
+ assert((incumbent_13b != NULL) && (((subject_reader_t*)incumbent_13b->user_context) == self));
+ if ((incumbent_13b == NULL) || (((subject_reader_t*)incumbent_13b->user_context) != self)) {
+ return NULL;
+ }
+ }
+ tombstone_remove(owner, self);
+ if (extent > self->base.extent) {
+ reader_set_extent(self, extent);
+ }
+ return self;
+}
+
+/// Finalize a reader: unsubscribe from canard and free memory. Does NOT unlink from any list.
+static void reader_finalize(cy_can_t* const owner, subject_reader_t* const self)
+{
+ assert((owner != NULL) && (self != NULL));
+ canard_unsubscribe(&owner->canard, &self->sub_16b);
+ subject_reader_pinned_t* const pinned = as_pinned(self);
+ if (pinned != NULL) {
+ canard_unsubscribe(&owner->canard, &pinned->sub_13b);
+ }
+ owner->vtable->realloc(owner->user, self, 0);
+}
+
+static cy_subject_reader_t* v_subject_reader_new(cy_platform_t* const base,
+ const uint32_t subject_id,
+ const size_t extent)
+{
+ cy_can_t* const owner = (cy_can_t*)base;
+ const bool pinned = (subject_id <= CY_SUBJECT_ID_PINNED_MAX);
+ const size_t sz = pinned ? sizeof(subject_reader_pinned_t) : sizeof(subject_reader_t);
+ subject_reader_t* const revived = reader_try_revive(owner, subject_id, extent);
+ if (revived != NULL) {
+ CY_TRACE(owner->base.cy, "CAN revive S%08jx extent=%zu", (uintmax_t)subject_id, revived->base.extent);
+ return (cy_subject_reader_t*)revived;
+ }
+
+ subject_reader_t* const self = (subject_reader_t*)owner->vtable->realloc(owner->user, NULL, sz);
+ if (self == NULL) {
+ return NULL;
+ }
+ (void)memset(self, 0, sz);
+ self->base.subject_id = subject_id;
+ self->base.extent = extent;
+ self->owner = owner;
+
+ // The extent from Cy already includes the header overhead.
+ canard_subscription_t* const sub_16b = canard_subscribe_16b(&owner->canard,
+ &self->sub_16b,
+ (uint16_t)subject_id,
+ extent,
+ CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_us,
+ &sub_vtable_16b);
+ assert(sub_16b == &self->sub_16b);
+ if (sub_16b != &self->sub_16b) {
+ owner->vtable->realloc(owner->user, self, 0);
+ return NULL;
+ }
+ self->sub_16b.user_context = self;
+
+ if (pinned) {
+ subject_reader_pinned_t* const p = (subject_reader_pinned_t*)self;
+ // 13-bit payload does not include the Cy header; we prepend it ourselves.
+ const size_t extent_13b = (extent > HEADER_BYTES) ? (extent - HEADER_BYTES) : 0;
+ assert(canard_find_subscription(&owner->canard, canard_kind_message_13b, (uint16_t)subject_id) == NULL);
+ canard_subscription_t* const sub_13b = canard_subscribe_13b(&owner->canard,
+ &p->sub_13b,
+ (uint16_t)subject_id,
+ extent_13b,
+ CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_us,
+ &sub_vtable_13b);
+ assert(sub_13b == &p->sub_13b);
+ if (sub_13b != &p->sub_13b) {
+ canard_unsubscribe(&owner->canard, &self->sub_16b);
+ owner->vtable->realloc(owner->user, self, 0);
+ return NULL;
+ }
+ p->sub_13b.user_context = self;
+ build_phony_header(p, subject_id);
+ }
+
+ CY_TRACE(owner->base.cy, "CAN reader S%08jx extent=%zu pinned=%d", (uintmax_t)subject_id, extent, (int)pinned);
+ return (cy_subject_reader_t*)self;
+}
+
+/// Tombstone a reader: unlink from main list, add to tombstone list for deferred finalization.
+static void v_subject_reader_destroy(cy_platform_t* const platform, cy_subject_reader_t* const base)
+{
+ cy_can_t* const owner = (cy_can_t*)platform;
+ subject_reader_t* const self = (subject_reader_t*)base;
+ tombstone_enqueue(owner, self);
+}
+
+static void v_subject_reader_extent_set(cy_platform_t* const base,
+ cy_subject_reader_t* const reader_base,
+ const size_t extent)
+{
+ (void)base;
+ subject_reader_t* const self = (subject_reader_t*)reader_base;
+ reader_set_extent(self, extent);
+}
+
+// =====================================================================================================================
+// UNICAST
+
+static cy_err_t v_unicast_send(cy_platform_t* const base,
+ const cy_lane_t* const lane,
+ const cy_us_t deadline,
+ const cy_bytes_t message)
+{
+ cy_can_t* const owner = (cy_can_t*)base;
+ const uint_least8_t remote = lane->ctx.state[0];
+ assert(remote <= CANARD_NODE_ID_MAX);
+ const uint64_t e_oom = owner->canard.err.oom;
+ const uint64_t e_cap = owner->canard.err.tx_capacity;
+ const uint_least8_t tid = owner->unicast_tid[remote];
+ owner->unicast_tid[remote] = (uint_least8_t)((tid + 1U) % CANARD_TRANSFER_ID_MODULO);
+ const bool ok = canard_request(&owner->canard,
+ deadline,
+ (canard_prio_t)lane->prio,
+ UNICAST_SERVICE_ID,
+ remote,
+ tid,
+ cy_bytes_to_canard(message),
+ NULL);
+ if (ok) {
+ return CY_OK;
+ }
+ if (owner->canard.err.oom > e_oom) {
+ return CY_ERR_MEMORY;
+ }
+ if (owner->canard.err.tx_capacity > e_cap) {
+ return CY_ERR_CAPACITY;
+ }
+ return CY_ERR_ARGUMENT;
+}
+
+static void v_unicast_extent_set(cy_platform_t* const base, const size_t extent)
+{
+ cy_can_t* const owner = (cy_can_t*)base;
+ if (extent > owner->unicast_sub.extent) {
+ owner->unicast_sub.extent = extent;
+ }
+}
+
+// =====================================================================================================================
+// EVENT LOOP
+
+static cy_err_t v_spin(cy_platform_t* const base, const cy_us_t deadline)
+{
+ cy_can_t* const owner = (cy_can_t*)base;
+ const uint_least8_t ibm = (uint_least8_t)((1U << owner->iface_count) - 1U);
+ while (true) {
+ canard_poll(&owner->canard, ibm);
+ {
+ subject_reader_t* const rd = tombstone_pop(owner);
+ if (rd != NULL) {
+ reader_finalize(owner, rd);
+ }
+ }
+ const uint_least8_t tx_pending = canard_pending_ifaces(&owner->canard);
+ cy_can_rx_t frame = { 0 };
+ if (owner->vtable->rx(owner->user, &frame, deadline, tx_pending)) {
+ const canard_bytes_t can_data = { .size = frame.len, .data = frame.data };
+ (void)canard_ingest_frame(&owner->canard, frame.timestamp, frame.iface_index, frame.can_id, can_data);
+ if (frame.timestamp > deadline) {
+ break;
+ }
+ } else {
+ if (owner->vtable->now(owner->user) > deadline) {
+ break;
+ }
+ }
+ }
+ return CY_OK;
+}
+
+// =====================================================================================================================
+// MISC
+
+static cy_us_t v_now(cy_platform_t* const base)
+{
+ const cy_can_t* const owner = (const cy_can_t*)base;
+ return owner->vtable->now(owner->user);
+}
+
+static void* v_realloc(cy_platform_t* const base, void* const ptr, const size_t new_size)
+{
+ const cy_can_t* const owner = (const cy_can_t*)base;
+ return owner->vtable->realloc(owner->user, ptr, new_size);
+}
+
+static uint64_t v_random(cy_platform_t* const base)
+{
+ cy_can_t* const owner = (cy_can_t*)base;
+ return owner->vtable->random(owner->user);
+}
+
+static const cy_platform_vtable_t platform_vtable = { .subject_writer_new = v_subject_writer_new,
+ .subject_writer_destroy = v_subject_writer_destroy,
+ .subject_writer_send = v_subject_writer_send,
+ .subject_reader_new = v_subject_reader_new,
+ .subject_reader_destroy = v_subject_reader_destroy,
+ .subject_reader_extent_set = v_subject_reader_extent_set,
+ .unicast = v_unicast_send,
+ .unicast_extent_set = v_unicast_extent_set,
+ .spin = v_spin,
+ .now = v_now,
+ .realloc = v_realloc,
+ .random = v_random };
+
+// =====================================================================================================================
+// PUBLIC API
+
+cy_platform_t* cy_can_new(const uint_least8_t iface_count,
+ const size_t tx_queue_capacity,
+ const size_t filter_count,
+ const cy_can_vtable_t* const vtable,
+ void* const user)
+{
+ if ((vtable == NULL) || (vtable->tx_classic == NULL) || (vtable->rx == NULL) || (vtable->now == NULL) ||
+ (vtable->realloc == NULL) || (vtable->random == NULL) || (iface_count == 0) ||
+ (iface_count > CANARD_IFACE_COUNT)) {
+ return NULL;
+ }
+ cy_can_t* const self = (cy_can_t*)vtable->realloc(user, NULL, sizeof(cy_can_t));
+ if (self == NULL) {
+ return NULL;
+ }
+ (void)memset(self, 0, sizeof(*self));
+ self->vtable = vtable;
+ self->user = user;
+ self->iface_count = iface_count;
+
+ self->base.subject_id_modulus = CY_SUBJECT_ID_MODULUS_16bit;
+ self->base.vtable = &platform_vtable;
+
+ const bool filtering_enabled = (filter_count > 0U) && (vtable->filter != NULL);
+ const canard_vtable_t* const canard_vtable = filtering_enabled ? &canard_vtbl_filter : &canard_vtbl_no_filter;
+ const bool ok = canard_new(&self->canard,
+ canard_vtable,
+ make_mem_set(self),
+ tx_queue_capacity,
+ vtable->random(user),
+ filtering_enabled ? filter_count : 0U);
+ if (!ok) {
+ vtable->realloc(user, self, 0);
+ return NULL;
+ }
+ self->canard.tx.fd = (vtable->tx_fd != NULL);
+ self->canard.user_context = self;
+
+ canard_subscription_t* const sub_uni = canard_subscribe_request(&self->canard,
+ &self->unicast_sub,
+ UNICAST_SERVICE_ID,
+ UNICAST_EXTENT_INITIAL,
+ CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_us,
+ &sub_vtable_unicast);
+ if (sub_uni != &self->unicast_sub) {
+ canard_destroy(&self->canard);
+ vtable->realloc(user, self, 0);
+ return NULL;
+ }
+ self->unicast_sub.user_context = self;
+ return &self->base;
+}
+
+void* cy_can_user(const cy_platform_t* const base) { return ((const cy_can_t*)base)->user; }
+
+void cy_can_destroy(cy_platform_t* const base)
+{
+ cy_can_t* const owner = (cy_can_t*)base;
+ while (owner->tombstone_head != NULL) {
+ subject_reader_t* const rd = tombstone_pop(owner);
+ assert(rd != NULL);
+ reader_finalize(owner, rd);
+ }
+ canard_unsubscribe(&owner->canard, &owner->unicast_sub);
+ canard_destroy(&owner->canard);
+ owner->vtable->realloc(owner->user, owner, 0);
+}
diff --git a/cy_can/cy_can.h b/cy_can/cy_can.h
new file mode 100644
index 0000000..9b62aa8
--- /dev/null
+++ b/cy_can/cy_can.h
@@ -0,0 +1,94 @@
+// ____ ______ __ __
+// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / /
+// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ /
+// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / /
+// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/
+// /_/ /____/_/
+//
+// A Cy platform layer for Cyphal/CAN with v1.0 interoperability.
+// This module is fully platform-agnostic; platform I/O is delegated to the cy_can_vtable_t provided by the user.
+//
+// See cy_can_socketcan.h for a Linux SocketCAN implementation of the vtable.
+// Baremetal/RTOS-based platforms are expected to provide their own vtable implementations, perhaps using the GNU/Linux
+// implementation as a reference.
+//
+// Copyright (c) Pavel Kirienko
+
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+/// A received CAN frame (classic or FD).
+typedef struct
+{
+ cy_us_t timestamp; ///< Monotonic reception timestamp in microseconds, in cy_can_vtable_t::now() timebase.
+ uint32_t can_id; ///< 29-bit extended CAN ID.
+ uint_least8_t iface_index; ///< The redundant interface the frame was received from.
+ uint_least8_t len; ///< Data length: 0-8 for classic CAN, 0-64 for CAN FD.
+ bool fd; ///< True if this is a CAN FD frame.
+ uint_least8_t data[64];
+} cy_can_rx_t;
+
+/// Platform-specific CAN driver abstraction. A single vtable manages ALL redundant interfaces.
+/// All functions are non-blocking except rx(), which may block up to the specified deadline.
+typedef struct
+{
+ /// Transmit a classic CAN frame (up to 8 bytes) on the given interface.
+ /// Returns true if the frame should be removed from the upstream TX queue (i.e., if the frame was accepted
+ /// for transmission or if it encountered a fatal failure and no further attempts are needed).
+ /// Returns false if the underlying CAN controller is not ready to accept a new frame (e.g., no free TX mailbox);
+ /// the caller will retry later.
+ bool (*tx_classic)(void* user, uint_least8_t iface_index, uint32_t can_id, const void* data, uint_least8_t len);
+
+ /// Transmit a CAN FD frame (up to 64 bytes) on the given interface.
+ /// Set to NULL if the underlying driver does not support CAN FD; all interfaces share the same FD capability.
+ bool (*tx_fd)(void* user, uint_least8_t iface_index, uint32_t can_id, const void* data, uint_least8_t len);
+
+ /// Poll all redundant interfaces for a received frame. Returns true if a frame was received.
+ /// The implementation may block up to the given deadline; baremetal implementations may ignore the deadline
+ /// and return immediately if no frame is available.
+ /// The tx_pending_iface_bitmap indicates which interfaces have pending TX frames; the implementation should
+ /// also unblock when any of those interfaces become writable, to allow the caller to retry transmissions.
+ bool (*rx)(void* user, cy_can_rx_t* out_frame, cy_us_t deadline, uint_least8_t tx_pending_iface_bitmap);
+
+ /// Replace the hardware acceptance filter configuration with the supplied filter set.
+ /// This callback is optional; if NULL, libcanard filtering is disabled even if filter_count passed to cy_can_new()
+ /// is nonzero. The callback is only invoked from canard_poll().
+ bool (*filter)(void* user, size_t filter_count, const canard_filter_t* filters);
+
+ /// Returns the current monotonic time in microseconds. Must be non-negative and non-decreasing.
+ cy_us_t (*now)(void* user);
+
+ /// Standard realloc semantics; if size is zero, the call shall behave as free.
+ void* (*realloc)(void* user, void* ptr, size_t size);
+
+ /// Returns a random 64-bit unsigned integer. See cy_platform_vtable_t for the seeding recommendations.
+ uint64_t (*random)(void* user);
+} cy_can_vtable_t;
+
+/// Create a new CAN platform instance. The node-ID will be allocated automatically by libcanard.
+/// The constructor will invoke vtable random() and realloc() immediately.
+/// The filter_count is the number of acceptance filters available to libcanard; pass zero to disable filtering.
+/// Filtering is also disabled if vtable->filter is NULL.
+/// Returns NULL on failure. The iface_count must be in [1, CANARD_IFACE_COUNT].
+cy_platform_t* cy_can_new(const uint_least8_t iface_count,
+ const size_t tx_queue_capacity,
+ const size_t filter_count,
+ const cy_can_vtable_t* const vtable,
+ void* const user);
+
+/// Returns the user context pointer that was passed to cy_can_new().
+void* cy_can_user(const cy_platform_t* const base);
+
+/// Requires the Cy instance to be unlinked first and all Cy-allocated resources freed.
+void cy_can_destroy(cy_platform_t* const base);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/cy_can/cy_can_socketcan.c b/cy_can/cy_can_socketcan.c
new file mode 100644
index 0000000..0dce28f
--- /dev/null
+++ b/cy_can/cy_can_socketcan.c
@@ -0,0 +1,318 @@
+// ____ ______ __ __
+// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / /
+// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ /
+// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / /
+// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/
+// /_/ /____/_/
+//
+// Copyright (c) Pavel Kirienko
+
+// Feature test macros must come before any system headers.
+// NOLINTBEGIN(bugprone-reserved-identifier)
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+// NOLINTEND(bugprone-reserved-identifier)
+
+#include "cy_can_socketcan.h"
+#include
+#include
+
+#define RAPIDHASH_COMPACT
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+typedef struct
+{
+ int sock_fd[CANARD_IFACE_COUNT];
+ uint_least8_t iface_count;
+ bool fd_capable;
+ uint64_t prng_state;
+} socketcan_t;
+
+static const size_t socketcan_filter_count = 511U;
+
+static cy_us_t socketcan_now(void)
+{
+ struct timespec ts;
+ (void)clock_gettime(CLOCK_MONOTONIC, &ts);
+ return ((cy_us_t)ts.tv_sec * 1000000LL) + (cy_us_t)(ts.tv_nsec / 1000L);
+}
+
+// =====================================================================================================================
+// VTABLE IMPLEMENTATION
+
+static bool v_tx_classic(void* const user,
+ const uint_least8_t iface_index,
+ const uint32_t can_id,
+ const void* const data,
+ const uint_least8_t len)
+{
+ const socketcan_t* const self = (const socketcan_t*)user;
+ assert(iface_index < self->iface_count);
+ struct can_frame frame = { .can_id = can_id | CAN_EFF_FLAG, .can_dlc = len };
+ if ((data != NULL) && (len > 0)) {
+ (void)memcpy(frame.data, data, (len <= 8) ? len : 8);
+ }
+ const ssize_t res = write(self->sock_fd[iface_index], &frame, sizeof(frame));
+ return res == (ssize_t)sizeof(frame);
+}
+
+static bool v_tx_fd(void* const user,
+ const uint_least8_t iface_index,
+ const uint32_t can_id,
+ const void* const data,
+ const uint_least8_t len)
+{
+ const socketcan_t* const self = (const socketcan_t*)user;
+ assert(iface_index < self->iface_count);
+ struct canfd_frame frame = { .can_id = can_id | CAN_EFF_FLAG, .len = len, .flags = CANFD_FDF };
+ if ((data != NULL) && (len > 0)) {
+ (void)memcpy(frame.data, data, (len <= 64) ? len : 64);
+ }
+ const ssize_t res = write(self->sock_fd[iface_index], &frame, sizeof(frame));
+ return res == (ssize_t)sizeof(frame);
+}
+
+static bool v_rx(void* const user,
+ cy_can_rx_t* const out_frame,
+ const cy_us_t deadline,
+ const uint_least8_t tx_pending_iface_bitmap)
+{
+ socketcan_t* const self = (socketcan_t*)user;
+ struct pollfd fds[CANARD_IFACE_COUNT];
+ for (uint_least8_t i = 0; i < self->iface_count; i++) {
+ fds[i].fd = self->sock_fd[i];
+ fds[i].events = POLLIN;
+ fds[i].revents = 0;
+ if (((tx_pending_iface_bitmap >> i) & 1U) != 0) { // NOLINT(*-signed-bitwise)
+ fds[i].events |= POLLOUT; // NOLINT(*-signed-bitwise)
+ }
+ }
+ const cy_us_t now = socketcan_now();
+ const int timeout_ms = (deadline > now) ? (int)((deadline - now) / 1000LL) : 0;
+ const int ret = poll(fds, self->iface_count, timeout_ms);
+ if (ret <= 0) {
+ return false;
+ }
+ for (uint_least8_t i = 0; i < self->iface_count; i++) {
+ if ((fds[i].revents & POLLIN) == 0) { // NOLINT(*-signed-bitwise)
+ continue;
+ }
+ out_frame->iface_index = i;
+ out_frame->timestamp = socketcan_now(); // embedded systems would typically sample the hardware clock
+ if (self->fd_capable) {
+ struct canfd_frame frame;
+ const ssize_t n = read(self->sock_fd[i], &frame, sizeof(frame));
+ if (n < (ssize_t)sizeof(struct can_frame)) {
+ continue;
+ }
+ out_frame->can_id = frame.can_id & CAN_EFF_MASK;
+ out_frame->fd = ((size_t)n == sizeof(struct canfd_frame));
+ out_frame->len = out_frame->fd ? frame.len : ((struct can_frame*)&frame)->can_dlc;
+ if (out_frame->len > 64) {
+ out_frame->len = 64;
+ }
+ (void)memcpy(out_frame->data, frame.data, out_frame->len);
+ } else {
+ struct can_frame frame;
+ const ssize_t n = read(self->sock_fd[i], &frame, sizeof(frame));
+ if (n < (ssize_t)sizeof(frame)) {
+ continue;
+ }
+ out_frame->can_id = frame.can_id & CAN_EFF_MASK;
+ out_frame->fd = false;
+ out_frame->len = frame.can_dlc;
+ if (out_frame->len > 8) {
+ out_frame->len = 8;
+ }
+ (void)memcpy(out_frame->data, frame.data, out_frame->len);
+ }
+ return true;
+ }
+ return false;
+}
+
+static bool v_filter(void* const user, const size_t filter_count, const canard_filter_t* const filters)
+{
+ const socketcan_t* const self = (const socketcan_t*)user;
+ const struct can_filter empty = { 0 };
+ struct can_filter* raw = NULL;
+ if ((filter_count > 0U) && (filters == NULL)) {
+ return false;
+ }
+ if (filter_count > 0U) {
+ raw = (struct can_filter*)calloc(filter_count, sizeof(struct can_filter));
+ if (raw == NULL) {
+ return false;
+ }
+ for (size_t i = 0; i < filter_count; i++) {
+ raw[i].can_id = CAN_EFF_FLAG | (filters[i].extended_can_id & CAN_EFF_MASK);
+ raw[i].can_mask = CAN_EFF_FLAG | CAN_RTR_FLAG | (filters[i].extended_mask & CAN_EFF_MASK);
+ }
+ }
+ const void* const optval = (filter_count > 0U) ? (const void*)raw : (const void*)∅
+ bool ok = true;
+ for (uint_least8_t i = 0; i < self->iface_count; i++) {
+ if (setsockopt(self->sock_fd[i],
+ SOL_CAN_RAW,
+ CAN_RAW_FILTER,
+ optval,
+ (socklen_t)(filter_count * sizeof(struct can_filter))) < 0) {
+ ok = false;
+ break;
+ }
+ }
+ free(raw);
+ return ok;
+}
+
+static cy_us_t v_now(void* const user)
+{
+ (void)user;
+ return socketcan_now();
+}
+
+static void* v_realloc(void* const user, void* const ptr, const size_t size)
+{
+ (void)user;
+ if (size > 0) {
+ return realloc(ptr, size);
+ }
+ free(ptr);
+ return NULL;
+}
+
+static uint64_t v_random(void* const user)
+{
+ socketcan_t* const self = (socketcan_t*)user;
+ self->prng_state += 0xA0761D6478BD642FULL;
+ return rapidhash_withSeed(&self->prng_state, sizeof(uint64_t), (uintptr_t)self);
+}
+
+static const cy_can_vtable_t socketcan_vtable_fd = { .tx_classic = v_tx_classic,
+ .tx_fd = v_tx_fd,
+ .rx = v_rx,
+ .filter = v_filter,
+ .now = v_now,
+ .realloc = v_realloc,
+ .random = v_random };
+static const cy_can_vtable_t socketcan_vtable_classic = { .tx_classic = v_tx_classic,
+ .tx_fd = NULL, // No FD support.
+ .rx = v_rx,
+ .filter = v_filter,
+ .now = v_now,
+ .realloc = v_realloc,
+ .random = v_random };
+
+// =====================================================================================================================
+// PUBLIC API
+
+cy_platform_t* cy_can_socketcan_new(const uint_least8_t iface_count,
+ const char* const iface_names[],
+ const size_t tx_queue_capacity)
+{
+ if ((iface_count == 0) || (iface_count > CANARD_IFACE_COUNT) || (iface_names == NULL)) {
+ return NULL;
+ }
+ socketcan_t* const self = (socketcan_t*)calloc(1, sizeof(socketcan_t));
+ if (self == NULL) {
+ return NULL;
+ }
+ self->iface_count = iface_count;
+ self->fd_capable = true; // Assume FD until proven otherwise.
+ for (uint_least8_t i = 0; i < CANARD_IFACE_COUNT; i++) {
+ self->sock_fd[i] = -1;
+ }
+
+ // Seed the PRNG from best available source.
+ {
+ const int fd = open("/dev/urandom", O_RDONLY);
+ const size_t sz = sizeof(self->prng_state);
+ const bool ok = (fd >= 0) && (read(fd, &self->prng_state, sz) == (ssize_t)sz);
+ (void)close(fd);
+ if (!ok) {
+ self->prng_state = ((uint64_t)socketcan_now()) ^ (uint64_t)time(NULL) ^ ((uint64_t)(uintptr_t)self);
+ }
+ }
+
+ for (uint_least8_t i = 0; i < iface_count; i++) {
+ const int sock = socket(PF_CAN, SOCK_RAW, CAN_RAW);
+ if (sock < 0) {
+ goto fail;
+ }
+ self->sock_fd[i] = sock;
+ // Bind to the named interface.
+ struct ifreq ifr;
+ (void)memset(&ifr, 0, sizeof(ifr));
+ (void)strncpy(ifr.ifr_name, iface_names[i], sizeof(ifr.ifr_name) - 1);
+ if (ioctl(sock, SIOCGIFINDEX, &ifr) < 0) {
+ goto fail;
+ }
+ struct sockaddr_can addr;
+ (void)memset(&addr, 0, sizeof(addr));
+ addr.can_family = AF_CAN;
+ addr.can_ifindex = ifr.ifr_ifindex;
+ if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
+ goto fail;
+ }
+ // Try enabling CAN FD. If it fails on any interface, disable FD for all.
+ {
+ const int enable = 1;
+ if (setsockopt(sock, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &enable, sizeof(enable)) < 0) {
+ self->fd_capable = false;
+ }
+ }
+ // Non-blocking mode.
+ {
+ const int flags = fcntl(sock, F_GETFL, 0);
+ if (flags >= 0) {
+ (void)fcntl(sock, F_SETFL, flags | O_NONBLOCK); // NOLINT(*-signed-bitwise)
+ }
+ }
+ }
+
+ const cy_can_vtable_t* const vtable = self->fd_capable ? &socketcan_vtable_fd : &socketcan_vtable_classic;
+ cy_platform_t* const result = cy_can_new(iface_count, tx_queue_capacity, socketcan_filter_count, vtable, self);
+ if (result == NULL) {
+ goto fail;
+ }
+ return result;
+
+fail:
+ for (uint_least8_t i = 0; i < CANARD_IFACE_COUNT; i++) {
+ if (self->sock_fd[i] >= 0) {
+ (void)close(self->sock_fd[i]);
+ }
+ }
+ free(self);
+ return NULL;
+}
+
+void cy_can_socketcan_destroy(cy_platform_t* const base)
+{
+ if (base == NULL) {
+ return;
+ }
+ socketcan_t* const self = (socketcan_t*)cy_can_user(base);
+ cy_can_destroy(base);
+ for (uint_least8_t i = 0; i < CANARD_IFACE_COUNT; i++) {
+ if (self->sock_fd[i] >= 0) {
+ (void)close(self->sock_fd[i]);
+ }
+ }
+ free(self);
+}
diff --git a/cy_can/cy_can_socketcan.h b/cy_can/cy_can_socketcan.h
new file mode 100644
index 0000000..ef60f25
--- /dev/null
+++ b/cy_can/cy_can_socketcan.h
@@ -0,0 +1,33 @@
+// ____ ______ __ __
+// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / /
+// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ /
+// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / /
+// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/
+// /_/ /____/_/
+//
+// A cy_can_vtable_t implementation for GNU/Linux SocketCAN.
+// This module is only usable on GNU/Linux (and potentially some RTOS that implement SocketCAN-like APIs).
+//
+// Copyright (c) Pavel Kirienko
+
+#pragma once
+
+#include "cy_can.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+/// Create a SocketCAN-backed CAN platform instance bound to the given interface names (e.g., "can0", "can1").
+/// All interfaces must share the same CAN FD capability; FD is auto-detected.
+/// Returns NULL on failure (e.g., interface not found, socket error).
+cy_platform_t* cy_can_socketcan_new(const uint_least8_t iface_count,
+ const char* const iface_names[],
+ const size_t tx_queue_capacity);
+
+void cy_can_socketcan_destroy(cy_platform_t* const base);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/cy_can/tests/CMakeLists.txt b/cy_can/tests/CMakeLists.txt
new file mode 100644
index 0000000..557caef
--- /dev/null
+++ b/cy_can/tests/CMakeLists.txt
@@ -0,0 +1,119 @@
+# Copyright (c) Pavel Kirienko
+
+cmake_minimum_required(VERSION 3.24)
+
+set(cy_can_tests_root ${CMAKE_CURRENT_SOURCE_DIR})
+set(unity_root "${CMAKE_SOURCE_DIR}/lib/unity")
+set(canard_root "${CMAKE_SOURCE_DIR}/lib/libcanard/libcanard")
+
+add_library(cy_can_tests_unity STATIC "${unity_root}/src/unity.c")
+target_include_directories(cy_can_tests_unity SYSTEM PUBLIC "${unity_root}/src")
+target_compile_definitions(cy_can_tests_unity PUBLIC UNITY_INCLUDE_DOUBLE=1 UNITY_OUTPUT_COLOR=1 UNITY_SUPPORT_64=1)
+target_compile_options(
+ cy_can_tests_unity
+ PRIVATE
+ -m32
+ -Wno-sign-conversion
+ -Wno-conversion
+ -Wno-switch-enum
+ -Wno-float-equal
+ -Wno-double-promotion
+ -Wno-missing-declarations
+)
+set_target_properties(
+ cy_can_tests_unity
+ PROPERTIES
+ C_STANDARD 99
+ C_STANDARD_REQUIRED ON
+ C_EXTENSIONS OFF
+ COMPILE_WARNING_AS_ERROR OFF
+ C_CLANG_TIDY ""
+ CXX_CLANG_TIDY ""
+ C_CPPCHECK ""
+ CXX_CPPCHECK ""
+)
+
+add_library(cy_can_tests_canard STATIC "${canard_root}/canard.c")
+target_include_directories(cy_can_tests_canard SYSTEM PUBLIC "${canard_root}")
+target_compile_options(cy_can_tests_canard PRIVATE -m32 -Wno-cast-align)
+set_target_properties(
+ cy_can_tests_canard
+ PROPERTIES
+ C_STANDARD 99
+ C_STANDARD_REQUIRED ON
+ C_EXTENSIONS OFF
+ COMPILE_WARNING_AS_ERROR OFF
+ C_CLANG_TIDY ""
+ CXX_CLANG_TIDY ""
+ C_CPPCHECK ""
+ CXX_CPPCHECK ""
+)
+
+function(cy_can_add_test name)
+ add_executable(
+ ${name}
+ ${ARGN}
+ ${CMAKE_SOURCE_DIR}/cy/cy.c
+ ${CMAKE_SOURCE_DIR}/cy_can/cy_can.c
+ ${cy_can_tests_root}/test_support.c
+ )
+ target_include_directories(
+ ${name}
+ PRIVATE
+ ${CMAKE_SOURCE_DIR}/cy
+ ${CMAKE_SOURCE_DIR}/cy_can
+ ${canard_root}
+ ${cy_can_tests_root}
+ )
+ target_link_libraries(${name} PRIVATE cy_can_tests_unity cy_can_tests_canard)
+ target_compile_options(${name} PRIVATE -m32)
+ target_link_options(${name} PRIVATE -m32)
+ set_target_properties(
+ ${name}
+ PROPERTIES
+ C_STANDARD 99
+ C_STANDARD_REQUIRED ON
+ C_EXTENSIONS OFF
+ COMPILE_WARNING_AS_ERROR ON
+ )
+ add_test(NAME run_${name} COMMAND ${name})
+endfunction()
+
+cy_can_add_test(test_api_can_constructor test_api_can_constructor.c)
+cy_can_add_test(test_api_can_pubsub test_api_can_pubsub.c)
+cy_can_add_test(test_api_can_reliable_rpc test_api_can_reliable_rpc.c)
+cy_can_add_test(test_api_can_redundancy_lifecycle test_api_can_redundancy_lifecycle.c)
+cy_can_add_test(test_api_can_tombstone_revival test_api_can_tombstone_revival.c)
+cy_can_add_test(test_api_can_failures test_api_can_failures.c)
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
+ add_executable(
+ test_api_can_socketcan_e2e
+ test_api_can_socketcan_e2e.c
+ ${CMAKE_SOURCE_DIR}/cy/cy.c
+ ${CMAKE_SOURCE_DIR}/cy_can/cy_can.c
+ ${CMAKE_SOURCE_DIR}/cy_can/cy_can_socketcan.c
+ )
+ target_include_directories(
+ test_api_can_socketcan_e2e
+ PRIVATE
+ ${CMAKE_SOURCE_DIR}/cy
+ ${CMAKE_SOURCE_DIR}/cy_can
+ ${canard_root}
+ ${cy_can_tests_root}
+ )
+ target_link_libraries(test_api_can_socketcan_e2e PRIVATE cy_can_tests_unity cy_can_tests_canard)
+ target_compile_definitions(test_api_can_socketcan_e2e PRIVATE _POSIX_C_SOURCE=200809L)
+ target_compile_options(test_api_can_socketcan_e2e PRIVATE -m32)
+ target_link_options(test_api_can_socketcan_e2e PRIVATE -m32)
+ set_target_properties(
+ test_api_can_socketcan_e2e
+ PROPERTIES
+ C_STANDARD 99
+ C_STANDARD_REQUIRED ON
+ C_EXTENSIONS OFF
+ COMPILE_WARNING_AS_ERROR ON
+ )
+ add_test(NAME run_test_api_can_socketcan_e2e COMMAND test_api_can_socketcan_e2e)
+ set_tests_properties(run_test_api_can_socketcan_e2e PROPERTIES SKIP_RETURN_CODE 77)
+endif ()
diff --git a/cy_can/tests/test_api_can_constructor.c b/cy_can/tests/test_api_can_constructor.c
new file mode 100644
index 0000000..3055e65
--- /dev/null
+++ b/cy_can/tests/test_api_can_constructor.c
@@ -0,0 +1,161 @@
+#include "test_support.h"
+#include
+#include
+#include
+#include
+#include
+
+static const cy_us_t spin_slice_us = (cy_us_t)10000;
+
+static void test_api_can_constructor_rejects_invalid_arguments(void)
+{
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, true);
+
+ TEST_ASSERT_NULL(cy_can_new(0U, 16U, 0U, &node.vtable, &node));
+ TEST_ASSERT_NULL(cy_can_new(CANARD_IFACE_COUNT + 1U, 16U, 0U, &node.vtable, &node));
+ TEST_ASSERT_NULL(cy_can_new(1U, 16U, 0U, NULL, &node));
+
+ {
+ cy_can_vtable_t v = node.vtable;
+ v.tx_classic = NULL;
+ TEST_ASSERT_NULL(cy_can_new(1U, 16U, 0U, &v, &node));
+ }
+ {
+ cy_can_vtable_t v = node.vtable;
+ v.rx = NULL;
+ TEST_ASSERT_NULL(cy_can_new(1U, 16U, 0U, &v, &node));
+ }
+ {
+ cy_can_vtable_t v = node.vtable;
+ v.now = NULL;
+ TEST_ASSERT_NULL(cy_can_new(1U, 16U, 0U, &v, &node));
+ }
+ {
+ cy_can_vtable_t v = node.vtable;
+ v.realloc = NULL;
+ TEST_ASSERT_NULL(cy_can_new(1U, 16U, 0U, &v, &node));
+ }
+ {
+ cy_can_vtable_t v = node.vtable;
+ v.random = NULL;
+ TEST_ASSERT_NULL(cy_can_new(1U, 16U, 0U, &v, &node));
+ }
+
+ can_test_node_destroy(&node);
+}
+
+static void test_api_can_constructor_user_roundtrip_and_destroy(void)
+{
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, true);
+ can_test_node_make_platform(&node, 32U, 4U);
+ TEST_ASSERT_TRUE(cy_can_user(node.platform) == &node);
+ can_test_node_make_cy(&node, "ctor_roundtrip");
+ can_test_node_destroy(&node);
+}
+
+static void test_api_can_constructor_filtering_disabled_by_zero_count(void)
+{
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, true);
+ can_test_node_make_platform(&node, 32U, 0U);
+ can_test_node_make_cy(&node, "filter_zero");
+
+ cy_future_t* const sub = cy_subscribe(node.cy, cy_str("111#111"), 64U);
+ TEST_ASSERT_NOT_NULL(sub);
+ can_test_node_spin(&node, spin_slice_us);
+ can_test_node_spin(&node, spin_slice_us);
+ TEST_ASSERT_EQUAL_size_t(0U, node.filter_calls);
+
+ cy_future_destroy(sub);
+ can_test_node_spin(&node, spin_slice_us);
+ can_test_node_destroy(&node);
+}
+
+static void test_api_can_constructor_filtering_disabled_by_null_callback(void)
+{
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, false);
+ can_test_node_make_platform(&node, 32U, 4U);
+ can_test_node_make_cy(&node, "filter_null");
+
+ cy_future_t* const sub = cy_subscribe(node.cy, cy_str("112#112"), 64U);
+ TEST_ASSERT_NOT_NULL(sub);
+ can_test_node_spin(&node, spin_slice_us);
+ can_test_node_spin(&node, spin_slice_us);
+ TEST_ASSERT_EQUAL_size_t(0U, node.filter_calls);
+
+ cy_future_destroy(sub);
+ can_test_node_spin(&node, spin_slice_us);
+ can_test_node_destroy(&node);
+}
+
+static void test_api_can_constructor_filtering_enabled_when_callback_and_count_present(void)
+{
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, true);
+ can_test_node_make_platform(&node, 32U, 8U);
+ can_test_node_make_cy(&node, "filter_enabled");
+
+ cy_future_t* const sub = cy_subscribe(node.cy, cy_str("113#113"), 64U);
+ TEST_ASSERT_NOT_NULL(sub);
+ can_test_node_spin(&node, spin_slice_us);
+ TEST_ASSERT_TRUE(node.filter_calls > 0U);
+ TEST_ASSERT_TRUE(node.last_filter_count > 0U);
+
+ cy_future_destroy(sub);
+ can_test_node_spin(&node, spin_slice_us);
+ can_test_node_destroy(&node);
+}
+
+static void test_api_can_constructor_filter_retries_after_failure(void)
+{
+ can_test_bus_t bus;
+ can_test_node_t node;
+ size_t first_filter_call_count = 0U;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, true);
+ can_test_node_make_platform(&node, 32U, 8U);
+ can_test_node_make_cy(&node, "filter_retry");
+
+ cy_future_t* const sub = cy_subscribe(node.cy, cy_str("114#114"), 64U);
+ TEST_ASSERT_NOT_NULL(sub);
+ node.filter_failures_remaining = 1U;
+
+ can_test_node_spin(&node, spin_slice_us);
+ first_filter_call_count = node.filter_calls;
+ TEST_ASSERT_TRUE(first_filter_call_count > 0U);
+ TEST_ASSERT_EQUAL_size_t(0U, node.filter_failures_remaining);
+ TEST_ASSERT_TRUE(first_filter_call_count >= 2U);
+
+ cy_future_destroy(sub);
+ can_test_node_spin(&node, spin_slice_us);
+ can_test_node_destroy(&node);
+}
+
+void setUp(void) {}
+
+void tearDown(void) {}
+
+int main(void)
+{
+ UNITY_BEGIN();
+ RUN_TEST(test_api_can_constructor_rejects_invalid_arguments);
+ RUN_TEST(test_api_can_constructor_user_roundtrip_and_destroy);
+ RUN_TEST(test_api_can_constructor_filtering_disabled_by_zero_count);
+ RUN_TEST(test_api_can_constructor_filtering_disabled_by_null_callback);
+ RUN_TEST(test_api_can_constructor_filtering_enabled_when_callback_and_count_present);
+ RUN_TEST(test_api_can_constructor_filter_retries_after_failure);
+ return UNITY_END();
+}
diff --git a/cy_can/tests/test_api_can_failures.c b/cy_can/tests/test_api_can_failures.c
new file mode 100644
index 0000000..024e906
--- /dev/null
+++ b/cy_can/tests/test_api_can_failures.c
@@ -0,0 +1,115 @@
+#include "test_support.h"
+#include
+#include
+#include
+#include
+
+static const cy_us_t spin_slice_us = (cy_us_t)10000;
+
+static void test_api_can_failures_constructor_oom(void)
+{
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, true);
+ can_test_heap_fail_after(&node.heap, 0U);
+ TEST_ASSERT_NULL(cy_can_new(1U, 32U, 4U, &node.vtable, &node));
+ can_test_heap_allow_all(&node.heap);
+ can_test_node_destroy(&node);
+}
+
+static void test_api_can_failures_subscribe_oom(void)
+{
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, false);
+ can_test_node_make_platform(&node, 64U, 0U);
+ can_test_node_make_cy(&node, "fail_subscribe");
+
+ can_test_heap_fail_after(&node.heap, 0U);
+ TEST_ASSERT_NULL(cy_subscribe(node.cy, cy_str("400#400"), 64U));
+ can_test_heap_allow_all(&node.heap);
+ can_test_node_spin(&node, spin_slice_us);
+ can_test_node_destroy(&node);
+}
+
+static void test_api_can_failures_receive_wrapper_oom_drops_message_without_leak(void)
+{
+ const uint8_t payload[] = { 0x71U, 0x72U, 0x73U, 0x74U };
+
+ can_test_bus_t bus;
+ can_test_node_t a;
+ can_test_node_t b;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&a, &bus, 1U, false, false);
+ can_test_node_prepare(&b, &bus, 1U, false, false);
+ can_test_node_make_platform(&a, 128U, 0U);
+ can_test_node_make_platform(&b, 128U, 0U);
+ can_test_node_make_cy(&a, "fail_rx_a");
+ can_test_node_make_cy(&b, "fail_rx_b");
+
+ cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("401#401"));
+ cy_future_t* const sub = cy_subscribe(b.cy, cy_str("401#401"), 64U);
+ TEST_ASSERT_NOT_NULL(pub);
+ TEST_ASSERT_NOT_NULL(sub);
+ can_test_spin_pair(&a, &b, 4U, spin_slice_us);
+
+ can_test_heap_fail_after(&b.heap, 0U);
+ TEST_ASSERT_EQUAL_INT(
+ CY_OK, cy_publish(pub, cy_now(a.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL }));
+ can_test_spin_pair(&a, &b, 12U, spin_slice_us);
+ TEST_ASSERT_FALSE(cy_future_done(sub));
+
+ can_test_heap_allow_all(&b.heap);
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ can_test_spin_pair(&a, &b, 4U, spin_slice_us);
+ can_test_node_destroy(&a);
+ can_test_node_destroy(&b);
+}
+
+static void test_api_can_failures_tx_queue_capacity_is_reported(void)
+{
+ uint8_t payload[48];
+ bool saw_capacity = false;
+
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, false);
+ can_test_node_make_platform(&node, 1U, 0U);
+ can_test_node_make_cy(&node, "fail_capacity");
+ can_test_fill_payload(payload, sizeof(payload), 0x80U);
+
+ cy_publisher_t* const pub = cy_advertise(node.cy, cy_str("402#402"));
+ TEST_ASSERT_NOT_NULL(pub);
+
+ for (size_t i = 0U; i < 8U; i++) {
+ const cy_err_t err =
+ cy_publish(pub, cy_now(node.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL });
+ if (err == CY_ERR_CAPACITY) {
+ saw_capacity = true;
+ break;
+ }
+ }
+ TEST_ASSERT_TRUE(saw_capacity);
+
+ cy_unadvertise(pub);
+ can_test_node_spin(&node, spin_slice_us);
+ can_test_node_destroy(&node);
+}
+
+void setUp(void) {}
+
+void tearDown(void) {}
+
+int main(void)
+{
+ UNITY_BEGIN();
+ RUN_TEST(test_api_can_failures_constructor_oom);
+ RUN_TEST(test_api_can_failures_subscribe_oom);
+ RUN_TEST(test_api_can_failures_receive_wrapper_oom_drops_message_without_leak);
+ RUN_TEST(test_api_can_failures_tx_queue_capacity_is_reported);
+ return UNITY_END();
+}
diff --git a/cy_can/tests/test_api_can_pubsub.c b/cy_can/tests/test_api_can_pubsub.c
new file mode 100644
index 0000000..687aee9
--- /dev/null
+++ b/cy_can/tests/test_api_can_pubsub.c
@@ -0,0 +1,235 @@
+#include "test_support.h"
+#include
+#include
+#include
+#include
+
+static const cy_us_t spin_slice_us = (cy_us_t)10000;
+
+static void spin_until_done(can_test_node_t* const node, cy_future_t* const future)
+{
+ TEST_ASSERT_NOT_NULL(node);
+ TEST_ASSERT_NOT_NULL(future);
+ for (size_t i = 0U; (i < 64U) && !cy_future_done(future); i++) {
+ can_test_node_spin(node, spin_slice_us);
+ }
+ TEST_ASSERT_TRUE(cy_future_done(future));
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(future));
+}
+
+static void test_api_can_pubsub_pinned_best_effort_uses_shorter_legacy_path(void)
+{
+ const uint8_t payload[] = { 0x11U, 0x22U, 0x33U };
+ size_t verbatim_frames = 0U;
+ size_t pinned_frames = 0U;
+
+ {
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, false);
+ node.self_loopback = true;
+ can_test_node_make_platform(&node, 256U, 0U);
+ can_test_node_make_cy(&node, "pubsub_verbatim");
+
+ cy_future_t* const sub = cy_subscribe(node.cy, cy_str("pubsub/verbatim"), 64U);
+ cy_publisher_t* const pub = cy_advertise(node.cy, cy_str("pubsub/verbatim"));
+ TEST_ASSERT_NOT_NULL(sub);
+ TEST_ASSERT_NOT_NULL(pub);
+ can_test_spin_pair(&node, NULL, 4U, spin_slice_us);
+ can_test_node_reset_history(&node);
+
+ TEST_ASSERT_EQUAL_INT(
+ CY_OK,
+ cy_publish(pub, cy_now(node.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL }));
+ spin_until_done(&node, sub);
+ verbatim_frames = node.tx_history_count;
+ TEST_ASSERT_TRUE(verbatim_frames > 1U);
+
+ {
+ const cy_arrival_t arrival = cy_arrival_move(sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ can_test_node_spin(&node, spin_slice_us);
+ can_test_node_destroy(&node);
+ }
+
+ {
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, false);
+ node.self_loopback = true;
+ can_test_node_make_platform(&node, 256U, 0U);
+ can_test_node_make_cy(&node, "pubsub_pinned");
+
+ cy_future_t* const sub = cy_subscribe(node.cy, cy_str("123#123"), 64U);
+ cy_publisher_t* const pub = cy_advertise(node.cy, cy_str("123#123"));
+ TEST_ASSERT_NOT_NULL(sub);
+ TEST_ASSERT_NOT_NULL(pub);
+ can_test_spin_pair(&node, NULL, 4U, spin_slice_us);
+ can_test_node_reset_history(&node);
+
+ TEST_ASSERT_EQUAL_INT(
+ CY_OK,
+ cy_publish(pub, cy_now(node.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL }));
+ spin_until_done(&node, sub);
+ pinned_frames = node.tx_history_count;
+ TEST_ASSERT_EQUAL_size_t(1U, pinned_frames);
+
+ {
+ const cy_arrival_t arrival = cy_arrival_move(sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ can_test_node_spin(&node, spin_slice_us);
+ can_test_node_destroy(&node);
+ }
+
+ TEST_ASSERT_TRUE(verbatim_frames > pinned_frames);
+}
+
+static void test_api_can_pubsub_multiframe_extent_truncation(void)
+{
+ uint8_t payload[120];
+
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, false);
+ node.self_loopback = true;
+ can_test_node_make_platform(&node, 256U, 0U);
+ can_test_node_make_cy(&node, "pubsub_extent");
+ can_test_fill_payload(payload, sizeof(payload), 0x40U);
+
+ cy_future_t* const sub = cy_subscribe(node.cy, cy_str("pubsub/extent"), 40U);
+ cy_publisher_t* const pub = cy_advertise(node.cy, cy_str("pubsub/extent"));
+ TEST_ASSERT_NOT_NULL(sub);
+ TEST_ASSERT_NOT_NULL(pub);
+ can_test_spin_pair(&node, NULL, 4U, spin_slice_us);
+ can_test_node_reset_history(&node);
+
+ TEST_ASSERT_EQUAL_INT(
+ CY_OK, cy_publish(pub, cy_now(node.cy) + (40 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL }));
+ spin_until_done(&node, sub);
+ TEST_ASSERT_TRUE(node.tx_history_count > 1U);
+
+ {
+ uint8_t received[64];
+ const cy_arrival_t arrival = cy_arrival_move(sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ TEST_ASSERT_EQUAL_size_t(40U, cy_message_size(arrival.message.content));
+ TEST_ASSERT_EQUAL_size_t(40U, can_test_message_read_all(arrival.message.content, received, sizeof(received)));
+ TEST_ASSERT_EQUAL_UINT8_ARRAY(payload, received, 40U);
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ can_test_node_spin(&node, spin_slice_us);
+ can_test_node_destroy(&node);
+}
+
+static void test_api_can_pubsub_fd_capable_uses_fd_frames(void)
+{
+ uint8_t payload[20];
+
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, true, false);
+ node.self_loopback = true;
+ can_test_node_make_platform(&node, 256U, 0U);
+ can_test_node_make_cy(&node, "pubsub_fd");
+ can_test_fill_payload(payload, sizeof(payload), 0x60U);
+
+ cy_future_t* const sub = cy_subscribe(node.cy, cy_str("pubsub/fd"), 64U);
+ cy_publisher_t* const pub = cy_advertise(node.cy, cy_str("pubsub/fd"));
+ TEST_ASSERT_NOT_NULL(sub);
+ TEST_ASSERT_NOT_NULL(pub);
+ can_test_spin_pair(&node, NULL, 4U, spin_slice_us);
+ can_test_node_reset_history(&node);
+
+ TEST_ASSERT_EQUAL_INT(
+ CY_OK, cy_publish(pub, cy_now(node.cy) + (40 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL }));
+ spin_until_done(&node, sub);
+ TEST_ASSERT_TRUE(node.tx_fd_calls > 0U);
+
+ {
+ uint8_t received[32];
+ const cy_arrival_t arrival = cy_arrival_move(sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ TEST_ASSERT_TRUE(cy_message_size(arrival.message.content) >= sizeof(payload));
+ TEST_ASSERT_EQUAL_size_t(sizeof(payload),
+ cy_message_read(arrival.message.content, 0U, sizeof(payload), received));
+ TEST_ASSERT_EQUAL_UINT8_ARRAY(payload, received, sizeof(payload));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ can_test_node_spin(&node, spin_slice_us);
+ can_test_node_destroy(&node);
+}
+
+static void test_api_can_pubsub_classic_only_emits_no_fd_frames(void)
+{
+ uint8_t payload[48];
+
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, false);
+ node.self_loopback = true;
+ can_test_node_make_platform(&node, 256U, 0U);
+ can_test_node_make_cy(&node, "pubsub_classic");
+ can_test_fill_payload(payload, sizeof(payload), 0x20U);
+
+ cy_future_t* const sub = cy_subscribe(node.cy, cy_str("125#125"), 64U);
+ cy_publisher_t* const pub = cy_advertise(node.cy, cy_str("125#125"));
+ TEST_ASSERT_NOT_NULL(sub);
+ TEST_ASSERT_NOT_NULL(pub);
+ can_test_spin_pair(&node, NULL, 4U, spin_slice_us);
+ can_test_node_reset_history(&node);
+
+ TEST_ASSERT_EQUAL_INT(
+ CY_OK, cy_publish(pub, cy_now(node.cy) + (40 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL }));
+ spin_until_done(&node, sub);
+ TEST_ASSERT_EQUAL_size_t(0U, node.tx_fd_calls);
+ TEST_ASSERT_TRUE(node.tx_classic_calls > 0U);
+
+ {
+ const cy_arrival_t arrival = cy_arrival_move(sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ can_test_node_spin(&node, spin_slice_us);
+ can_test_node_destroy(&node);
+}
+
+void setUp(void) {}
+
+void tearDown(void) {}
+
+int main(void)
+{
+ UNITY_BEGIN();
+ RUN_TEST(test_api_can_pubsub_pinned_best_effort_uses_shorter_legacy_path);
+ RUN_TEST(test_api_can_pubsub_multiframe_extent_truncation);
+ RUN_TEST(test_api_can_pubsub_fd_capable_uses_fd_frames);
+ RUN_TEST(test_api_can_pubsub_classic_only_emits_no_fd_frames);
+ return UNITY_END();
+}
diff --git a/cy_can/tests/test_api_can_redundancy_lifecycle.c b/cy_can/tests/test_api_can_redundancy_lifecycle.c
new file mode 100644
index 0000000..5403d67
--- /dev/null
+++ b/cy_can/tests/test_api_can_redundancy_lifecycle.c
@@ -0,0 +1,219 @@
+#include "test_support.h"
+#include
+#include
+#include
+#include
+
+static const cy_us_t spin_slice_us = (cy_us_t)10000;
+
+static void spin_until_done(can_test_node_t* const a, can_test_node_t* const b, cy_future_t* const future)
+{
+ TEST_ASSERT_NOT_NULL(future);
+ TEST_ASSERT_TRUE(can_test_spin_pair_until_future_done(a, b, future, 80U, spin_slice_us));
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(future));
+}
+
+static void test_api_can_redundancy_duplicate_ingress_is_delivered_once(void)
+{
+ const uint8_t payload[] = { 0x31U, 0x32U, 0x33U, 0x34U };
+
+ can_test_bus_t bus;
+ can_test_node_t a;
+ can_test_node_t b;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&a, &bus, 2U, false, false);
+ can_test_node_prepare(&b, &bus, 2U, false, false);
+ can_test_node_make_platform(&a, 256U, 0U);
+ can_test_node_make_platform(&b, 256U, 0U);
+ can_test_node_make_cy(&a, "redundancy_a");
+ can_test_node_make_cy(&b, "redundancy_b");
+
+ cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("300#300"));
+ cy_future_t* const sub = cy_subscribe(b.cy, cy_str("300#300"), 64U);
+ TEST_ASSERT_NOT_NULL(pub);
+ TEST_ASSERT_NOT_NULL(sub);
+ can_test_spin_pair(&a, &b, 4U, spin_slice_us);
+ can_test_node_reset_history(&a);
+
+ TEST_ASSERT_EQUAL_INT(
+ CY_OK, cy_publish(pub, cy_now(a.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL }));
+ spin_until_done(&a, &b, sub);
+ TEST_ASSERT_TRUE(can_test_node_count_records_on_iface(&a, 0U) > 0U);
+ TEST_ASSERT_TRUE(can_test_node_count_records_on_iface(&a, 1U) > 0U);
+ TEST_ASSERT_EQUAL_UINT64(1U, cy_arrival_count(sub));
+
+ {
+ const cy_arrival_t arrival = cy_arrival_move(sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ can_test_spin_pair(&a, &b, 4U, spin_slice_us);
+ can_test_node_destroy(&a);
+ can_test_node_destroy(&b);
+}
+
+static void test_api_can_redundancy_retries_backpressured_interface(void)
+{
+ const uint8_t payload[] = { 0x41U, 0x42U };
+
+ can_test_bus_t bus;
+ can_test_node_t a;
+ can_test_node_t b;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&a, &bus, 2U, false, false);
+ can_test_node_prepare(&b, &bus, 2U, false, false);
+ can_test_node_make_platform(&a, 256U, 0U);
+ can_test_node_make_platform(&b, 256U, 0U);
+ can_test_node_make_cy(&a, "backpressure_a");
+ can_test_node_make_cy(&b, "backpressure_b");
+
+ cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("301#301"));
+ cy_future_t* const sub = cy_subscribe(b.cy, cy_str("301#301"), 64U);
+ TEST_ASSERT_NOT_NULL(pub);
+ TEST_ASSERT_NOT_NULL(sub);
+ can_test_spin_pair(&a, &b, 4U, spin_slice_us);
+ can_test_node_reset_history(&a);
+ a.tx_blocked[1] = 1U;
+
+ TEST_ASSERT_EQUAL_INT(
+ CY_OK, cy_publish(pub, cy_now(a.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL }));
+ can_test_spin_pair(&a, &b, 1U, spin_slice_us);
+ TEST_ASSERT_TRUE(can_test_node_count_records_on_iface(&a, 0U) > 0U);
+ TEST_ASSERT_EQUAL_size_t(0U, a.tx_blocked[1]);
+
+ can_test_spin_pair(&a, &b, 1U, spin_slice_us);
+ TEST_ASSERT_TRUE(can_test_node_count_records_on_iface(&a, 1U) > 0U);
+ spin_until_done(&a, &b, sub);
+
+ {
+ const cy_arrival_t arrival = cy_arrival_move(sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ can_test_spin_pair(&a, &b, 4U, spin_slice_us);
+ can_test_node_destroy(&a);
+ can_test_node_destroy(&b);
+}
+
+static void run_subscription_revival_case(const char* const topic_name, const char* const home)
+{
+ const uint8_t payload[] = { 0x51U, 0x52U, 0x53U, 0x54U, 0x55U };
+
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, false);
+ node.self_loopback = true;
+ can_test_node_make_platform(&node, 256U, 0U);
+ can_test_node_make_cy(&node, home);
+
+ cy_future_t* const first = cy_subscribe(node.cy, cy_str(topic_name), 64U);
+ cy_future_t* second = NULL;
+ cy_publisher_t* pub = NULL;
+ TEST_ASSERT_NOT_NULL(first);
+ cy_future_destroy(first);
+
+ second = cy_subscribe(node.cy, cy_str(topic_name), 64U);
+ pub = cy_advertise(node.cy, cy_str(topic_name));
+ TEST_ASSERT_NOT_NULL(second);
+ TEST_ASSERT_NOT_NULL(pub);
+
+ TEST_ASSERT_EQUAL_INT(
+ CY_OK, cy_publish(pub, cy_now(node.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL }));
+ for (size_t i = 0U; (i < 80U) && !cy_future_done(second); i++) {
+ can_test_node_spin(&node, spin_slice_us);
+ }
+ TEST_ASSERT_TRUE(cy_future_done(second));
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(second));
+
+ {
+ const cy_arrival_t arrival = cy_arrival_move(second);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ cy_unadvertise(pub);
+ cy_future_destroy(second);
+ can_test_node_spin(&node, spin_slice_us);
+ can_test_node_destroy(&node);
+}
+
+static void test_api_can_lifecycle_revives_verbatim_and_pinned_subscriptions(void)
+{
+ run_subscription_revival_case("lifecycle/verbatim", "revive_verbatim");
+ run_subscription_revival_case("302#302", "revive_pinned");
+}
+
+static void test_api_can_lifecycle_recreates_cy_on_same_platform(void)
+{
+ const uint8_t payload[] = { 0x61U, 0x62U, 0x63U };
+
+ can_test_bus_t bus;
+ can_test_node_t node;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&node, &bus, 1U, false, false);
+ node.self_loopback = true;
+ can_test_node_make_platform(&node, 256U, 0U);
+ can_test_node_make_cy(&node, "first_cy");
+
+ {
+ cy_future_t* const first = cy_subscribe(node.cy, cy_str("303#303"), 64U);
+ TEST_ASSERT_NOT_NULL(first);
+ cy_future_destroy(first);
+ can_test_node_spin(&node, spin_slice_us);
+ }
+ cy_destroy(node.cy);
+ node.cy = NULL;
+
+ can_test_node_make_cy(&node, "second_cy");
+ {
+ cy_future_t* const sub = cy_subscribe(node.cy, cy_str("303#303"), 64U);
+ cy_publisher_t* const pub = cy_advertise(node.cy, cy_str("303#303"));
+ TEST_ASSERT_NOT_NULL(sub);
+ TEST_ASSERT_NOT_NULL(pub);
+
+ TEST_ASSERT_EQUAL_INT(
+ CY_OK,
+ cy_publish(pub, cy_now(node.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL }));
+ for (size_t i = 0U; (i < 80U) && !cy_future_done(sub); i++) {
+ can_test_node_spin(&node, spin_slice_us);
+ }
+ TEST_ASSERT_TRUE(cy_future_done(sub));
+
+ {
+ const cy_arrival_t arrival = cy_arrival_move(sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ can_test_node_spin(&node, spin_slice_us);
+ }
+
+ can_test_node_destroy(&node);
+}
+
+void setUp(void) {}
+
+void tearDown(void) {}
+
+int main(void)
+{
+ UNITY_BEGIN();
+ RUN_TEST(test_api_can_redundancy_duplicate_ingress_is_delivered_once);
+ RUN_TEST(test_api_can_redundancy_retries_backpressured_interface);
+ RUN_TEST(test_api_can_lifecycle_revives_verbatim_and_pinned_subscriptions);
+ RUN_TEST(test_api_can_lifecycle_recreates_cy_on_same_platform);
+ return UNITY_END();
+}
diff --git a/cy_can/tests/test_api_can_reliable_rpc.c b/cy_can/tests/test_api_can_reliable_rpc.c
new file mode 100644
index 0000000..fceef66
--- /dev/null
+++ b/cy_can/tests/test_api_can_reliable_rpc.c
@@ -0,0 +1,261 @@
+#include "test_support.h"
+#include
+#include
+#include
+#include
+#include
+
+static const cy_us_t spin_slice_us = (cy_us_t)10000;
+
+typedef struct
+{
+ size_t count;
+ size_t transient_error_count;
+ cy_err_t last_error;
+ uint64_t remote_id[4];
+ uint64_t seqno[4];
+ size_t size[4];
+ uint8_t payload[4][64];
+} response_capture_t;
+
+typedef struct
+{
+ size_t calls;
+ cy_future_t* reliable_future;
+ uint8_t response_a[8];
+ uint8_t response_b[8];
+ size_t response_a_size;
+ size_t response_b_size;
+} request_server_t;
+
+static void on_response_capture(cy_future_t* const future)
+{
+ response_capture_t* const cap = (response_capture_t*)cy_future_context(future).ptr[0];
+ TEST_ASSERT_NOT_NULL(cap);
+ if (!cy_future_done(future)) {
+ cap->transient_error_count++;
+ cap->last_error = cy_future_error(future);
+ return;
+ }
+ if (cy_future_error(future) != CY_OK) {
+ cap->last_error = cy_future_error(future);
+ return;
+ }
+ {
+ const cy_response_t rsp = cy_response_move(future);
+ TEST_ASSERT_NOT_NULL(rsp.message.content);
+ TEST_ASSERT_TRUE(cap->count < 4U);
+ cap->remote_id[cap->count] = rsp.remote_id;
+ cap->seqno[cap->count] = rsp.seqno;
+ cap->size[cap->count] =
+ can_test_message_read_all(rsp.message.content, cap->payload[cap->count], sizeof(cap->payload[cap->count]));
+ cap->count++;
+ cy_message_refcount_dec(rsp.message.content);
+ }
+}
+
+static void on_request_server(cy_future_t* const future)
+{
+ request_server_t* const server = (request_server_t*)cy_future_context(future).ptr[0];
+ TEST_ASSERT_NOT_NULL(server);
+ if (!cy_future_done(future)) {
+ return;
+ }
+ {
+ cy_arrival_t arrival = cy_arrival_move(future);
+ if (arrival.message.content == NULL) {
+ return;
+ }
+ server->calls++;
+ TEST_ASSERT_EQUAL_INT(CY_OK,
+ cy_respond(&arrival.breadcrumb,
+ cy_now(arrival.breadcrumb.cy) + (20 * spin_slice_us),
+ (cy_bytes_t){ server->response_a_size, server->response_a, NULL }));
+ server->reliable_future =
+ cy_respond_reliable(&arrival.breadcrumb,
+ cy_now(arrival.breadcrumb.cy) + (20 * spin_slice_us),
+ (cy_bytes_t){ server->response_b_size, server->response_b, NULL });
+ TEST_ASSERT_NOT_NULL(server->reliable_future);
+ cy_message_refcount_dec(arrival.message.content);
+ }
+}
+
+static bool two_responses_received(void* const context)
+{
+ const response_capture_t* const cap = (const response_capture_t*)context;
+ return cap->count >= 2U;
+}
+
+static void test_api_can_reliable_publish_success(void)
+{
+ const uint8_t payload[] = { 1U, 2U, 3U, 4U, 5U, 6U };
+
+ can_test_bus_t bus;
+ can_test_node_t a;
+ can_test_node_t b;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&a, &bus, 1U, false, false);
+ can_test_node_prepare(&b, &bus, 1U, false, false);
+ can_test_node_make_platform(&a, 256U, 0U);
+ can_test_node_make_platform(&b, 256U, 0U);
+ can_test_node_make_cy(&a, "reliable_pub_a");
+ can_test_node_make_cy(&b, "reliable_pub_b");
+
+ cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("200#200"));
+ cy_future_t* const sub = cy_subscribe(b.cy, cy_str("200#200"), 64U);
+ TEST_ASSERT_NOT_NULL(pub);
+ TEST_ASSERT_NOT_NULL(sub);
+ can_test_spin_pair(&a, &b, 4U, spin_slice_us);
+
+ cy_future_t* const fut =
+ cy_publish_reliable(pub, cy_now(a.cy) + (30 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL });
+ TEST_ASSERT_NOT_NULL(fut);
+ TEST_ASSERT_TRUE(can_test_spin_pair_until_future_done(&a, &b, fut, 80U, spin_slice_us));
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(fut));
+ TEST_ASSERT_TRUE(cy_future_done(sub));
+
+ {
+ const cy_arrival_t arrival = cy_arrival_move(sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ cy_future_destroy(fut);
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ can_test_spin_pair(&a, &b, 4U, spin_slice_us);
+ can_test_node_destroy(&a);
+ can_test_node_destroy(&b);
+}
+
+static void test_api_can_reliable_publish_times_out_when_ack_is_lost(void)
+{
+ const uint8_t payload[] = { 9U, 8U, 7U, 6U };
+
+ can_test_bus_t bus;
+ can_test_node_t a;
+ can_test_node_t b;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&a, &bus, 1U, false, false);
+ can_test_node_prepare(&b, &bus, 1U, false, false);
+ can_test_node_make_platform(&a, 256U, 0U);
+ can_test_node_make_platform(&b, 256U, 0U);
+ can_test_node_make_cy(&a, "reliable_timeout_a");
+ can_test_node_make_cy(&b, "reliable_timeout_b");
+
+ cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("201#201"));
+ cy_future_t* const sub = cy_subscribe(b.cy, cy_str("201#201"), 64U);
+ TEST_ASSERT_NOT_NULL(pub);
+ TEST_ASSERT_NOT_NULL(sub);
+ can_test_spin_pair(&a, &b, 4U, spin_slice_us);
+ b.drop_tx[0] = true;
+
+ cy_future_t* const fut =
+ cy_publish_reliable(pub, cy_now(a.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL });
+ TEST_ASSERT_NOT_NULL(fut);
+ TEST_ASSERT_TRUE(can_test_spin_pair_until_future_done(&a, &b, fut, 80U, spin_slice_us));
+ TEST_ASSERT_EQUAL_INT(CY_ERR_DELIVERY, cy_future_error(fut));
+ TEST_ASSERT_TRUE(cy_future_done(sub));
+
+ {
+ const cy_arrival_t arrival = cy_arrival_move(sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ cy_future_destroy(fut);
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ can_test_spin_pair(&a, &b, 4U, spin_slice_us);
+ can_test_node_destroy(&a);
+ can_test_node_destroy(&b);
+}
+
+static void test_api_can_request_response_streaming_and_reliable_response(void)
+{
+ const uint8_t request_payload[] = { 0xAAU, 0xBBU, 0xCCU };
+
+ can_test_bus_t bus;
+ can_test_node_t client;
+ can_test_node_t server;
+ response_capture_t capture;
+ request_server_t server_state;
+ can_test_bus_init(&bus);
+ can_test_node_prepare(&client, &bus, 1U, false, false);
+ can_test_node_prepare(&server, &bus, 1U, false, false);
+ can_test_node_make_platform(&client, 256U, 0U);
+ can_test_node_make_platform(&server, 256U, 0U);
+ can_test_node_make_cy(&client, "rpc_client");
+ can_test_node_make_cy(&server, "rpc_server");
+ (void)memset(&capture, 0, sizeof(capture));
+ (void)memset(&server_state, 0, sizeof(server_state));
+ server_state.response_a[0] = 0x10U;
+ server_state.response_a[1] = 0x11U;
+ server_state.response_a[2] = 0x12U;
+ server_state.response_a_size = 3U;
+ server_state.response_b[0] = 0x20U;
+ server_state.response_b[1] = 0x21U;
+ server_state.response_b_size = 2U;
+
+ cy_publisher_t* const pub = cy_advertise_client(client.cy, cy_str("202#202"), 64U);
+ cy_future_t* const sub = cy_subscribe(server.cy, cy_str("202#202"), 64U);
+ TEST_ASSERT_NOT_NULL(pub);
+ TEST_ASSERT_NOT_NULL(sub);
+ {
+ cy_user_context_t ctx = CY_USER_CONTEXT_EMPTY;
+ ctx.ptr[0] = &server_state;
+ cy_future_context_set(sub, ctx);
+ cy_future_callback_set(sub, on_request_server);
+ }
+ can_test_spin_pair(&client, &server, 4U, spin_slice_us);
+
+ cy_future_t* const req = cy_request(pub,
+ cy_now(client.cy) + (30 * spin_slice_us),
+ 10 * spin_slice_us,
+ (cy_bytes_t){ sizeof(request_payload), request_payload, NULL });
+ TEST_ASSERT_NOT_NULL(req);
+ {
+ cy_user_context_t ctx = CY_USER_CONTEXT_EMPTY;
+ ctx.ptr[0] = &capture;
+ cy_future_context_set(req, ctx);
+ cy_future_callback_set(req, on_response_capture);
+ }
+
+ TEST_ASSERT_TRUE(
+ can_test_spin_pair_until_condition(&client, &server, two_responses_received, &capture, 120U, spin_slice_us));
+ TEST_ASSERT_EQUAL_size_t(1U, server_state.calls);
+ TEST_ASSERT_NOT_NULL(server_state.reliable_future);
+ TEST_ASSERT_TRUE(
+ can_test_spin_pair_until_future_done(&client, &server, server_state.reliable_future, 120U, spin_slice_us));
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(server_state.reliable_future));
+ TEST_ASSERT_EQUAL_size_t(2U, capture.count);
+ TEST_ASSERT_EQUAL_UINT64(0U, capture.seqno[0]);
+ TEST_ASSERT_EQUAL_UINT64(1U, capture.seqno[1]);
+ TEST_ASSERT_EQUAL_size_t(server_state.response_a_size, capture.size[0]);
+ TEST_ASSERT_EQUAL_size_t(server_state.response_b_size, capture.size[1]);
+ TEST_ASSERT_EQUAL_UINT8_ARRAY(server_state.response_a, capture.payload[0], server_state.response_a_size);
+ TEST_ASSERT_EQUAL_UINT8_ARRAY(server_state.response_b, capture.payload[1], server_state.response_b_size);
+
+ cy_future_destroy(server_state.reliable_future);
+ cy_future_destroy(req);
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ can_test_spin_pair(&client, &server, 4U, spin_slice_us);
+ can_test_node_destroy(&client);
+ can_test_node_destroy(&server);
+}
+
+void setUp(void) {}
+
+void tearDown(void) {}
+
+int main(void)
+{
+ UNITY_BEGIN();
+ RUN_TEST(test_api_can_reliable_publish_success);
+ RUN_TEST(test_api_can_reliable_publish_times_out_when_ack_is_lost);
+ RUN_TEST(test_api_can_request_response_streaming_and_reliable_response);
+ return UNITY_END();
+}
diff --git a/cy_can/tests/test_api_can_socketcan_e2e.c b/cy_can/tests/test_api_can_socketcan_e2e.c
new file mode 100644
index 0000000..d28d517
--- /dev/null
+++ b/cy_can/tests/test_api_can_socketcan_e2e.c
@@ -0,0 +1,228 @@
+#include "test_support.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+typedef struct
+{
+ size_t calls;
+} socketcan_server_t;
+
+static char* iface_name_buffer(void)
+{
+ static char value[16] = { 0 };
+ return value;
+}
+
+static void sleep_2ms(void)
+{
+ const struct timespec ts = { .tv_sec = 0, .tv_nsec = 2 * 1000 * 1000 };
+ (void)nanosleep(&ts, NULL);
+}
+
+static int run_cmd(const char* const command)
+{
+ TEST_ASSERT_NOT_NULL(command);
+ return system(command); // NOLINT(cert-env33-c)
+}
+
+static int skip_with_reason(const char* const reason)
+{
+ (void)fprintf(stderr, "SKIP: %s\n", reason);
+ return CAN_TEST_SKIP_CODE;
+}
+
+static int provision_vcan_interface(char* const out_name, const size_t out_size)
+{
+ char command[256];
+ const unsigned long tag = (unsigned long)getpid() % 100000UL;
+ TEST_ASSERT_NOT_NULL(out_name);
+ TEST_ASSERT_TRUE(out_size >= sizeof(iface_name_buffer()[0]) * 16U);
+ (void)snprintf(out_name, out_size, "vcan%05lu", tag);
+
+ if (run_cmd("command -v ip >/dev/null 2>&1") != 0) {
+ return skip_with_reason("ip tool is unavailable");
+ }
+ if (run_cmd("sudo -n true >/dev/null 2>&1") != 0) {
+ return skip_with_reason("passwordless sudo is unavailable");
+ }
+ if (run_cmd("sudo -n modprobe vcan >/dev/null 2>&1") != 0) {
+ return skip_with_reason("vcan kernel module could not be loaded");
+ }
+
+ (void)snprintf(command, sizeof(command), "sudo -n ip link add dev %s type vcan >/dev/null 2>&1", out_name);
+ if (run_cmd(command) != 0) {
+ return skip_with_reason("failed to create vcan interface");
+ }
+
+ (void)snprintf(command, sizeof(command), "sudo -n ip link set dev %s up >/dev/null 2>&1", out_name);
+ if (run_cmd(command) != 0) {
+ (void)snprintf(command, sizeof(command), "sudo -n ip link del dev %s >/dev/null 2>&1", out_name);
+ (void)run_cmd(command);
+ return skip_with_reason("failed to bring vcan interface up");
+ }
+ return 0;
+}
+
+static void destroy_vcan_interface(const char* const iface_name)
+{
+ char command[256];
+ if ((iface_name == NULL) || (iface_name[0] == '\0')) {
+ return;
+ }
+ (void)snprintf(command, sizeof(command), "sudo -n ip link del dev %s >/dev/null 2>&1", iface_name);
+ (void)run_cmd(command);
+}
+
+static void on_socketcan_request(cy_future_t* const future)
+{
+ socketcan_server_t* const state = (socketcan_server_t*)cy_future_context(future).ptr[0];
+ TEST_ASSERT_NOT_NULL(state);
+ if (!cy_future_done(future)) {
+ return;
+ }
+ {
+ const uint8_t response[] = { 0x91U, 0x92U, 0x93U };
+ cy_arrival_t arrival = cy_arrival_move(future);
+ if (arrival.message.content == NULL) {
+ return;
+ }
+ state->calls++;
+ TEST_ASSERT_EQUAL_INT(CY_OK,
+ cy_respond(&arrival.breadcrumb,
+ cy_now(arrival.breadcrumb.cy) + 300000,
+ (cy_bytes_t){ sizeof(response), response, NULL }));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+}
+
+static void test_api_can_socketcan_e2e_smoke(void)
+{
+ const char* ifaces[1] = { iface_name_buffer() };
+ const uint8_t pub_payload[] = { 0x81U, 0x82U, 0x83U, 0x84U };
+ const uint8_t req_payload[] = { 0xA1U, 0xA2U };
+ const uint8_t rsp_payload[] = { 0x91U, 0x92U, 0x93U };
+ socketcan_server_t server_state;
+ cy_platform_t* platform_a = NULL;
+ cy_platform_t* platform_b = NULL;
+ cy_t* cy_a = NULL;
+ cy_t* cy_b = NULL;
+ cy_future_t* sub = NULL;
+ cy_publisher_t* pub = NULL;
+ cy_publisher_t* client = NULL;
+ cy_future_t* server_sub = NULL;
+ cy_future_t* req = NULL;
+ (void)memset(&server_state, 0, sizeof(server_state));
+
+ platform_a = cy_can_socketcan_new(1U, ifaces, 256U);
+ platform_b = cy_can_socketcan_new(1U, ifaces, 256U);
+ TEST_ASSERT_NOT_NULL(platform_a);
+ TEST_ASSERT_NOT_NULL(platform_b);
+
+ cy_a = cy_new(platform_a, cy_str("socketcan_a"), (cy_str_t){ 0U, NULL });
+ cy_b = cy_new(platform_b, cy_str("socketcan_b"), (cy_str_t){ 0U, NULL });
+ TEST_ASSERT_NOT_NULL(cy_a);
+ TEST_ASSERT_NOT_NULL(cy_b);
+
+ sub = cy_subscribe(cy_b, cy_str("500#500"), 64U);
+ pub = cy_advertise(cy_a, cy_str("500#500"));
+ TEST_ASSERT_NOT_NULL(sub);
+ TEST_ASSERT_NOT_NULL(pub);
+
+ TEST_ASSERT_EQUAL_INT(
+ CY_OK, cy_publish(pub, cy_now(cy_a) + 300000, (cy_bytes_t){ sizeof(pub_payload), pub_payload, NULL }));
+ for (size_t i = 0U; (i < 200U) && !cy_future_done(sub); i++) {
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_a, cy_now(cy_a) + (cy_us_t)2000));
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_b, cy_now(cy_b) + (cy_us_t)2000));
+ sleep_2ms();
+ }
+ TEST_ASSERT_TRUE(cy_future_done(sub));
+ {
+ uint8_t received[16];
+ const cy_arrival_t arrival = cy_arrival_move(sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ TEST_ASSERT_EQUAL_size_t(sizeof(pub_payload), cy_message_size(arrival.message.content));
+ TEST_ASSERT_EQUAL_size_t(sizeof(pub_payload),
+ cy_message_read(arrival.message.content, 0U, sizeof(received), received));
+ TEST_ASSERT_EQUAL_UINT8_ARRAY(pub_payload, received, sizeof(pub_payload));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ client = cy_advertise_client(cy_a, cy_str("501#501"), 64U);
+ server_sub = cy_subscribe(cy_b, cy_str("501#501"), 64U);
+ TEST_ASSERT_NOT_NULL(client);
+ TEST_ASSERT_NOT_NULL(server_sub);
+ {
+ cy_user_context_t ctx = CY_USER_CONTEXT_EMPTY;
+ ctx.ptr[0] = &server_state;
+ cy_future_context_set(server_sub, ctx);
+ cy_future_callback_set(server_sub, on_socketcan_request);
+ }
+ for (size_t i = 0U; i < 8U; i++) {
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_a, cy_now(cy_a) + (cy_us_t)2000));
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_b, cy_now(cy_b) + (cy_us_t)2000));
+ sleep_2ms();
+ }
+
+ req = cy_request(client, cy_now(cy_a) + 300000, 100000, (cy_bytes_t){ sizeof(req_payload), req_payload, NULL });
+ TEST_ASSERT_NOT_NULL(req);
+ for (size_t i = 0U; (i < 200U) && (cy_response_count(req) == 0U); i++) {
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_a, cy_now(cy_a) + (cy_us_t)2000));
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_b, cy_now(cy_b) + (cy_us_t)2000));
+ sleep_2ms();
+ }
+ TEST_ASSERT_EQUAL_size_t(1U, server_state.calls);
+ TEST_ASSERT_TRUE(cy_response_count(req) > 0U);
+ {
+ uint8_t received[16] = { 0 };
+ const cy_response_t response = cy_response_move(req);
+ TEST_ASSERT_NOT_NULL(response.message.content);
+ TEST_ASSERT_TRUE(cy_message_size(response.message.content) >= sizeof(rsp_payload));
+ TEST_ASSERT_EQUAL_size_t(sizeof(rsp_payload),
+ cy_message_read(response.message.content, 0U, sizeof(rsp_payload), received));
+ TEST_ASSERT_EQUAL_UINT8_ARRAY(rsp_payload, received, sizeof(rsp_payload));
+ cy_message_refcount_dec(response.message.content);
+ }
+
+ cy_future_destroy(req);
+ cy_unadvertise(client);
+ cy_future_destroy(server_sub);
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ for (size_t i = 0U; i < 8U; i++) {
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_a, cy_now(cy_a) + (cy_us_t)2000));
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_b, cy_now(cy_b) + (cy_us_t)2000));
+ sleep_2ms();
+ }
+ cy_destroy(cy_a);
+ cy_destroy(cy_b);
+ cy_can_socketcan_destroy(platform_a);
+ cy_can_socketcan_destroy(platform_b);
+}
+
+void setUp(void) {}
+
+void tearDown(void) {}
+
+int main(void)
+{
+ int result = 0;
+ result = provision_vcan_interface(iface_name_buffer(), sizeof(iface_name_buffer()[0]) * 16U);
+ if (result != 0) {
+ return result;
+ }
+
+ UNITY_BEGIN();
+ RUN_TEST(test_api_can_socketcan_e2e_smoke);
+ result = UNITY_END();
+
+ destroy_vcan_interface(iface_name_buffer());
+ return result;
+}
diff --git a/cy_can/tests/test_api_can_tombstone_revival.c b/cy_can/tests/test_api_can_tombstone_revival.c
new file mode 100644
index 0000000..fd5ff84
--- /dev/null
+++ b/cy_can/tests/test_api_can_tombstone_revival.c
@@ -0,0 +1,222 @@
+#include "test_support.h"
+#include
+#include
+#include
+#include
+
+static const cy_us_t spin_slice_us = (cy_us_t)10000;
+
+typedef struct
+{
+ can_test_bus_t bus;
+ can_test_node_t node;
+} fixture_t;
+
+static void fixture_init(fixture_t* const self, const char* const home)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ TEST_ASSERT_NOT_NULL(home);
+ can_test_bus_init(&self->bus);
+ can_test_node_prepare(&self->node, &self->bus, 1U, false, true);
+ self->node.self_loopback = true;
+ can_test_node_make_platform(&self->node, 256U, 16U);
+ can_test_node_make_cy(&self->node, home);
+}
+
+static void fixture_deinit(fixture_t* const self)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ can_test_node_destroy(&self->node);
+}
+
+static size_t settle_filters(can_test_node_t* const node)
+{
+ TEST_ASSERT_NOT_NULL(node);
+ can_test_node_spin(node, spin_slice_us);
+ can_test_node_spin(node, spin_slice_us);
+ {
+ const size_t out = node->filter_calls;
+ can_test_node_spin(node, spin_slice_us);
+ TEST_ASSERT_EQUAL_size_t(out, node->filter_calls);
+ return out;
+ }
+}
+
+static void spin_until_done(can_test_node_t* const node, cy_future_t* const future)
+{
+ TEST_ASSERT_NOT_NULL(node);
+ TEST_ASSERT_NOT_NULL(future);
+ for (size_t i = 0U; (i < 80U) && !cy_future_done(future); i++) {
+ can_test_node_spin(node, spin_slice_us);
+ }
+ TEST_ASSERT_TRUE(cy_future_done(future));
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(future));
+}
+
+static void publish_and_expect_payload(can_test_node_t* const node,
+ cy_future_t* const sub,
+ const char* const topic_name,
+ const void* const payload,
+ const size_t size)
+{
+ cy_publisher_t* const pub = cy_advertise(node->cy, cy_str(topic_name));
+ TEST_ASSERT_NOT_NULL(pub);
+ TEST_ASSERT_EQUAL_INT(
+ CY_OK, cy_publish(pub, cy_now(node->cy) + (20 * spin_slice_us), (cy_bytes_t){ size, payload, NULL }));
+ spin_until_done(node, sub);
+ TEST_ASSERT_EQUAL_UINT64(1U, cy_arrival_count(sub));
+ {
+ const cy_arrival_t arrival = cy_arrival_move(sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ TEST_ASSERT_EQUAL_size_t(size, cy_message_size(arrival.message.content));
+ can_test_assert_message_equals(arrival.message.content, payload, size);
+ cy_message_refcount_dec(arrival.message.content);
+ }
+ cy_unadvertise(pub);
+}
+
+static void run_revive_extent_case(const char* const topic_name,
+ const char* const home,
+ const size_t first_extent,
+ const size_t second_extent)
+{
+ uint8_t payload[96];
+ fixture_t fix;
+ size_t baseline_filter_calls = 0U;
+ can_test_fill_payload(payload, sizeof(payload), 0x30U);
+ fixture_init(&fix, home);
+
+ cy_future_t* const first = cy_subscribe(fix.node.cy, cy_str(topic_name), first_extent);
+ TEST_ASSERT_NOT_NULL(first);
+ baseline_filter_calls = settle_filters(&fix.node);
+
+ cy_future_destroy(first);
+ {
+ cy_future_t* const second = cy_subscribe(fix.node.cy, cy_str(topic_name), second_extent);
+ TEST_ASSERT_NOT_NULL(second);
+ TEST_ASSERT_EQUAL_size_t(baseline_filter_calls, settle_filters(&fix.node));
+ publish_and_expect_payload(&fix.node, second, topic_name, payload, sizeof(payload));
+ cy_future_destroy(second);
+ }
+
+ can_test_node_spin(&fix.node, spin_slice_us);
+ fixture_deinit(&fix);
+}
+
+static void test_api_can_tombstone_verbatim_revive_preserves_old_larger_extent(void)
+{
+ run_revive_extent_case("tombstone/verbatim_preserve", "tombstone_verbatim_preserve", 128U, 64U);
+}
+
+static void test_api_can_tombstone_verbatim_revive_grows_extent(void)
+{
+ run_revive_extent_case("tombstone/verbatim_grow", "tombstone_verbatim_grow", 64U, 128U);
+}
+
+static void test_api_can_tombstone_pinned_revive_preserves_old_larger_extent(void)
+{
+ run_revive_extent_case("123#123", "tombstone_pinned_preserve", 128U, 64U);
+}
+
+static void test_api_can_tombstone_pinned_revive_grows_extent(void)
+{
+ run_revive_extent_case("124#124", "tombstone_pinned_grow", 64U, 128U);
+}
+
+static void test_api_can_tombstone_recreate_after_spin_reconfigures_filters(void)
+{
+ const uint8_t payload[] = { 0x41U, 0x42U, 0x43U, 0x44U };
+ fixture_t fix;
+ size_t before_destroy = 0U;
+ size_t after_destroy = 0U;
+ fixture_init(&fix, "tombstone_recreate_after_spin");
+
+ cy_future_t* first = cy_subscribe(fix.node.cy, cy_str("125#125"), 64U);
+ TEST_ASSERT_NOT_NULL(first);
+ before_destroy = settle_filters(&fix.node);
+
+ cy_future_destroy(first);
+ after_destroy = settle_filters(&fix.node);
+ TEST_ASSERT_TRUE(after_destroy > before_destroy);
+
+ {
+ cy_future_t* const second = cy_subscribe(fix.node.cy, cy_str("125#125"), 64U);
+ TEST_ASSERT_NOT_NULL(second);
+ TEST_ASSERT_TRUE(settle_filters(&fix.node) > after_destroy);
+ publish_and_expect_payload(&fix.node, second, "125#125", payload, sizeof(payload));
+ cy_future_destroy(second);
+ }
+
+ can_test_node_spin(&fix.node, spin_slice_us);
+ fixture_deinit(&fix);
+}
+
+static void test_api_can_tombstone_multiple_destroy_recreate_cycles_before_spin(void)
+{
+ const uint8_t payload[] = { 0x51U, 0x52U, 0x53U, 0x54U, 0x55U };
+ fixture_t fix;
+ size_t baseline_filter_calls = 0U;
+ fixture_init(&fix, "tombstone_multi_cycle");
+
+ cy_future_t* first = cy_subscribe(fix.node.cy, cy_str("126#126"), 96U);
+ TEST_ASSERT_NOT_NULL(first);
+ baseline_filter_calls = settle_filters(&fix.node);
+
+ cy_future_destroy(first);
+ {
+ cy_future_t* const second = cy_subscribe(fix.node.cy, cy_str("126#126"), 64U);
+ TEST_ASSERT_NOT_NULL(second);
+ cy_future_destroy(second);
+ }
+ {
+ cy_future_t* const third = cy_subscribe(fix.node.cy, cy_str("126#126"), 96U);
+ TEST_ASSERT_NOT_NULL(third);
+ TEST_ASSERT_EQUAL_size_t(baseline_filter_calls, settle_filters(&fix.node));
+ publish_and_expect_payload(&fix.node, third, "126#126", payload, sizeof(payload));
+ cy_future_destroy(third);
+ }
+
+ can_test_node_spin(&fix.node, spin_slice_us);
+ fixture_deinit(&fix);
+}
+
+static void test_api_can_tombstone_broadcast_reader_revives_across_cy_recreation(void)
+{
+ const uint8_t payload[] = { 0x61U, 0x62U, 0x63U };
+ fixture_t fix;
+ size_t baseline_filter_calls = 0U;
+ fixture_init(&fix, "tombstone_broadcast_a");
+ baseline_filter_calls = settle_filters(&fix.node);
+
+ cy_destroy(fix.node.cy);
+ fix.node.cy = NULL;
+ can_test_node_make_cy(&fix.node, "tombstone_broadcast_b");
+ TEST_ASSERT_EQUAL_size_t(baseline_filter_calls, settle_filters(&fix.node));
+
+ {
+ cy_future_t* const sub = cy_subscribe(fix.node.cy, cy_str("127#127"), 64U);
+ TEST_ASSERT_NOT_NULL(sub);
+ publish_and_expect_payload(&fix.node, sub, "127#127", payload, sizeof(payload));
+ cy_future_destroy(sub);
+ }
+
+ can_test_node_spin(&fix.node, spin_slice_us);
+ fixture_deinit(&fix);
+}
+
+void setUp(void) {}
+
+void tearDown(void) {}
+
+int main(void)
+{
+ UNITY_BEGIN();
+ RUN_TEST(test_api_can_tombstone_verbatim_revive_preserves_old_larger_extent);
+ RUN_TEST(test_api_can_tombstone_verbatim_revive_grows_extent);
+ RUN_TEST(test_api_can_tombstone_pinned_revive_preserves_old_larger_extent);
+ RUN_TEST(test_api_can_tombstone_pinned_revive_grows_extent);
+ RUN_TEST(test_api_can_tombstone_recreate_after_spin_reconfigures_filters);
+ RUN_TEST(test_api_can_tombstone_multiple_destroy_recreate_cycles_before_spin);
+ RUN_TEST(test_api_can_tombstone_broadcast_reader_revives_across_cy_recreation);
+ return UNITY_END();
+}
diff --git a/cy_can/tests/test_support.c b/cy_can/tests/test_support.c
new file mode 100644
index 0000000..0f14b6a
--- /dev/null
+++ b/cy_can/tests/test_support.c
@@ -0,0 +1,498 @@
+#include "test_support.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+typedef struct allocation_header_t
+{
+ size_t size;
+ uint32_t magic;
+ uint32_t reserved;
+ can_test_heap_t* owner;
+} allocation_header_t;
+
+static const uint32_t allocation_magic = UINT32_C(0xC0DEFACE);
+
+static size_t smaller(const size_t a, const size_t b) { return (a < b) ? a : b; }
+
+static void* heap_allocate(can_test_heap_t* self, const size_t size)
+{
+ if ((self->fail_after_n_allocations != SIZE_MAX) && (self->allocation_attempts >= self->fail_after_n_allocations)) {
+ return NULL;
+ }
+ self->allocation_attempts++;
+
+ allocation_header_t* const header = (allocation_header_t*)malloc(sizeof(allocation_header_t) + size);
+ if (header == NULL) {
+ return NULL;
+ }
+ header->size = size;
+ header->magic = allocation_magic;
+ header->owner = self;
+ self->allocated_fragments++;
+ self->allocated_bytes += size;
+ return (void*)(header + 1);
+}
+
+static allocation_header_t* header_from_payload(void* const ptr)
+{
+ return (allocation_header_t*)((uint8_t*)ptr - sizeof(allocation_header_t));
+}
+
+static const allocation_header_t* header_from_payload_const(const void* const ptr)
+{
+ return (const allocation_header_t*)((const uint8_t*)ptr - sizeof(allocation_header_t));
+}
+
+static void heap_free(can_test_heap_t* const self, void* const ptr)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ if (ptr != NULL) {
+ allocation_header_t* const header = header_from_payload(ptr);
+ TEST_ASSERT_EQUAL_UINT32(allocation_magic, header->magic);
+ TEST_ASSERT_TRUE(header->owner == self);
+ TEST_ASSERT_TRUE(self->allocated_fragments > 0U);
+ TEST_ASSERT_TRUE(self->allocated_bytes >= header->size);
+ self->allocated_fragments--;
+ self->allocated_bytes -= header->size;
+ header->magic = 0U;
+ free(header);
+ }
+}
+
+static void bus_register_node(can_test_bus_t* const bus, can_test_node_t* const node)
+{
+ if (bus == NULL) {
+ return;
+ }
+ for (size_t i = 0U; i < CAN_TEST_MAX_NODES; i++) {
+ if (bus->nodes[i] == NULL) {
+ bus->nodes[i] = node;
+ return;
+ }
+ }
+ TEST_FAIL_MESSAGE("bus registry exhausted");
+}
+
+static void bus_unregister_node(can_test_bus_t* const bus, can_test_node_t* const node)
+{
+ if (bus == NULL) {
+ return;
+ }
+ for (size_t i = 0U; i < CAN_TEST_MAX_NODES; i++) {
+ if (bus->nodes[i] == node) {
+ bus->nodes[i] = NULL;
+ return;
+ }
+ }
+}
+
+static void enqueue_rx(can_test_node_t* const self, const cy_can_rx_t* const frame)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ TEST_ASSERT_NOT_NULL(frame);
+ TEST_ASSERT_TRUE(self->rx_count < CAN_TEST_MAX_QUEUE);
+ self->rx_queue[(self->rx_head + self->rx_count) % CAN_TEST_MAX_QUEUE] = *frame;
+ self->rx_count++;
+}
+
+static bool dequeue_rx(can_test_node_t* const self, cy_can_rx_t* const out_frame)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ TEST_ASSERT_NOT_NULL(out_frame);
+ if (self->rx_count == 0U) {
+ return false;
+ }
+ *out_frame = self->rx_queue[self->rx_head];
+ self->rx_head = (self->rx_head + 1U) % CAN_TEST_MAX_QUEUE;
+ self->rx_count--;
+ return true;
+}
+
+static void record_tx(can_test_node_t* const self,
+ const uint_least8_t iface_index,
+ const uint32_t can_id,
+ const bool fd,
+ const void* const data,
+ const uint_least8_t len)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ TEST_ASSERT_TRUE(self->tx_history_count < CAN_TEST_MAX_TX_RECORD);
+ can_test_tx_record_t* const rec = &self->tx_history[self->tx_history_count++];
+ rec->can_id = can_id;
+ rec->iface_index = iface_index;
+ rec->len = len;
+ rec->fd = fd;
+ if (len > 0U) {
+ (void)memcpy(rec->data, data, len);
+ }
+}
+
+static void propagate_tx(can_test_node_t* const self,
+ const uint_least8_t iface_index,
+ const uint32_t can_id,
+ const bool fd,
+ const void* const data,
+ const uint_least8_t len)
+{
+ if (self->bus == NULL) {
+ return;
+ }
+ for (size_t i = 0U; i < CAN_TEST_MAX_NODES; i++) {
+ can_test_node_t* const other = self->bus->nodes[i];
+ if (other == NULL) {
+ continue;
+ }
+ if ((other == self) && !self->self_loopback) {
+ continue;
+ }
+ if (iface_index >= other->iface_count) {
+ continue;
+ }
+ cy_can_rx_t frame;
+ (void)memset(&frame, 0, sizeof(frame));
+ frame.timestamp = self->now;
+ frame.can_id = can_id;
+ frame.iface_index = iface_index;
+ frame.len = len;
+ frame.fd = fd;
+ if (len > 0U) {
+ (void)memcpy(frame.data, data, len);
+ }
+ enqueue_rx(other, &frame);
+ }
+}
+
+static bool tx_common(void* const user,
+ const uint_least8_t iface_index,
+ const uint32_t can_id,
+ const bool fd,
+ const void* const data,
+ const uint_least8_t len)
+{
+ can_test_node_t* const self = (can_test_node_t*)user;
+ TEST_ASSERT_NOT_NULL(self);
+ TEST_ASSERT_TRUE(iface_index < self->iface_count);
+ if (self->tx_blocked[iface_index] > 0U) {
+ self->tx_blocked[iface_index]--;
+ return false;
+ }
+ record_tx(self, iface_index, can_id, fd, data, len);
+ if (!self->drop_tx[iface_index]) {
+ propagate_tx(self, iface_index, can_id, fd, data, len);
+ }
+ return true;
+}
+
+static bool v_tx_classic(void* const user,
+ const uint_least8_t iface_index,
+ const uint32_t can_id,
+ const void* const data,
+ const uint_least8_t len)
+{
+ can_test_node_t* const self = (can_test_node_t*)user;
+ TEST_ASSERT_NOT_NULL(self);
+ TEST_ASSERT_TRUE(len <= 8U);
+ self->tx_classic_calls++;
+ return tx_common(user, iface_index, can_id, false, data, len);
+}
+
+static bool v_tx_fd(void* const user,
+ const uint_least8_t iface_index,
+ const uint32_t can_id,
+ const void* const data,
+ const uint_least8_t len)
+{
+ can_test_node_t* const self = (can_test_node_t*)user;
+ TEST_ASSERT_NOT_NULL(self);
+ TEST_ASSERT_TRUE(len <= 64U);
+ self->tx_fd_calls++;
+ return tx_common(user, iface_index, can_id, true, data, len);
+}
+
+static bool v_rx(void* const user,
+ cy_can_rx_t* const out_frame,
+ const cy_us_t deadline,
+ const uint_least8_t tx_pending_iface_bitmap)
+{
+ can_test_node_t* const self = (can_test_node_t*)user;
+ TEST_ASSERT_NOT_NULL(self);
+ TEST_ASSERT_NOT_NULL(out_frame);
+ self->rx_calls++;
+ self->last_tx_pending_iface_bitmap = tx_pending_iface_bitmap;
+ if (dequeue_rx(self, out_frame)) {
+ if (self->now < out_frame->timestamp) {
+ self->now = out_frame->timestamp;
+ }
+ return true;
+ }
+ if (self->now <= deadline) {
+ self->now = deadline + 1;
+ }
+ return false;
+}
+
+static bool v_filter(void* const user, const size_t filter_count, const canard_filter_t* const filters)
+{
+ can_test_node_t* const self = (can_test_node_t*)user;
+ TEST_ASSERT_NOT_NULL(self);
+ if ((filter_count > 0U) && (filters == NULL)) {
+ return false;
+ }
+ self->filter_calls++;
+ self->last_filter_count = filter_count;
+ if (filter_count > 0U) {
+ TEST_ASSERT_TRUE(filter_count <= CAN_TEST_MAX_FILTERS);
+ (void)memcpy(self->last_filters, filters, filter_count * sizeof(canard_filter_t));
+ }
+ if (self->filter_failures_remaining > 0U) {
+ self->filter_failures_remaining--;
+ return false;
+ }
+ return true;
+}
+
+static cy_us_t v_now(void* const user)
+{
+ const can_test_node_t* const self = (const can_test_node_t*)user;
+ TEST_ASSERT_NOT_NULL(self);
+ return self->now;
+}
+
+static uint64_t v_random(void* const user)
+{
+ can_test_node_t* const self = (can_test_node_t*)user;
+ TEST_ASSERT_NOT_NULL(self);
+ self->random_state = (self->random_state * UINT64_C(6364136223846793005)) + UINT64_C(1);
+ return self->random_state;
+}
+
+static void* v_realloc(void* const user, void* const ptr, const size_t size)
+{
+ can_test_node_t* const self = (can_test_node_t*)user;
+ TEST_ASSERT_NOT_NULL(self);
+ return can_test_heap_realloc(&self->heap, ptr, size);
+}
+
+void can_test_heap_init(can_test_heap_t* const self)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ (void)memset(self, 0, sizeof(*self));
+ self->fail_after_n_allocations = SIZE_MAX;
+}
+
+void can_test_heap_fail_after(can_test_heap_t* const self, const size_t successful_allocations)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ self->allocation_attempts = 0U;
+ self->fail_after_n_allocations = successful_allocations;
+}
+
+void can_test_heap_allow_all(can_test_heap_t* const self)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ self->allocation_attempts = 0U;
+ self->fail_after_n_allocations = SIZE_MAX;
+}
+
+size_t can_test_heap_allocated_fragments(const can_test_heap_t* const self)
+{
+ return (self != NULL) ? self->allocated_fragments : 0U;
+}
+
+size_t can_test_heap_allocated_bytes(const can_test_heap_t* const self)
+{
+ return (self != NULL) ? self->allocated_bytes : 0U;
+}
+
+void* can_test_heap_realloc(void* const user, void* const ptr, const size_t size)
+{
+ can_test_heap_t* const self = (can_test_heap_t*)user;
+ TEST_ASSERT_NOT_NULL(self);
+ if (size == 0U) {
+ heap_free(self, ptr);
+ return NULL;
+ }
+ if (ptr == NULL) {
+ return heap_allocate(self, size);
+ }
+
+ const allocation_header_t* const old_header = header_from_payload_const(ptr);
+ TEST_ASSERT_EQUAL_UINT32(allocation_magic, old_header->magic);
+ TEST_ASSERT_TRUE(old_header->owner == self);
+ void* const out = heap_allocate(self, size);
+ if (out == NULL) {
+ return NULL;
+ }
+ (void)memcpy(out, ptr, smaller(size, old_header->size));
+ heap_free(self, ptr);
+ return out;
+}
+
+void can_test_bus_init(can_test_bus_t* const self)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ (void)memset(self, 0, sizeof(*self));
+}
+
+void can_test_node_prepare(can_test_node_t* const self,
+ can_test_bus_t* const bus,
+ const uint_least8_t iface_count,
+ const bool fd_capable,
+ const bool enable_filter_callback)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ TEST_ASSERT_TRUE((iface_count > 0U) && (iface_count <= CAN_TEST_MAX_IFACES));
+ (void)memset(self, 0, sizeof(*self));
+ self->bus = bus;
+ self->now = 1000;
+ self->random_state = UINT64_C(0x123456789ABCDEF0);
+ self->iface_count = iface_count;
+ self->fd_capable = fd_capable;
+ can_test_heap_init(&self->heap);
+ self->vtable.tx_classic = v_tx_classic;
+ self->vtable.tx_fd = fd_capable ? v_tx_fd : NULL;
+ self->vtable.rx = v_rx;
+ self->vtable.filter = enable_filter_callback ? v_filter : NULL;
+ self->vtable.now = v_now;
+ self->vtable.realloc = v_realloc;
+ self->vtable.random = v_random;
+ bus_register_node(bus, self);
+}
+
+void can_test_node_make_platform(can_test_node_t* const self, const size_t tx_queue_capacity, const size_t filter_count)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ self->platform = cy_can_new(self->iface_count, tx_queue_capacity, filter_count, &self->vtable, self);
+ TEST_ASSERT_NOT_NULL(self->platform);
+}
+
+void can_test_node_make_cy(can_test_node_t* const self, const char* const home)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ TEST_ASSERT_NOT_NULL(home);
+ TEST_ASSERT_NOT_NULL(self->platform);
+ self->cy = cy_new(self->platform, cy_str(home), (cy_str_t){ 0U, NULL });
+ TEST_ASSERT_NOT_NULL(self->cy);
+}
+
+void can_test_node_destroy(can_test_node_t* const self)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ if (self->cy != NULL) {
+ cy_destroy(self->cy);
+ self->cy = NULL;
+ }
+ if (self->platform != NULL) {
+ cy_can_destroy(self->platform);
+ self->platform = NULL;
+ }
+ bus_unregister_node(self->bus, self);
+ TEST_ASSERT_EQUAL_size_t(0U, self->rx_count);
+ TEST_ASSERT_EQUAL_size_t(0U, can_test_heap_allocated_fragments(&self->heap));
+ TEST_ASSERT_EQUAL_size_t(0U, can_test_heap_allocated_bytes(&self->heap));
+}
+
+void can_test_node_spin(can_test_node_t* const self, const cy_us_t slice)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ TEST_ASSERT_NOT_NULL(self->cy);
+ TEST_ASSERT_TRUE(slice >= 0);
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(self->cy, cy_now(self->cy) + slice));
+}
+
+void can_test_spin_pair(can_test_node_t* const a, can_test_node_t* const b, const size_t rounds, const cy_us_t slice)
+{
+ for (size_t i = 0U; i < rounds; i++) {
+ if (a != NULL) {
+ can_test_node_spin(a, slice);
+ }
+ if (b != NULL) {
+ can_test_node_spin(b, slice);
+ }
+ }
+}
+
+bool can_test_spin_pair_until_future_done(can_test_node_t* const a,
+ can_test_node_t* const b,
+ cy_future_t* const future,
+ const size_t rounds,
+ const cy_us_t slice)
+{
+ TEST_ASSERT_NOT_NULL(future);
+ for (size_t i = 0U; i < rounds; i++) {
+ if (cy_future_done(future)) {
+ return true;
+ }
+ can_test_spin_pair(a, b, 1U, slice);
+ }
+ return cy_future_done(future);
+}
+
+bool can_test_spin_pair_until_condition(can_test_node_t* const a,
+ can_test_node_t* const b,
+ bool (*const condition)(void*),
+ void* const context,
+ const size_t rounds,
+ const cy_us_t slice)
+{
+ TEST_ASSERT_NOT_NULL(condition);
+ for (size_t i = 0U; i < rounds; i++) {
+ if (condition(context)) {
+ return true;
+ }
+ can_test_spin_pair(a, b, 1U, slice);
+ }
+ return condition(context);
+}
+
+void can_test_node_reset_history(can_test_node_t* const self)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ self->tx_history_count = 0U;
+ self->tx_classic_calls = 0U;
+ self->tx_fd_calls = 0U;
+ self->last_tx_pending_iface_bitmap = 0U;
+}
+
+size_t can_test_node_count_records_on_iface(const can_test_node_t* const self, const uint_least8_t iface_index)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ size_t out = 0U;
+ for (size_t i = 0U; i < self->tx_history_count; i++) {
+ if (self->tx_history[i].iface_index == iface_index) {
+ out++;
+ }
+ }
+ return out;
+}
+
+size_t can_test_message_read_all(const cy_message_t* const message, void* const out, const size_t capacity)
+{
+ TEST_ASSERT_NOT_NULL(message);
+ TEST_ASSERT_NOT_NULL(out);
+ const size_t size = cy_message_size(message);
+ TEST_ASSERT_TRUE(size <= capacity);
+ TEST_ASSERT_EQUAL_size_t(size, cy_message_read(message, 0U, size, out));
+ return size;
+}
+
+void can_test_assert_message_equals(const cy_message_t* const message, const void* const expected, const size_t size)
+{
+ uint8_t buffer[512];
+ TEST_ASSERT_TRUE(size <= sizeof(buffer));
+ TEST_ASSERT_EQUAL_size_t(size, can_test_message_read_all(message, buffer, sizeof(buffer)));
+ TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, buffer, size);
+}
+
+void can_test_fill_payload(uint8_t* const out, const size_t size, const uint8_t seed)
+{
+ TEST_ASSERT_NOT_NULL(out);
+ for (size_t i = 0U; i < size; i++) {
+ out[i] = (uint8_t)(seed + (uint8_t)i);
+ }
+}
diff --git a/cy_can/tests/test_support.h b/cy_can/tests/test_support.h
new file mode 100644
index 0000000..07d18ff
--- /dev/null
+++ b/cy_can/tests/test_support.h
@@ -0,0 +1,114 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#define CAN_TEST_MAX_IFACES 2U
+#define CAN_TEST_MAX_NODES 4U
+#define CAN_TEST_MAX_QUEUE 512U
+#define CAN_TEST_MAX_TX_RECORD 1024U
+#define CAN_TEST_MAX_FILTERS 64U
+#define CAN_TEST_SKIP_CODE 77
+
+typedef struct
+{
+ size_t allocated_fragments;
+ size_t allocated_bytes;
+ size_t allocation_attempts;
+ size_t fail_after_n_allocations;
+} can_test_heap_t;
+
+typedef struct
+{
+ uint32_t can_id;
+ uint_least8_t iface_index;
+ uint_least8_t len;
+ bool fd;
+ uint8_t data[64];
+} can_test_tx_record_t;
+
+typedef struct can_test_node_t can_test_node_t;
+
+typedef struct
+{
+ can_test_node_t* nodes[CAN_TEST_MAX_NODES];
+} can_test_bus_t;
+
+struct can_test_node_t
+{
+ can_test_bus_t* bus;
+ can_test_heap_t heap;
+ cy_can_vtable_t vtable;
+ cy_platform_t* platform;
+ cy_t* cy;
+ cy_us_t now;
+ uint64_t random_state;
+ uint_least8_t iface_count;
+ bool fd_capable;
+ bool self_loopback;
+ bool drop_tx[CAN_TEST_MAX_IFACES];
+ size_t tx_blocked[CAN_TEST_MAX_IFACES];
+ size_t tx_classic_calls;
+ size_t tx_fd_calls;
+ size_t rx_calls;
+ uint_least8_t last_tx_pending_iface_bitmap;
+ size_t filter_calls;
+ size_t filter_failures_remaining;
+ size_t last_filter_count;
+ canard_filter_t last_filters[CAN_TEST_MAX_FILTERS];
+ size_t rx_head;
+ size_t rx_count;
+ cy_can_rx_t rx_queue[CAN_TEST_MAX_QUEUE];
+ size_t tx_history_count;
+ can_test_tx_record_t tx_history[CAN_TEST_MAX_TX_RECORD];
+};
+
+void can_test_heap_init(can_test_heap_t* self);
+void can_test_heap_fail_after(can_test_heap_t* self, size_t successful_allocations);
+void can_test_heap_allow_all(can_test_heap_t* self);
+size_t can_test_heap_allocated_fragments(const can_test_heap_t* self);
+size_t can_test_heap_allocated_bytes(const can_test_heap_t* self);
+void* can_test_heap_realloc(void* user, void* ptr, size_t size);
+
+void can_test_bus_init(can_test_bus_t* self);
+
+void can_test_node_prepare(can_test_node_t* self,
+ can_test_bus_t* bus,
+ uint_least8_t iface_count,
+ bool fd_capable,
+ bool enable_filter_callback);
+void can_test_node_make_platform(can_test_node_t* self, size_t tx_queue_capacity, size_t filter_count);
+void can_test_node_make_cy(can_test_node_t* self, const char* home);
+void can_test_node_destroy(can_test_node_t* self);
+
+void can_test_node_spin(can_test_node_t* self, cy_us_t slice);
+void can_test_spin_pair(can_test_node_t* a, can_test_node_t* b, size_t rounds, cy_us_t slice);
+bool can_test_spin_pair_until_future_done(can_test_node_t* a,
+ can_test_node_t* b,
+ cy_future_t* future,
+ size_t rounds,
+ cy_us_t slice);
+bool can_test_spin_pair_until_condition(can_test_node_t* a,
+ can_test_node_t* b,
+ bool (*condition)(void*),
+ void* context,
+ size_t rounds,
+ cy_us_t slice);
+
+void can_test_node_reset_history(can_test_node_t* self);
+size_t can_test_node_count_records_on_iface(const can_test_node_t* self, uint_least8_t iface_index);
+
+size_t can_test_message_read_all(const cy_message_t* message, void* out, size_t capacity);
+void can_test_assert_message_equals(const cy_message_t* message, const void* expected, size_t size);
+void can_test_fill_payload(uint8_t* out, size_t size, uint8_t seed);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/cy_udp_posix/CMakeLists.txt b/cy_udp_posix/CMakeLists.txt
index 6a6fc00..f37c58b 100644
--- a/cy_udp_posix/CMakeLists.txt
+++ b/cy_udp_posix/CMakeLists.txt
@@ -21,27 +21,12 @@ set_target_properties(
CXX_CPPCHECK ""
)
-# Cy UDP POSIX static library. Cy is included as well.
-function(configure_cy_udp_posix_target target)
- target_link_libraries(${target} PUBLIC udpard)
- target_include_directories(${target} SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
- target_include_directories(${target} PUBLIC ${CMAKE_SOURCE_DIR}/cy)
- set_target_properties(
- ${target}
- PROPERTIES
- COMPILE_WARNING_AS_ERROR ON
- C_STANDARD_REQUIRED ON
- C_EXTENSIONS OFF
- )
-endfunction()
+# Cy UDP POSIX transport glue static library.
+# Consumers link cy/cy.c themselves; this target only contains the transport/platform layer.
+add_library(cy_udp_posix STATIC cy_udp_posix.c udp_wrapper.c)
+target_link_libraries(cy_udp_posix PUBLIC udpard)
+target_include_directories(cy_udp_posix SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
+target_include_directories(cy_udp_posix PUBLIC ${CMAKE_SOURCE_DIR}/cy)
+set_target_properties(cy_udp_posix PROPERTIES COMPILE_WARNING_AS_ERROR ON C_STANDARD_REQUIRED ON C_EXTENSIONS OFF)
-set(cy_udp_posix_sources cy_udp_posix.c udp_wrapper.c ../cy/cy.c)
-
-# The normal production build.
-add_library(cy_udp_posix STATIC ${cy_udp_posix_sources})
-configure_cy_udp_posix_target(cy_udp_posix)
-
-# Same but with CY_CONFIG_TRACE=1 for examples and tests.
-add_library(cy_udp_posix_trace STATIC ${cy_udp_posix_sources} cy_trace_stderr.c)
-target_compile_definitions(cy_udp_posix_trace PUBLIC CY_CONFIG_TRACE=1)
-configure_cy_udp_posix_target(cy_udp_posix_trace)
+add_subdirectory(tests)
diff --git a/cy_udp_posix/cy_udp_posix.c b/cy_udp_posix/cy_udp_posix.c
index 11e196c..f3a9086 100644
--- a/cy_udp_posix/cy_udp_posix.c
+++ b/cy_udp_posix/cy_udp_posix.c
@@ -30,6 +30,12 @@
#include
#include
+#if __STDC_VERSION__ < 201112L
+#define static_assert(x, ...) typedef char _sa_gl(_sa_, __LINE__)[(x) ? 1 : -1]
+#define _sa_gl(a, b) _sa_gl2(a, b)
+#define _sa_gl2(a, b) a##b
+#endif
+
#if __STDC_VERSION__ >= 201112L
#include
#else
@@ -139,14 +145,15 @@ static uint64_t prng64(uint64_t* const state, const uint64_t local_uid)
return rapidhash_withSeed(state, sizeof(uint64_t), local_uid);
}
+static_assert(offsetof(udpard_bytes_scattered_t, bytes.size) == offsetof(cy_bytes_t, size), "");
+static_assert(offsetof(udpard_bytes_scattered_t, bytes.data) == offsetof(cy_bytes_t, data), "");
+static_assert(offsetof(udpard_bytes_scattered_t, next) == offsetof(cy_bytes_t, next), "");
+
static udpard_bytes_scattered_t cy_bytes_to_udpard_bytes(const cy_bytes_t message)
{
// Instead of converting the entire payload chain, we can just statically validate that the memory layouts
// are compatible. We cannot make neither libudpard nor cy depend on each other, but perhaps in the future
// we could introduce a tiny single header providing some common definitions for both, to eliminate such aliasing.
- static_assert(offsetof(udpard_bytes_scattered_t, bytes.size) == offsetof(cy_bytes_t, size), "");
- static_assert(offsetof(udpard_bytes_scattered_t, bytes.data) == offsetof(cy_bytes_t, data), "");
- static_assert(offsetof(udpard_bytes_scattered_t, next) == offsetof(cy_bytes_t, next), "");
return (udpard_bytes_scattered_t){ .bytes = { .size = message.size, .data = message.data },
.next = (const udpard_bytes_scattered_t*)message.next };
}
@@ -326,6 +333,9 @@ struct subject_reader_t
subject_reader_t* next;
};
+static_assert(sizeof(((subject_reader_t*)0)->history) / sizeof(((subject_reader_t*)0)->history[0]) == 2, "");
+static_assert(sizeof(((udpard_rx_transfer_t*)0)->remote.endpoints) <= sizeof(((cy_lane_t*)0)->ctx), "");
+
// We use the same handler for both subject and unicast messages, since they both use the same ingestion callback.
// The difference here is that unicast messages have no associated reader instance.
static void v_on_msg(udpard_rx_t* const rx, udpard_rx_port_t* const port, const udpard_rx_transfer_t tr)
@@ -335,7 +345,6 @@ static void v_on_msg(udpard_rx_t* const rx, udpard_rx_port_t* const port, const
.content = make_message(owner, tr.payload_size_stored, tr.payload) };
if (msg.content != NULL) {
cy_lane_t lane = { .id = tr.remote.uid, .prio = (cy_prio_t)tr.priority };
- static_assert(sizeof(tr.remote.endpoints) <= sizeof(lane.ctx), "");
memcpy(&lane.ctx, tr.remote.endpoints, sizeof(tr.remote.endpoints));
const uint32_t* const subject_id = (port->user == NULL) ? NULL // user is NULL for unicast
: &((subject_reader_t*)port->user)->base.subject_id;
@@ -350,7 +359,6 @@ static void v_on_msg_stateless(udpard_rx_t* const rx, udpard_rx_port_t* const po
{
cy_udp_posix_t* const owner = rx->user;
subject_reader_t* const self = port->user;
- static_assert(sizeof(self->history) / sizeof(self->history[0]) == 2, "");
// In the stateless mode, libudpard does not bother deduplicating messages. Gossips/scouts are dup-tolerant,
// so we could just pass all messages as-is and it will work fine, but it would waste CPU cycles because each
// message requires some log-time index lookups.
@@ -490,6 +498,9 @@ static void v_subject_reader_tombstone(cy_platform_t* const platform, cy_subject
assert(!self->tombstone);
self->tombstone = true;
// Close sockets now to stop further reads while we defer the final teardown.
+ // This also makes same-subject recreation safe: unlike libcanard, libudpard has no global uniqueness rule for
+ // subject subscriptions. The old port instance becomes inert as soon as its sockets are closed because only open
+ // sockets are polled, so a new reader may be constructed for the same subject-ID before the old port is freed.
for (uint_fast8_t i = 0; i < CY_UDP_POSIX_IFACE_COUNT_MAX; i++) {
udp_wrapper_close(&self->sock[i]);
}
@@ -508,6 +519,8 @@ static void v_subject_reader_extent_set(cy_platform_t* const base,
// ---------------------------------------------------------------------------------------------------------------------
// UNICAST
+static_assert(sizeof(udpard_udpip_ep_t[UDPARD_IFACE_COUNT_MAX]) <= sizeof(((cy_lane_t*)0)->ctx), "");
+
static cy_err_t v_unicast_send(cy_platform_t* const base,
const cy_lane_t* const lane,
const cy_us_t deadline,
@@ -516,7 +529,6 @@ static cy_err_t v_unicast_send(cy_platform_t* const base,
cy_udp_posix_t* const owner = (cy_udp_posix_t*)base;
// Unbox the unicast context.
udpard_udpip_ep_t endpoints[UDPARD_IFACE_COUNT_MAX] = { 0 };
- static_assert(sizeof(endpoints) <= sizeof(lane->ctx), "");
memcpy(endpoints, &lane->ctx, sizeof(udpard_udpip_ep_t) * UDPARD_IFACE_COUNT_MAX);
// Push the message.
// We may need better error reporting in libudpard, this is a bit unwieldy.
@@ -655,6 +667,8 @@ static cy_err_t spin_once_until(cy_udp_posix_t* const self, const cy_us_t deadli
for (subject_reader_t* rd_iter = self->reader_head; rd_iter != NULL;) {
subject_reader_t* const next = rd_iter->next;
if (rd_iter->tombstone) {
+ // Deferred teardown exists only to keep list traversal/reentrancy simple; the reader was already rendered
+ // inactive when it was tombstoned by closing its sockets.
subject_reader_destroy(self, rd_iter);
} else {
for (uint_fast8_t i = 0; i < CY_UDP_POSIX_IFACE_COUNT_MAX; i++) {
diff --git a/cy_udp_posix/cy_udp_posix.h b/cy_udp_posix/cy_udp_posix.h
index dbfe160..01b6edd 100644
--- a/cy_udp_posix/cy_udp_posix.h
+++ b/cy_udp_posix/cy_udp_posix.h
@@ -52,7 +52,7 @@ typedef struct cy_udp_posix_stats_t
} cy_udp_posix_stats_t;
/// The default factory that automatically assigns the node parameters that fit most applications:
-/// - A semi-random EUI-64: 20 most significant bits are host-specific, the lower 44 bits are random.
+/// - A semi-random EUI-64.
/// - The local interfaces are chosen per the defaults configured on the local system.
/// - The TX queue capacity is set to a reasonable large value.
cy_platform_t* cy_udp_posix_new(void);
diff --git a/cy_udp_posix/eui64.h b/cy_udp_posix/eui64.h
index 63d5620..55bc105 100644
--- a/cy_udp_posix/eui64.h
+++ b/cy_udp_posix/eui64.h
@@ -2,11 +2,10 @@
#include "rapidhash.h"
#include
-#include
#include
-#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
-#include
-#endif
+#include
+#include
+#include
/// Generates a new random locally administered unicast EUI-64 identifier suitable for use as a 64-bit Cyphal node-ID.
/// Returns zero on failure, which is not a valid EUI-64.
@@ -19,41 +18,16 @@ static inline uint64_t eui64_semirandom(void)
{
uint32_t host_20 = 0; // 2 of these bits are used for EUI-64 flags, 18 bits remain. These are first 5 hex digits.
uint64_t rand_44 = 0; // The remaining 44 random bits, which are the last 11 hex digits.
-#ifdef __linux__
{
- const int fd = open("/etc/machine-id", O_RDONLY);
- if (fd < 0) {
- return 0;
- }
- char buf[32];
- const ssize_t n = read(fd, buf, sizeof(buf));
- close(fd);
- if (n < 32) {
+ struct utsname info = { 0 };
+ if (uname(&info) != 0) {
return 0;
}
- host_20 = (uint32_t)(rapidhash(buf, (size_t)n) & 0xFFFFFU);
- }
- {
- const int fd = open("/dev/urandom", O_RDONLY);
- if (fd < 0) {
- return 0;
- }
- if (read(fd, &rand_44, sizeof(rand_44)) != sizeof(rand_44)) {
- close(fd);
- return 0;
- }
- close(fd);
- }
-#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
- {
- // Use sysctl kern.hostid for host identifier
- int mib[2] = { CTL_KERN, KERN_HOSTID };
- uint32_t hostid = 0;
- size_t len = sizeof(hostid);
- if (sysctl(mib, 2, &hostid, &len, NULL, 0) != 0) {
+ const size_t length = strlen(info.nodename);
+ if (length == 0U) {
return 0;
}
- host_20 = hostid & 0xFFFFFU;
+ host_20 = (uint32_t)(rapidhash(info.nodename, length) & 0xFFFFFU);
}
{
const int fd = open("/dev/urandom", O_RDONLY);
@@ -66,9 +40,6 @@ static inline uint64_t eui64_semirandom(void)
}
close(fd);
}
-#else
-#error "eui64_semirandom() is not implemented for this platform yet."
-#endif
uint64_t out = (((uint64_t)host_20) << 44U) | (rand_44 & ((1ULL << 44U) - 1U));
out &= ~(1ULL << 56U); // clear bit I/G (unicast)
out |= (1ULL << 57U); // set bit U/L (locally administered)
diff --git a/cy_udp_posix/tests/CMakeLists.txt b/cy_udp_posix/tests/CMakeLists.txt
new file mode 100644
index 0000000..d0f73b6
--- /dev/null
+++ b/cy_udp_posix/tests/CMakeLists.txt
@@ -0,0 +1,94 @@
+# Copyright (c) Pavel Kirienko
+
+cmake_minimum_required(VERSION 3.24)
+
+set(cy_udp_posix_tests_root ${CMAKE_CURRENT_SOURCE_DIR})
+set(unity_root "${CMAKE_SOURCE_DIR}/lib/unity")
+set(udpard_root "${CMAKE_SOURCE_DIR}/lib/libudpard/libudpard")
+
+add_library(cy_udp_posix_tests_unity STATIC "${unity_root}/src/unity.c")
+target_include_directories(cy_udp_posix_tests_unity SYSTEM PUBLIC "${unity_root}/src")
+target_compile_definitions(
+ cy_udp_posix_tests_unity
+ PUBLIC
+ UNITY_INCLUDE_DOUBLE=1
+ UNITY_OUTPUT_COLOR=1
+ UNITY_SUPPORT_64=1
+)
+target_compile_options(
+ cy_udp_posix_tests_unity
+ PRIVATE
+ -m32
+ -Wno-sign-conversion
+ -Wno-conversion
+ -Wno-switch-enum
+ -Wno-float-equal
+ -Wno-double-promotion
+ -Wno-missing-declarations
+)
+set_target_properties(
+ cy_udp_posix_tests_unity
+ PROPERTIES
+ C_STANDARD 99
+ C_STANDARD_REQUIRED ON
+ C_EXTENSIONS OFF
+ COMPILE_WARNING_AS_ERROR OFF
+ C_CLANG_TIDY ""
+ CXX_CLANG_TIDY ""
+ C_CPPCHECK ""
+ CXX_CPPCHECK ""
+)
+
+add_library(cy_udp_posix_tests_udpard STATIC "${udpard_root}/udpard.c")
+target_include_directories(cy_udp_posix_tests_udpard SYSTEM PUBLIC "${udpard_root}")
+target_compile_options(cy_udp_posix_tests_udpard PRIVATE -m32 -Wno-cast-align)
+set_target_properties(
+ cy_udp_posix_tests_udpard
+ PROPERTIES
+ C_STANDARD 99
+ C_STANDARD_REQUIRED ON
+ C_EXTENSIONS OFF
+ COMPILE_WARNING_AS_ERROR OFF
+ C_CLANG_TIDY ""
+ CXX_CLANG_TIDY ""
+ C_CPPCHECK ""
+ CXX_CPPCHECK ""
+)
+
+function(cy_udp_posix_add_test name)
+ add_executable(
+ ${name}
+ ${ARGN}
+ ${CMAKE_SOURCE_DIR}/cy/cy.c
+ ${CMAKE_SOURCE_DIR}/cy_udp_posix/cy_udp_posix.c
+ ${CMAKE_SOURCE_DIR}/cy_udp_posix/udp_wrapper.c
+ ${cy_udp_posix_tests_root}/test_support.c
+ )
+ target_include_directories(
+ ${name}
+ PRIVATE
+ ${CMAKE_SOURCE_DIR}/cy
+ ${CMAKE_SOURCE_DIR}/cy_udp_posix
+ ${udpard_root}
+ ${cy_udp_posix_tests_root}
+ )
+ target_link_libraries(${name} PRIVATE cy_udp_posix_tests_unity cy_udp_posix_tests_udpard)
+ target_compile_definitions(${name} PRIVATE _POSIX_C_SOURCE=200809L)
+ target_compile_options(${name} PRIVATE -m32)
+ target_link_options(${name} PRIVATE -m32)
+ set_target_properties(
+ ${name}
+ PROPERTIES
+ C_STANDARD 99
+ C_STANDARD_REQUIRED ON
+ C_EXTENSIONS OFF
+ COMPILE_WARNING_AS_ERROR ON
+ )
+ add_test(NAME run_${name} COMMAND ${name})
+endfunction()
+
+cy_udp_posix_add_test(test_api_udp_posix_basic test_api_udp_posix_basic.c)
+cy_udp_posix_add_test(test_api_udp_posix_pubsub test_api_udp_posix_pubsub.c)
+cy_udp_posix_add_test(test_api_udp_posix_rpc_lifecycle test_api_udp_posix_rpc_lifecycle.c)
+cy_udp_posix_add_test(test_api_udp_posix_default_ctor test_api_udp_posix_default_ctor.c)
+set_tests_properties(run_test_api_udp_posix_default_ctor PROPERTIES SKIP_RETURN_CODE 77)
diff --git a/cy_udp_posix/tests/test_api_udp_posix_basic.c b/cy_udp_posix/tests/test_api_udp_posix_basic.c
new file mode 100644
index 0000000..4d4fed3
--- /dev/null
+++ b/cy_udp_posix/tests/test_api_udp_posix_basic.c
@@ -0,0 +1,93 @@
+#include "test_support.h"
+
+#include
+
+#include
+#include
+
+static void test_api_udp_posix_basic_parse_iface_address(void)
+{
+ TEST_ASSERT_EQUAL_HEX32(0x7F000001U, cy_udp_parse_iface_address("127.0.0.1"));
+ TEST_ASSERT_EQUAL_HEX32(0U, cy_udp_parse_iface_address("127.0.0"));
+ TEST_ASSERT_EQUAL_HEX32(0U, cy_udp_parse_iface_address("not_an_ip"));
+ TEST_ASSERT_EQUAL_HEX32(0U, cy_udp_parse_iface_address(NULL));
+}
+
+static void test_api_udp_posix_basic_manual_constructor_and_home(void)
+{
+ static const uint32_t no_ifaces[CY_UDP_POSIX_IFACE_COUNT_MAX] = { 0U, 0U, 0U };
+ static const uint32_t loopback_iface[CY_UDP_POSIX_IFACE_COUNT_MAX] = { 0x7F000001U, 0U, 0U };
+ cy_platform_t* platform = NULL;
+ cy_str_t home;
+ cy_udp_posix_stats_t stats;
+
+ TEST_ASSERT_NULL(cy_udp_posix_new_manual(UINT64_C(0x0123456789ABCDEF), no_ifaces, 16U));
+
+ platform = cy_udp_posix_new_manual(UINT64_C(0x0123456789ABCDEF), loopback_iface, 64U);
+ TEST_ASSERT_NOT_NULL(platform);
+
+ home = cy_udp_posix_home(platform, NULL);
+ TEST_ASSERT_EQUAL_size_t(16U, home.len);
+ TEST_ASSERT_EQUAL_STRING_LEN("0123456789abcdef", home.str, 16);
+
+ home = cy_udp_posix_home(platform, "udp");
+ TEST_ASSERT_EQUAL_size_t(strlen("udp/0123456789abcdef"), home.len);
+ TEST_ASSERT_EQUAL_STRING_LEN("udp/0123456789abcdef", home.str, home.len);
+
+ stats = cy_udp_posix_stats(platform);
+ TEST_ASSERT_EQUAL_size_t(0U, stats.subject_writer_count);
+ TEST_ASSERT_EQUAL_size_t(0U, stats.subject_reader_count);
+ TEST_ASSERT_EQUAL_size_t(0U, stats.mem.allocated_fragments);
+ TEST_ASSERT_EQUAL_size_t(0U, stats.mem.allocated_bytes);
+ TEST_ASSERT_EQUAL_UINT64(0U, stats.mem.oom_count);
+ TEST_ASSERT_EQUAL_UINT64(0U, stats.message_loss_count);
+ for (size_t i = 0U; i < CY_UDP_POSIX_IFACE_COUNT_MAX; i++) {
+ TEST_ASSERT_EQUAL_UINT64(0U, stats.sock_tx.error_count[i]);
+ TEST_ASSERT_EQUAL_UINT64(0U, stats.sock_rx.error_count[i]);
+ }
+ TEST_ASSERT_EQUAL_INT64(0, stats.sock_tx.last_error_at);
+ TEST_ASSERT_EQUAL_INT64(0, stats.sock_rx.last_error_at);
+
+ cy_udp_posix_destroy(platform);
+}
+
+static void test_api_udp_posix_basic_namespace_and_time(void)
+{
+ const char* const old_env = getenv("CYPHAL_NAMESPACE");
+ char* const backup = (old_env != NULL) ? strdup(old_env) : NULL;
+ const cy_us_t t0 = cy_udp_posix_now();
+ const cy_us_t t1 = cy_udp_posix_now();
+
+ TEST_ASSERT_TRUE(t1 >= t0);
+
+ TEST_ASSERT_EQUAL_INT(0, setenv("CYPHAL_NAMESPACE", "ns/test", 1));
+ {
+ const cy_str_t ns = cy_udp_posix_namespace();
+ TEST_ASSERT_EQUAL_size_t(strlen("ns/test"), ns.len);
+ TEST_ASSERT_EQUAL_STRING_LEN("ns/test", ns.str, ns.len);
+ }
+
+ TEST_ASSERT_EQUAL_INT(0, unsetenv("CYPHAL_NAMESPACE"));
+ {
+ const cy_str_t ns = cy_udp_posix_namespace();
+ TEST_ASSERT_EQUAL_size_t(0U, ns.len);
+ }
+
+ if (backup != NULL) {
+ TEST_ASSERT_EQUAL_INT(0, setenv("CYPHAL_NAMESPACE", backup, 1));
+ free(backup);
+ }
+}
+
+void setUp(void) {}
+
+void tearDown(void) {}
+
+int main(void)
+{
+ UNITY_BEGIN();
+ RUN_TEST(test_api_udp_posix_basic_parse_iface_address);
+ RUN_TEST(test_api_udp_posix_basic_manual_constructor_and_home);
+ RUN_TEST(test_api_udp_posix_basic_namespace_and_time);
+ return UNITY_END();
+}
diff --git a/cy_udp_posix/tests/test_api_udp_posix_default_ctor.c b/cy_udp_posix/tests/test_api_udp_posix_default_ctor.c
new file mode 100644
index 0000000..ca06eab
--- /dev/null
+++ b/cy_udp_posix/tests/test_api_udp_posix_default_ctor.c
@@ -0,0 +1,52 @@
+#include "test_support.h"
+
+#include
+
+#include
+
+static void test_api_udp_posix_default_ctor_hostname_hash_and_flags(void)
+{
+ cy_platform_t* const platform_a = cy_udp_posix_new();
+ cy_platform_t* const platform_b = cy_udp_posix_new();
+ const uint32_t expected = udp_test_expected_eui64_prefix();
+
+ TEST_ASSERT_NOT_NULL(platform_a);
+ TEST_ASSERT_NOT_NULL(platform_b);
+
+ {
+ const uint64_t uid_a = udp_test_parse_uid_from_home(cy_udp_posix_home(platform_a, NULL));
+ const uint64_t uid_b = udp_test_parse_uid_from_home(cy_udp_posix_home(platform_b, NULL));
+
+ TEST_ASSERT_EQUAL_HEX32(expected, (uint32_t)(uid_a >> 44U));
+ TEST_ASSERT_EQUAL_HEX32(expected, (uint32_t)(uid_b >> 44U));
+ TEST_ASSERT_TRUE((uid_a & (1ULL << 57U)) != 0U);
+ TEST_ASSERT_TRUE((uid_b & (1ULL << 57U)) != 0U);
+ TEST_ASSERT_TRUE((uid_a & (1ULL << 56U)) == 0U);
+ TEST_ASSERT_TRUE((uid_b & (1ULL << 56U)) == 0U);
+ }
+
+ cy_udp_posix_destroy(platform_a);
+ cy_udp_posix_destroy(platform_b);
+}
+
+void setUp(void) {}
+
+void tearDown(void) {}
+
+int main(void)
+{
+ cy_platform_t* const probe_a = cy_udp_posix_new();
+ cy_platform_t* const probe_b = cy_udp_posix_new();
+ if ((probe_a == NULL) || (probe_b == NULL)) {
+ cy_udp_posix_destroy(probe_a);
+ cy_udp_posix_destroy(probe_b);
+ (void)fprintf(stderr, "cy_udp_posix_new() unavailable on this host, skipping default-constructor smoke test\n");
+ return UDP_TEST_SKIP_CODE;
+ }
+ cy_udp_posix_destroy(probe_a);
+ cy_udp_posix_destroy(probe_b);
+
+ UNITY_BEGIN();
+ RUN_TEST(test_api_udp_posix_default_ctor_hostname_hash_and_flags);
+ return UNITY_END();
+}
diff --git a/cy_udp_posix/tests/test_api_udp_posix_pubsub.c b/cy_udp_posix/tests/test_api_udp_posix_pubsub.c
new file mode 100644
index 0000000..93cc226
--- /dev/null
+++ b/cy_udp_posix/tests/test_api_udp_posix_pubsub.c
@@ -0,0 +1,136 @@
+#include "test_support.h"
+
+#include
+
+#include
+#include
+#include
+
+static const cy_us_t spin_slice_us = (cy_us_t)10000;
+
+typedef struct
+{
+ cy_future_t* future;
+} future_done_context_t;
+
+typedef struct
+{
+ cy_future_t* future;
+ uint64_t minimum_count;
+} arrival_count_context_t;
+
+static bool is_future_done(void* const context)
+{
+ const future_done_context_t* const ctx = (const future_done_context_t*)context;
+ return (ctx != NULL) && cy_future_done(ctx->future);
+}
+
+static bool has_arrival_count(void* const context)
+{
+ const arrival_count_context_t* const ctx = (const arrival_count_context_t*)context;
+ return (ctx != NULL) && (cy_arrival_count(ctx->future) >= ctx->minimum_count);
+}
+
+static void test_api_udp_posix_pubsub_best_effort_and_stats(void)
+{
+ static const uint8_t payload[] = { 0x11U, 0x22U, 0x33U, 0x44U };
+ udp_test_node_t a;
+ udp_test_node_t b;
+ size_t writer_baseline = 0U;
+ size_t reader_baseline = 0U;
+
+ udp_test_node_init_manual(&a, UINT64_C(0xA000000000000001), "udp_pub_a", 256U);
+ udp_test_node_init_manual(&b, UINT64_C(0xA000000000000002), "udp_pub_b", 256U);
+ udp_test_spin_pair(&a, &b, 4U, spin_slice_us);
+ writer_baseline = cy_udp_posix_stats(a.platform).subject_writer_count;
+ reader_baseline = cy_udp_posix_stats(b.platform).subject_reader_count;
+
+ cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("udp/basic"));
+ cy_future_t* const sub = cy_subscribe(b.cy, cy_str("udp/basic"), 64U);
+ TEST_ASSERT_NOT_NULL(pub);
+ TEST_ASSERT_NOT_NULL(sub);
+
+ udp_test_spin_pair(&a, &b, 8U, spin_slice_us);
+ TEST_ASSERT_EQUAL_size_t(writer_baseline + 1U, cy_udp_posix_stats(a.platform).subject_writer_count);
+ TEST_ASSERT_TRUE(cy_udp_posix_stats(b.platform).subject_reader_count >= (reader_baseline + 1U));
+
+ TEST_ASSERT_EQUAL_INT(CY_OK,
+ cy_publish(pub, cy_now(a.cy) + (50 * spin_slice_us), (cy_bytes_t){ 4U, payload, NULL }));
+
+ {
+ arrival_count_context_t ctx = { .future = sub, .minimum_count = 1U };
+ TEST_ASSERT_TRUE(udp_test_spin_pair_until_condition(&a, &b, has_arrival_count, &ctx, 100U, spin_slice_us));
+ }
+
+ {
+ const cy_arrival_t arrival = cy_arrival_move(sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ udp_test_assert_message_equals(arrival.message.content, payload, sizeof(payload));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ udp_test_spin_pair(&a, &b, 6U, spin_slice_us);
+
+ udp_test_node_deinit(&a);
+ udp_test_node_deinit(&b);
+}
+
+static void test_api_udp_posix_pubsub_large_reliable_delivery(void)
+{
+ uint8_t payload[1024];
+ udp_test_node_t a;
+ udp_test_node_t b;
+ future_done_context_t delivery_ctx;
+
+ udp_test_fill_payload(payload, sizeof(payload), 0x40U);
+ udp_test_node_init_manual(&a, UINT64_C(0xA000000000000011), "udp_rel_a", 512U);
+ udp_test_node_init_manual(&b, UINT64_C(0xA000000000000012), "udp_rel_b", 512U);
+
+ cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("udp/large"));
+ cy_future_t* const sub = cy_subscribe(b.cy, cy_str("udp/large"), sizeof(payload));
+ cy_future_t* delivery = NULL;
+ TEST_ASSERT_NOT_NULL(pub);
+ TEST_ASSERT_NOT_NULL(sub);
+
+ udp_test_spin_pair(&a, &b, 8U, spin_slice_us);
+ delivery =
+ cy_publish_reliable(pub, cy_now(a.cy) + (200 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL });
+ TEST_ASSERT_NOT_NULL(delivery);
+ delivery_ctx.future = delivery;
+ TEST_ASSERT_TRUE(udp_test_spin_pair_until_condition(&a, &b, is_future_done, &delivery_ctx, 200U, spin_slice_us));
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(delivery));
+ TEST_ASSERT_TRUE(cy_arrival_count(sub) > 0U);
+
+ {
+ uint8_t received[sizeof(payload)];
+ const cy_arrival_t arrival = cy_arrival_move(sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ TEST_ASSERT_EQUAL_size_t(sizeof(payload), cy_message_size(arrival.message.content));
+ TEST_ASSERT_EQUAL_size_t(sizeof(payload),
+ udp_test_message_read_all(arrival.message.content, received, sizeof(received)));
+ TEST_ASSERT_EQUAL_UINT8_ARRAY(payload, received, sizeof(payload));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ cy_future_destroy(delivery);
+ cy_unadvertise(pub);
+ cy_future_destroy(sub);
+ udp_test_spin_pair(&a, &b, 6U, spin_slice_us);
+
+ udp_test_node_deinit(&a);
+ udp_test_node_deinit(&b);
+}
+
+void setUp(void) {}
+
+void tearDown(void) {}
+
+int main(void)
+{
+ UNITY_BEGIN();
+ RUN_TEST(test_api_udp_posix_pubsub_best_effort_and_stats);
+ RUN_TEST(test_api_udp_posix_pubsub_large_reliable_delivery);
+ return UNITY_END();
+}
diff --git a/cy_udp_posix/tests/test_api_udp_posix_rpc_lifecycle.c b/cy_udp_posix/tests/test_api_udp_posix_rpc_lifecycle.c
new file mode 100644
index 0000000..63e88fa
--- /dev/null
+++ b/cy_udp_posix/tests/test_api_udp_posix_rpc_lifecycle.c
@@ -0,0 +1,161 @@
+#include "test_support.h"
+
+#include
+
+#include
+#include
+#include
+
+static const cy_us_t spin_slice_us = (cy_us_t)10000;
+
+typedef struct
+{
+ cy_future_t* future;
+} future_done_context_t;
+
+typedef struct
+{
+ cy_future_t* future;
+ uint64_t minimum_count;
+} response_count_context_t;
+
+static bool is_future_done(void* const context)
+{
+ const future_done_context_t* const ctx = (const future_done_context_t*)context;
+ return (ctx != NULL) && cy_future_done(ctx->future);
+}
+
+static bool has_response_count(void* const context)
+{
+ const response_count_context_t* const ctx = (const response_count_context_t*)context;
+ return (ctx != NULL) && (cy_response_count(ctx->future) >= ctx->minimum_count);
+}
+
+static void test_api_udp_posix_rpc_request_response_reliable(void)
+{
+ static const uint8_t request_payload[] = { 0x10U, 0x20U, 0x30U };
+ static const uint8_t response_payload[] = { 0xAAU, 0xBBU, 0xCCU, 0xDDU };
+ udp_test_node_t client_node;
+ udp_test_node_t server_node;
+ cy_future_t* response_delivery = NULL;
+
+ udp_test_node_init_manual(&client_node, UINT64_C(0xB000000000000001), "udp_cli", 256U);
+ udp_test_node_init_manual(&server_node, UINT64_C(0xB000000000000002), "udp_srv", 256U);
+
+ cy_publisher_t* const client_pub = cy_advertise_client(client_node.cy, cy_str("udp/rpc"), 64U);
+ cy_future_t* const server_sub = cy_subscribe(server_node.cy, cy_str("udp/rpc"), 64U);
+ cy_future_t* request = NULL;
+ TEST_ASSERT_NOT_NULL(client_pub);
+ TEST_ASSERT_NOT_NULL(server_sub);
+
+ udp_test_spin_pair(&client_node, &server_node, 8U, spin_slice_us);
+ request = cy_request(client_pub,
+ cy_now(client_node.cy) + (200 * spin_slice_us),
+ 50 * spin_slice_us,
+ (cy_bytes_t){ sizeof(request_payload), request_payload, NULL });
+ TEST_ASSERT_NOT_NULL(request);
+ {
+ future_done_context_t ctx = { .future = server_sub };
+ TEST_ASSERT_TRUE(
+ udp_test_spin_pair_until_condition(&client_node, &server_node, is_future_done, &ctx, 120U, spin_slice_us));
+ }
+
+ {
+ const cy_arrival_t arrival = cy_arrival_move(server_sub);
+ cy_breadcrumb_t breadcrumb = arrival.breadcrumb;
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ udp_test_assert_message_equals(arrival.message.content, request_payload, sizeof(request_payload));
+ response_delivery = cy_respond_reliable(&breadcrumb,
+ cy_now(server_node.cy) + (200 * spin_slice_us),
+ (cy_bytes_t){ sizeof(response_payload), response_payload, NULL });
+ TEST_ASSERT_NOT_NULL(response_delivery);
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ {
+ response_count_context_t ctx = { .future = request, .minimum_count = 1U };
+ TEST_ASSERT_TRUE(udp_test_spin_pair_until_condition(
+ &client_node, &server_node, has_response_count, &ctx, 200U, spin_slice_us));
+ }
+ TEST_ASSERT_TRUE(cy_future_done(request));
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(request));
+ {
+ future_done_context_t ctx = { .future = response_delivery };
+ TEST_ASSERT_TRUE(
+ udp_test_spin_pair_until_condition(&client_node, &server_node, is_future_done, &ctx, 120U, spin_slice_us));
+ }
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(response_delivery));
+
+ {
+ const cy_response_t response = cy_response_move(request);
+ TEST_ASSERT_NOT_NULL(response.message.content);
+ TEST_ASSERT_EQUAL_UINT64(server_node.uid, response.remote_id);
+ udp_test_assert_message_equals(response.message.content, response_payload, sizeof(response_payload));
+ cy_message_refcount_dec(response.message.content);
+ }
+
+ cy_future_destroy(response_delivery);
+ cy_future_destroy(request);
+ cy_unadvertise(client_pub);
+ cy_future_destroy(server_sub);
+ udp_test_spin_pair(&client_node, &server_node, 6U, spin_slice_us);
+
+ udp_test_node_deinit(&client_node);
+ udp_test_node_deinit(&server_node);
+}
+
+static void test_api_udp_posix_lifecycle_subscriber_recreation_before_spin(void)
+{
+ static const uint8_t payload[] = { 0x51U, 0x52U, 0x53U, 0x54U };
+ udp_test_node_t a;
+ udp_test_node_t b;
+
+ udp_test_node_init_manual(&a, UINT64_C(0xB000000000000011), "udp_life_a", 256U);
+ udp_test_node_init_manual(&b, UINT64_C(0xB000000000000012), "udp_life_b", 256U);
+
+ cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("udp/revive"));
+ cy_future_t* old_sub = cy_subscribe(b.cy, cy_str("udp/revive"), 64U);
+ cy_future_t* new_sub = NULL;
+ TEST_ASSERT_NOT_NULL(pub);
+ TEST_ASSERT_NOT_NULL(old_sub);
+
+ cy_future_destroy(old_sub);
+ old_sub = NULL;
+ new_sub = cy_subscribe(b.cy, cy_str("udp/revive"), 64U);
+ TEST_ASSERT_NOT_NULL(new_sub);
+
+ udp_test_spin_pair(&a, &b, 8U, spin_slice_us);
+ TEST_ASSERT_EQUAL_INT(CY_OK,
+ cy_publish(pub, cy_now(a.cy) + (50 * spin_slice_us), (cy_bytes_t){ 4U, payload, NULL }));
+
+ {
+ future_done_context_t ctx = { .future = new_sub };
+ TEST_ASSERT_TRUE(udp_test_spin_pair_until_condition(&a, &b, is_future_done, &ctx, 120U, spin_slice_us));
+ }
+
+ {
+ const cy_arrival_t arrival = cy_arrival_move(new_sub);
+ TEST_ASSERT_NOT_NULL(arrival.message.content);
+ udp_test_assert_message_equals(arrival.message.content, payload, sizeof(payload));
+ cy_message_refcount_dec(arrival.message.content);
+ }
+
+ cy_unadvertise(pub);
+ cy_future_destroy(new_sub);
+ udp_test_spin_pair(&a, &b, 6U, spin_slice_us);
+
+ udp_test_node_deinit(&a);
+ udp_test_node_deinit(&b);
+}
+
+void setUp(void) {}
+
+void tearDown(void) {}
+
+int main(void)
+{
+ UNITY_BEGIN();
+ RUN_TEST(test_api_udp_posix_rpc_request_response_reliable);
+ RUN_TEST(test_api_udp_posix_lifecycle_subscriber_recreation_before_spin);
+ return UNITY_END();
+}
diff --git a/cy_udp_posix/tests/test_support.c b/cy_udp_posix/tests/test_support.c
new file mode 100644
index 0000000..5719d1f
--- /dev/null
+++ b/cy_udp_posix/tests/test_support.c
@@ -0,0 +1,134 @@
+#include "test_support.h"
+
+#include
+
+#define RAPIDHASH_COMPACT
+#include
+
+#include
+#include
+#include
+
+void udp_test_node_init_manual(udp_test_node_t* const self,
+ const uint64_t uid,
+ const char* const home_prefix,
+ const size_t tx_queue_capacity)
+{
+ static const uint32_t iface_address[CY_UDP_POSIX_IFACE_COUNT_MAX] = { 0x7F000001U, 0U, 0U };
+ TEST_ASSERT_NOT_NULL(self);
+ (void)memset(self, 0, sizeof(*self));
+ self->uid = uid;
+ self->platform = cy_udp_posix_new_manual(uid, iface_address, tx_queue_capacity);
+ TEST_ASSERT_NOT_NULL(self->platform);
+ self->cy = cy_new(self->platform, cy_udp_posix_home(self->platform, home_prefix), cy_str(""));
+ TEST_ASSERT_NOT_NULL(self->cy);
+}
+
+void udp_test_node_deinit(udp_test_node_t* const self)
+{
+ TEST_ASSERT_NOT_NULL(self);
+ if (self->cy != NULL) {
+ cy_destroy(self->cy);
+ self->cy = NULL;
+ }
+ if (self->platform != NULL) {
+ cy_udp_posix_destroy(self->platform);
+ self->platform = NULL;
+ }
+}
+
+void udp_test_spin(udp_test_node_t* const node, const cy_us_t slice)
+{
+ TEST_ASSERT_NOT_NULL(node);
+ TEST_ASSERT_NOT_NULL(node->cy);
+ TEST_ASSERT_TRUE(slice >= 0);
+ TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(node->cy, cy_now(node->cy) + slice));
+}
+
+void udp_test_spin_pair(udp_test_node_t* const a, udp_test_node_t* const b, const size_t rounds, const cy_us_t slice)
+{
+ for (size_t i = 0U; i < rounds; i++) {
+ if (a != NULL) {
+ udp_test_spin(a, slice);
+ }
+ if (b != NULL) {
+ udp_test_spin(b, slice);
+ }
+ }
+}
+
+bool udp_test_spin_pair_until_condition(udp_test_node_t* const a,
+ udp_test_node_t* const b,
+ bool (*const condition)(void*),
+ void* const context,
+ const size_t rounds,
+ const cy_us_t slice)
+{
+ TEST_ASSERT_NOT_NULL(condition);
+ for (size_t i = 0U; i < rounds; i++) {
+ if (condition(context)) {
+ return true;
+ }
+ udp_test_spin_pair(a, b, 1U, slice);
+ }
+ return condition(context);
+}
+
+size_t udp_test_message_read_all(const cy_message_t* const message, void* const out, const size_t capacity)
+{
+ TEST_ASSERT_NOT_NULL(message);
+ TEST_ASSERT_NOT_NULL(out);
+ const size_t size = cy_message_size(message);
+ TEST_ASSERT_TRUE(size <= capacity);
+ TEST_ASSERT_EQUAL_size_t(size, cy_message_read(message, 0U, size, out));
+ return size;
+}
+
+void udp_test_assert_message_equals(const cy_message_t* const message, const void* const expected, const size_t size)
+{
+ uint8_t buffer[4096];
+ TEST_ASSERT_TRUE(size <= sizeof(buffer));
+ TEST_ASSERT_EQUAL_size_t(size, udp_test_message_read_all(message, buffer, sizeof(buffer)));
+ TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, buffer, size);
+}
+
+void udp_test_fill_payload(uint8_t* const out, const size_t size, const uint8_t seed)
+{
+ TEST_ASSERT_NOT_NULL(out);
+ for (size_t i = 0U; i < size; i++) {
+ out[i] = (uint8_t)(seed + (uint8_t)i);
+ }
+}
+
+uint64_t udp_test_parse_uid_from_home(const cy_str_t home)
+{
+ char buffer[17] = { 0 };
+ size_t start = 0U;
+ TEST_ASSERT_NOT_NULL(home.str);
+ for (size_t i = 0U; i < home.len; i++) {
+ if (home.str[i] == '/') {
+ start = i + 1U;
+ }
+ }
+ TEST_ASSERT_TRUE(home.len >= start);
+ TEST_ASSERT_EQUAL_size_t(16U, home.len - start);
+ (void)memcpy(buffer, &home.str[start], 16U);
+ return (uint64_t)strtoull(buffer, NULL, 16);
+}
+
+uint32_t udp_test_hostname_hash20(void)
+{
+ char hostname[256] = { 0 };
+ TEST_ASSERT_EQUAL_INT(0, gethostname(hostname, sizeof(hostname)));
+ hostname[sizeof(hostname) - 1U] = 0;
+ TEST_ASSERT_TRUE(hostname[0] != 0);
+ return (uint32_t)(rapidhash(hostname, strlen(hostname)) & 0xFFFFFU);
+}
+
+uint32_t udp_test_expected_eui64_prefix(void)
+{
+ uint32_t out = udp_test_hostname_hash20();
+ out &= ~(1U << 12U);
+ out |= 1U << 13U;
+ return out;
+}
diff --git a/cy_udp_posix/tests/test_support.h b/cy_udp_posix/tests/test_support.h
new file mode 100644
index 0000000..6c953b7
--- /dev/null
+++ b/cy_udp_posix/tests/test_support.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#define UDP_TEST_SKIP_CODE 77
+
+typedef struct
+{
+ cy_platform_t* platform;
+ cy_t* cy;
+ uint64_t uid;
+} udp_test_node_t;
+
+void udp_test_node_init_manual(udp_test_node_t* self, uint64_t uid, const char* home_prefix, size_t tx_queue_capacity);
+void udp_test_node_deinit(udp_test_node_t* self);
+
+void udp_test_spin(udp_test_node_t* node, cy_us_t slice);
+void udp_test_spin_pair(udp_test_node_t* a, udp_test_node_t* b, size_t rounds, cy_us_t slice);
+bool udp_test_spin_pair_until_condition(udp_test_node_t* a,
+ udp_test_node_t* b,
+ bool (*condition)(void*),
+ void* context,
+ size_t rounds,
+ cy_us_t slice);
+
+size_t udp_test_message_read_all(const cy_message_t* message, void* out, size_t capacity);
+void udp_test_assert_message_equals(const cy_message_t* message, const void* expected, size_t size);
+void udp_test_fill_payload(uint8_t* out, size_t size, uint8_t seed);
+uint64_t udp_test_parse_uid_from_home(const cy_str_t home);
+uint32_t udp_test_hostname_hash20(void);
+uint32_t udp_test_expected_eui64_prefix(void);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 27efa83..158a825 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -6,24 +6,40 @@ enable_testing()
include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/lib")
-# UDP node examples.
-add_executable(udp_pub main_udp_pub.c)
-add_executable(udp_sub main_udp_sub.c)
-add_executable(udp_time_pub main_udp_time_pub.c)
-add_executable(udp_echo main_udp_echo.c)
-target_link_libraries(udp_pub cy_udp_posix_trace)
-target_link_libraries(udp_sub cy_udp_posix_trace)
-target_link_libraries(udp_time_pub cy_udp_posix_trace)
-target_link_libraries(udp_echo cy_udp_posix_trace)
-
-# UDP file transfer examples.
-add_executable(udp_file_server main_udp_file_server.c)
-add_executable(udp_file_client main_udp_file_client.c)
-target_link_libraries(udp_file_server cy_udp_posix_trace)
-target_link_libraries(udp_file_client cy_udp_posix_trace)
-
-# UDP streaming examples.
-add_executable(udp_streaming_server main_udp_streaming_server.c)
-add_executable(udp_streaming_client main_udp_streaming_client.c)
-target_link_libraries(udp_streaming_server cy_udp_posix_trace)
-target_link_libraries(udp_streaming_client cy_udp_posix_trace)
+option(CY_EXAMPLES_TRACE "Enable cy_trace() logging in the example executables" ON)
+
+add_library(cy_examples_support STATIC ../cy/cy.c)
+target_include_directories(cy_examples_support PUBLIC ${CMAKE_SOURCE_DIR}/cy)
+set_target_properties(
+ cy_examples_support
+ PROPERTIES
+ COMPILE_WARNING_AS_ERROR ON
+ C_STANDARD_REQUIRED ON
+ C_EXTENSIONS OFF
+)
+if (CY_EXAMPLES_TRACE)
+ target_sources(cy_examples_support PRIVATE cy_trace_stderr.c)
+ target_compile_definitions(cy_examples_support PUBLIC CY_CONFIG_TRACE=1)
+endif ()
+
+function(cy_add_example target platform_target)
+ add_executable(${target} ${ARGN})
+ target_link_libraries(${target} PRIVATE cy_examples_support ${platform_target})
+ if (TARGET cy_can_socketcan)
+ target_link_libraries(${target} PRIVATE cy_can_socketcan)
+ endif ()
+endfunction()
+
+# Node examples.
+cy_add_example(example_pub cy_udp_posix example_pub.c)
+cy_add_example(example_sub cy_udp_posix example_sub.c)
+cy_add_example(example_time_pub cy_udp_posix example_time_pub.c)
+cy_add_example(example_echo cy_udp_posix example_echo.c)
+
+# File transfer examples.
+cy_add_example(example_file_server cy_udp_posix example_file_server.c)
+cy_add_example(example_file_client cy_udp_posix example_file_client.c)
+
+# Streaming examples.
+cy_add_example(example_streaming_server cy_udp_posix example_streaming_server.c)
+cy_add_example(example_streaming_client cy_udp_posix example_streaming_client.c)
diff --git a/cy_udp_posix/cy_trace_stderr.c b/examples/cy_trace_stderr.c
similarity index 99%
rename from cy_udp_posix/cy_trace_stderr.c
rename to examples/cy_trace_stderr.c
index 30dde00..f33d674 100644
--- a/cy_udp_posix/cy_trace_stderr.c
+++ b/examples/cy_trace_stderr.c
@@ -1,10 +1,12 @@
// Add this file to your build to define cy_trace() that prints trace messages into stderr.
#include "cy_platform.h"
-#include
-#include
+
+#include
#include
+#include
#include
+#include
void cy_trace(cy_t* const cy,
const char* const file,
diff --git a/examples/main_udp_echo.c b/examples/example_echo.c
similarity index 85%
rename from examples/main_udp_echo.c
rename to examples/example_echo.c
index 8bb8d6c..45c9cf0 100644
--- a/examples/main_udp_echo.c
+++ b/examples/example_echo.c
@@ -2,8 +2,8 @@
// Hint: use pattern subscriptions to receive data from multiple topics concurrently; e.g., `>` to receive everything.
#include
-#include
+#include "example_platform.h"
#include "hexdump.h"
#include
@@ -43,19 +43,18 @@ static void on_message(cy_future_t* const subscriber)
cy_message_refcount_dec(arrival.message.content);
}
-int main(const int argc, const char* const argv[])
+int main(int argc, char* argv[])
{
- if (argc != 2) { // TODO: subscribe to multiple patterns
- (void)fprintf(stderr, "Usage: %s \n", argv[0]);
+ const example_platform_t platform = example_platform_make(&argc, argv);
+ if (platform.platform == NULL) {
return 1;
}
-
- cy_platform_t* const platform = cy_udp_posix_new();
- if (platform == NULL) {
- (void)fprintf(stderr, "cy_udp_posix_new\n");
+ if (argc != 2) { // TODO: subscribe to multiple patterns
+ (void)fprintf(stderr, "Usage: %s [iface=...] \n", argv[0]);
return 1;
}
- cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, "udp_echo"), cy_udp_posix_namespace());
+
+ cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace());
if (cy == NULL) {
(void)fprintf(stderr, "cy_new\n");
return 1;
diff --git a/examples/main_udp_file_client.c b/examples/example_file_client.c
similarity index 92%
rename from examples/main_udp_file_client.c
rename to examples/example_file_client.c
index 39646b2..934a887 100644
--- a/examples/main_udp_file_client.c
+++ b/examples/example_file_client.c
@@ -1,9 +1,10 @@
#include
-#include
+
+#include "example_platform.h"
+
#include
#include
#include
-#include
#define MEGA 1000000LL
@@ -27,10 +28,14 @@ typedef struct file_read_response_t
// Command line arguments: file name.
// The read file will be written into stdout as-is.
-int main(const int argc, const char* const argv[])
+int main(int argc, char* argv[])
{
+ const example_platform_t platform = example_platform_make(&argc, argv);
+ if (platform.platform == NULL) {
+ return 1;
+ }
if (argc < 2) {
- (void)fprintf(stderr, "Usage: %s \n", argv[0]);
+ (void)fprintf(stderr, "Usage: %s [iface=...] \n", argv[0]);
return 1;
}
@@ -45,12 +50,7 @@ int main(const int argc, const char* const argv[])
memcpy(req.path, argv[1], req.path_len);
// SET UP THE NODE.
- cy_platform_t* const platform = cy_udp_posix_new();
- if (platform == NULL) {
- (void)fprintf(stderr, "cy_udp_posix_new\n");
- return 1;
- }
- cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, "udp_file_client"), cy_udp_posix_namespace());
+ cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace());
if (cy == NULL) {
(void)fprintf(stderr, "cy_new\n");
return 1;
diff --git a/examples/main_udp_file_server.c b/examples/example_file_server.c
similarity index 91%
rename from examples/main_udp_file_server.c
rename to examples/example_file_server.c
index a28ed60..f6aea11 100644
--- a/examples/main_udp_file_server.c
+++ b/examples/example_file_server.c
@@ -1,7 +1,8 @@
#include
-#include
+
+#include "example_platform.h"
+
#include
-#include
#include
#include
@@ -81,14 +82,13 @@ static void on_file_read_msg(cy_future_t* const subscriber)
cy_message_refcount_dec(arv.message.content);
}
-int main(void)
+int main(int argc, char* argv[])
{
- cy_platform_t* const platform = cy_udp_posix_new();
- if (platform == NULL) {
- (void)fprintf(stderr, "cy_udp_posix_new\n");
+ const example_platform_t platform = example_platform_make(&argc, argv);
+ if (platform.platform == NULL) {
return 1;
}
- cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, "udp_file_server"), cy_udp_posix_namespace());
+ cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace());
if (cy == NULL) {
(void)fprintf(stderr, "cy_new\n");
return 1;
diff --git a/examples/example_platform.h b/examples/example_platform.h
new file mode 100644
index 0000000..b63b1ce
--- /dev/null
+++ b/examples/example_platform.h
@@ -0,0 +1,151 @@
+/// A simple helper that constructs a cy_platform_t instance based on the command-line arguments.
+
+#pragma once
+
+#include
+#if defined(__linux__)
+#include
+#endif
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+/// Stores the platform instance constructed through parsing the argv and the corresponding destructor.
+typedef struct example_platform_t
+{
+ cy_platform_t* platform;
+ void (*destroy)(cy_platform_t*); ///< Invoke this to dispose the platform instance.
+} example_platform_t;
+
+/// The returned string reference is valid until the next invocation.
+static inline cy_str_t example_platform_home(void)
+{
+#if defined(__cplusplus) || (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 202311L))
+ thread_local
+#elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)
+ _Thread_local
+#endif
+ static char home_storage[CY_TOPIC_NAME_MAX + 1U];
+ const uint64_t uid = eui64_semirandom();
+ const int written = snprintf(home_storage, sizeof(home_storage), "%016llx", (unsigned long long)uid);
+ if ((uid == 0U) || (written <= 0) || ((size_t)written >= sizeof(home_storage))) {
+ return (cy_str_t){ .len = 0, .str = NULL };
+ }
+ return (cy_str_t){ .len = (size_t)written, .str = home_storage };
+}
+
+/// The returned string reference is valid until the environment is modified, otherwise static.
+static inline cy_str_t example_platform_namespace(void) { return cy_str(getenv("CYPHAL_NAMESPACE")); }
+
+/// Consumes the argv elements starting with `iface=` and constructs the appropriate platform instance.
+/// The argc/argv will be modified in-place to remove the consumed arguments.
+/// Returns platform=NULL if the arguments are invalid.
+static inline example_platform_t example_platform_make(int* const argc, char** argv)
+{
+ example_platform_t out = { 0 };
+ if ((argc == NULL) || (*argc <= 0) || (argv == NULL)) {
+ (void)fprintf(stderr, "Invalid argv\n");
+ return out;
+ }
+
+ uint32_t udp_iface_address[CY_UDP_POSIX_IFACE_COUNT_MAX] = { 0 };
+#if defined(__linux__)
+ const char* can_iface_name[CANARD_IFACE_COUNT] = { 0 };
+#endif
+ size_t iface_count = 0U;
+ bool use_udp = false;
+ bool use_can = false;
+ int dst = 1;
+ for (int src = 1; src < *argc; src++) {
+ char* const arg = argv[src];
+ if ((arg != NULL) && (strncmp(arg, "iface=", 6) == 0)) {
+ const char* const spec = arg + 6;
+ if (spec[0] == '\0') {
+ (void)fprintf(stderr, "iface= requires a value\n");
+ return out;
+ }
+ if (strncmp(spec, "socketcan:", 10) == 0) {
+#if defined(__linux__)
+ const char* const name = spec + 10;
+ if (name[0] == '\0') {
+ (void)fprintf(stderr, "SocketCAN iface name is empty: %s\n", arg);
+ return out;
+ }
+ if (use_udp) {
+ (void)fprintf(stderr, "Mixed UDP and SocketCAN iface specs are not supported\n");
+ return out;
+ }
+ if (iface_count >= CANARD_IFACE_COUNT) {
+ (void)fprintf(stderr, "Too many SocketCAN ifaces, limit is %u\n", CANARD_IFACE_COUNT);
+ return out;
+ }
+ for (size_t i = 0; i < iface_count; i++) {
+ if ((can_iface_name[i] != NULL) && (strcmp(can_iface_name[i], name) == 0)) {
+ (void)fprintf(stderr, "Duplicate SocketCAN iface: %s\n", name);
+ return out;
+ }
+ }
+ can_iface_name[iface_count++] = name;
+ use_can = true;
+#else
+ (void)fprintf(stderr, "SocketCAN is only supported on Linux\n");
+ return out;
+#endif
+ } else {
+ const uint32_t addr = cy_udp_parse_iface_address(spec);
+ if (addr == 0U) {
+ (void)fprintf(stderr, "Invalid iface spec: %s\n", arg);
+ return out;
+ }
+ if (use_can) {
+ (void)fprintf(stderr, "Mixed UDP and SocketCAN iface specs are not supported\n");
+ return out;
+ }
+ if (iface_count >= CY_UDP_POSIX_IFACE_COUNT_MAX) {
+ (void)fprintf(stderr, "Too many UDP ifaces, limit is %u\n", (unsigned)CY_UDP_POSIX_IFACE_COUNT_MAX);
+ return out;
+ }
+ for (size_t i = 0; i < iface_count; i++) {
+ if (udp_iface_address[i] == addr) {
+ (void)fprintf(stderr, "Duplicate UDP iface: %s\n", spec);
+ return out;
+ }
+ }
+ udp_iface_address[iface_count++] = addr;
+ use_udp = true;
+ }
+ } else {
+ argv[dst++] = argv[src];
+ }
+ }
+ argv[dst] = NULL;
+ *argc = dst;
+
+ if (use_can) {
+#if defined(__linux__)
+ out.platform = cy_can_socketcan_new((uint_least8_t)iface_count, can_iface_name, 1000U);
+ out.destroy = cy_can_socketcan_destroy;
+#endif
+ } else if (use_udp) {
+ const uint64_t uid = eui64_semirandom();
+ if (uid == 0U) {
+ (void)fprintf(stderr, "eui64_semirandom\n");
+ return out;
+ }
+ out.platform = cy_udp_posix_new_manual(uid, udp_iface_address, 50000U);
+ out.destroy = cy_udp_posix_destroy;
+ } else {
+ out.platform = cy_udp_posix_new();
+ out.destroy = cy_udp_posix_destroy;
+ }
+ if (out.platform == NULL) {
+ (void)fprintf(stderr, "platform factory failure\n");
+ return out;
+ }
+ return out;
+}
diff --git a/examples/main_udp_pub.c b/examples/example_pub.c
similarity index 71%
rename from examples/main_udp_pub.c
rename to examples/example_pub.c
index 7254939..3716909 100644
--- a/examples/main_udp_pub.c
+++ b/examples/example_pub.c
@@ -1,13 +1,12 @@
#include
-#include
-#include
+
+#include "example_platform.h"
#include "arg_kv.h"
#include "hexdump.h"
#include
#include
#include
#include
-#include
#include
#define KILO 1000L
@@ -20,31 +19,18 @@ struct config_publication_t
struct config_t
{
- uint64_t local_uid;
- uint32_t iface_address[CY_UDP_POSIX_IFACE_COUNT_MAX];
- size_t tx_queue_capacity;
-
size_t pub_count;
struct config_publication_t* pubs;
};
static struct config_t load_config(const int argc, char* const argv[])
{
- struct config_t cfg = {
- .local_uid = eui64_semirandom(),
- .tx_queue_capacity = 1000,
- .pub_count = 0,
- .pubs = calloc((size_t)(argc - 1), sizeof(struct config_publication_t)),
- };
- size_t iface_count = 0;
- arg_kv_t arg;
+ const size_t config_capacity = (size_t)((argc > 1) ? (argc - 1) : 1);
+ struct config_t cfg = { .pub_count = 0, .pubs = calloc(config_capacity, sizeof(struct config_publication_t)) };
+ arg_kv_t arg;
while ((arg = arg_kv_next(argc, argv)).key_hash != 0) {
assert(arg.value != NULL);
- if ((arg_kv_hash("iface") == arg.key_hash) && (iface_count < CY_UDP_POSIX_IFACE_COUNT_MAX)) {
- cfg.iface_address[iface_count++] = cy_udp_parse_iface_address(arg.value);
- } else if (arg_kv_hash("txq") == arg.key_hash) {
- cfg.tx_queue_capacity = strtoul(arg.value, NULL, 0);
- } else if (arg_kv_hash("pub") == arg.key_hash) {
+ if (arg_kv_hash("pub") == arg.key_hash) {
struct config_publication_t* x = NULL;
for (size_t i = 0; i < cfg.pub_count; i++) {
if (strcmp(cfg.pubs[i].name, arg.value) == 0) {
@@ -59,12 +45,6 @@ static struct config_t load_config(const int argc, char* const argv[])
}
}
// Print the actual configs we're using.
- (void)fprintf(stderr, "ifaces:");
- for (size_t i = 0; i < CY_UDP_POSIX_IFACE_COUNT_MAX; i++) {
- (void)fprintf(stderr, " 0x%08x", (unsigned)cfg.iface_address[i]);
- }
- (void)fprintf(stderr, "\nuid: 0x%016llx\n", (unsigned long long)cfg.local_uid);
- (void)fprintf(stderr, "tx_queue_frames: %zu\n", cfg.tx_queue_capacity);
(void)fprintf(stderr, "publications:\n");
for (size_t i = 0; i < cfg.pub_count; i++) {
(void)fprintf(stderr, "\t%s\n", cfg.pubs[i].name);
@@ -110,23 +90,20 @@ static void on_result(cy_future_t* const future)
cy_future_destroy(future);
}
-int main(const int argc, char* const argv[])
+int main(int argc, char* argv[])
{
srand((unsigned)time(NULL));
- const struct config_t cfg = load_config(argc, argv);
-
- // Set up the platform layer that connects Cy to the underlying transport and OS.
- // This is the only part of the code that is platform-specific; the rest is all portable Cy API usage.
- cy_platform_t* const platform = cy_udp_posix_new_manual(cfg.local_uid, cfg.iface_address, cfg.tx_queue_capacity);
- if (platform == NULL) {
- (void)fprintf(stderr, "cy_udp_posix_new\n");
+ const example_platform_t platform = example_platform_make(&argc, argv);
+ if (platform.platform == NULL) {
return 1;
}
+ const struct config_t cfg = load_config(argc, argv);
// Set up the node instance.
- cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, "udp_pub"), cy_udp_posix_namespace());
+ cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace());
if (cy == NULL) {
(void)fprintf(stderr, "cy_new\n");
+ free(cfg.pubs);
return 1;
}
@@ -136,6 +113,7 @@ int main(const int argc, char* const argv[])
publishers[i] = cy_advertise_client(cy, cy_str(cfg.pubs[i].name), MEGA);
if (publishers[i] == NULL) {
(void)fprintf(stderr, "cy_advertise_client: NULL\n");
+ free(cfg.pubs);
return 1;
}
}
@@ -153,10 +131,7 @@ int main(const int argc, char* const argv[])
if (now >= next_publish_at) {
for (size_t i = 0; i < cfg.pub_count; i++) {
char msg[256];
- (void)sprintf(msg,
- "Hello from %016llx! The current time is %.6f s.",
- (unsigned long long)cfg.local_uid,
- 1e-6 * (double)now);
+ (void)sprintf(msg, "Hello from %s! The current time is %.6f s.", cy_home(cy).str, 1e-6 * (double)now);
cy_future_t* const future = cy_request(publishers[i], //
now + (MEGA * 2),
MEGA * 10,
@@ -171,5 +146,6 @@ int main(const int argc, char* const argv[])
next_publish_at += 5 * MEGA;
}
}
+ free(cfg.pubs);
return 0;
}
diff --git a/examples/main_udp_streaming_client.c b/examples/example_streaming_client.c
similarity index 92%
rename from examples/main_udp_streaming_client.c
rename to examples/example_streaming_client.c
index cc2abef..2b6dcfa 100644
--- a/examples/main_udp_streaming_client.c
+++ b/examples/example_streaming_client.c
@@ -1,7 +1,8 @@
// A tiny streaming client demo: send one request and keep receiving responses.
#include
-#include
+
+#include "example_platform.h"
#include
#include
@@ -58,8 +59,13 @@ static void on_stream_response(cy_future_t* const future)
}
}
-int main(const int argc, const char* const argv[])
+int main(int argc, char* argv[])
{
+ const example_platform_t platform = example_platform_make(&argc, argv);
+ if (platform.platform == NULL) {
+ return 1;
+ }
+
// Parse optional args: count [period_ms].
uint32_t count = DEFAULT_COUNT;
uint32_t period_ms = DEFAULT_PERIOD_ms;
@@ -77,12 +83,7 @@ int main(const int argc, const char* const argv[])
}
// Initialize the node.
- cy_platform_t* const platform = cy_udp_posix_new();
- if (platform == NULL) {
- (void)fprintf(stderr, "cy_udp_posix_new\n");
- return 1;
- }
- cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, NULL), cy_udp_posix_namespace());
+ cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace());
if (cy == NULL) {
(void)fprintf(stderr, "cy_new\n");
return 1;
diff --git a/examples/main_udp_streaming_server.c b/examples/example_streaming_server.c
similarity index 96%
rename from examples/main_udp_streaming_server.c
rename to examples/example_streaming_server.c
index 22d2c38..02a2684 100644
--- a/examples/main_udp_streaming_server.c
+++ b/examples/example_streaming_server.c
@@ -1,7 +1,9 @@
// A tiny streaming server demo: receive a request and stream reliable responses.
#include
-#include
+
+#include "example_platform.h"
+
#include
#include
@@ -178,14 +180,13 @@ static void on_stream_request(cy_future_t* const subscriber)
cy_message_refcount_dec(arv.message.content);
}
-int main(void)
+int main(int argc, char* argv[])
{
- cy_platform_t* const platform = cy_udp_posix_new();
- if (platform == NULL) {
- (void)fprintf(stderr, "cy_udp_posix_new\n");
+ const example_platform_t platform = example_platform_make(&argc, argv);
+ if (platform.platform == NULL) {
return 1;
}
- cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, NULL), cy_udp_posix_namespace());
+ cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace());
if (cy == NULL) {
(void)fprintf(stderr, "cy_new\n");
return 1;
diff --git a/examples/main_udp_sub.c b/examples/example_sub.c
similarity index 73%
rename from examples/main_udp_sub.c
rename to examples/example_sub.c
index 6003767..0b9c5b8 100644
--- a/examples/main_udp_sub.c
+++ b/examples/example_sub.c
@@ -1,13 +1,12 @@
#include
-#include
-#include
+
+#include "example_platform.h"
#include "arg_kv.h"
#include "hexdump.h"
#include
#include
#include
#include
-#include
#include
#define KILO 1000L
@@ -21,33 +20,18 @@ struct config_subscription_t
struct config_t
{
- uint32_t iface_address[CY_UDP_POSIX_IFACE_COUNT_MAX];
- uint64_t local_uid;
- size_t tx_queue_capacity;
-
size_t sub_count;
struct config_subscription_t* subs;
};
static struct config_t load_config(const int argc, char* const argv[])
{
- struct config_t cfg = {
- .local_uid = eui64_semirandom(),
- .tx_queue_capacity = 1000,
- .sub_count = 0,
- .subs = calloc((size_t)(argc - 1), sizeof(struct config_subscription_t)),
- };
- size_t iface_count = 0;
- arg_kv_t arg;
+ const size_t config_capacity = (size_t)((argc > 1) ? (argc - 1) : 1);
+ struct config_t cfg = { .sub_count = 0, .subs = calloc(config_capacity, sizeof(struct config_subscription_t)) };
+ arg_kv_t arg;
while ((arg = arg_kv_next(argc, argv)).key_hash != 0) {
assert(arg.value != NULL);
- if ((arg_kv_hash("iface") == arg.key_hash) && (iface_count < CY_UDP_POSIX_IFACE_COUNT_MAX)) {
- cfg.iface_address[iface_count++] = cy_udp_parse_iface_address(arg.value);
- } else if (arg_kv_hash("uid") == arg.key_hash) {
- cfg.local_uid = (uint64_t)strtoull(arg.value, NULL, 0);
- } else if (arg_kv_hash("txq") == arg.key_hash) {
- cfg.tx_queue_capacity = strtoul(arg.value, NULL, 0);
- } else if ((arg_kv_hash("sub") == arg.key_hash) || (arg_kv_hash("subord") == arg.key_hash)) {
+ if ((arg_kv_hash("sub") == arg.key_hash) || (arg_kv_hash("subord") == arg.key_hash)) {
struct config_subscription_t* x = NULL;
for (size_t i = 0; i < cfg.sub_count; i++) {
if (strcmp(cfg.subs[i].name, arg.value) == 0) {
@@ -63,12 +47,6 @@ static struct config_t load_config(const int argc, char* const argv[])
}
}
// Print the actual configs we're using.
- (void)fprintf(stderr, "ifaces:");
- for (size_t i = 0; i < CY_UDP_POSIX_IFACE_COUNT_MAX; i++) {
- (void)fprintf(stderr, " 0x%08x", (unsigned)cfg.iface_address[i]);
- }
- (void)fprintf(stderr, "\nuid: 0x%016llx\n", (unsigned long long)cfg.local_uid);
- (void)fprintf(stderr, "tx_queue_frames: %zu\n", cfg.tx_queue_capacity);
(void)fprintf(stderr, "subscriptions:\n");
for (size_t i = 0; i < cfg.sub_count; i++) {
(void)fprintf(stderr, "\t%s\n", cfg.subs[i].name);
@@ -135,23 +113,20 @@ static void on_message(cy_future_t* const subscriber)
cy_message_refcount_dec(arrival.message.content);
}
-int main(const int argc, char* const argv[])
+int main(int argc, char* argv[])
{
srand((unsigned)time(NULL));
- const struct config_t cfg = load_config(argc, argv);
-
- // Set up the platform layer that connects Cy to the underlying transport and OS.
- // This is the only part of the code that is platform-specific; the rest is all portable Cy API usage.
- cy_platform_t* const platform = cy_udp_posix_new_manual(cfg.local_uid, cfg.iface_address, cfg.tx_queue_capacity);
- if (platform == NULL) {
- (void)fprintf(stderr, "cy_udp_posix_new\n");
+ const example_platform_t platform = example_platform_make(&argc, argv);
+ if (platform.platform == NULL) {
return 1;
}
+ const struct config_t cfg = load_config(argc, argv);
// Set up the node instance.
- cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, "udp_sub"), cy_udp_posix_namespace());
+ cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace());
if (cy == NULL) {
(void)fprintf(stderr, "cy_new\n");
+ free(cfg.subs);
return 1;
}
@@ -162,6 +137,7 @@ int main(const int argc, char* const argv[])
: cy_subscribe(cy, cy_str(cfg.subs[i].name), MEGA);
if (subscribers[i] == NULL) {
(void)fprintf(stderr, "cy_subscribe(): NULL\n");
+ free(cfg.subs);
return 1;
}
cy_future_callback_set(subscribers[i], on_message);
@@ -175,5 +151,6 @@ int main(const int argc, char* const argv[])
break;
}
}
+ free(cfg.subs);
return 0;
}
diff --git a/examples/main_udp_time_pub.c b/examples/example_time_pub.c
similarity index 87%
rename from examples/main_udp_time_pub.c
rename to examples/example_time_pub.c
index ab310be..b7e8c00 100644
--- a/examples/main_udp_time_pub.c
+++ b/examples/example_time_pub.c
@@ -1,7 +1,8 @@
// Publishes the current time every second.
#include
-#include
+
+#include "example_platform.h"
#include
#include
@@ -26,14 +27,13 @@ static void on_publish(cy_future_t* const future)
}
}
-int main(void)
+int main(int argc, char* argv[])
{
- cy_platform_t* const platform = cy_udp_posix_new();
- if (platform == NULL) {
- (void)fprintf(stderr, "cy_udp_posix_new\n");
+ const example_platform_t platform = example_platform_make(&argc, argv);
+ if (platform.platform == NULL) {
return 1;
}
- cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, "udp_time_pub"), cy_udp_posix_namespace());
+ cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace());
if (cy == NULL) {
(void)fprintf(stderr, "cy_new\n");
return 1;
diff --git a/lib/libcanard b/lib/libcanard
new file mode 160000
index 0000000..f5a00fc
--- /dev/null
+++ b/lib/libcanard
@@ -0,0 +1 @@
+Subproject commit f5a00fc64ba9898948a4a7504bb1d16d87a946d6
diff --git a/tests/src/intrusive_fixture_utils.h b/tests/src/intrusive_fixture_utils.h
index de65924..615b2ab 100644
--- a/tests/src/intrusive_fixture_utils.h
+++ b/tests/src/intrusive_fixture_utils.h
@@ -40,6 +40,7 @@ static inline cy_subject_reader_t* intrusive_subject_reader_new(guarded_heap_t*
(intrusive_test_subject_reader_t*)guarded_heap_alloc(heap, sizeof(*out));
if (out != NULL) {
out->base.subject_id = subject_id;
+ out->base.extent = extent;
out->extent = extent;
}
return (out != NULL) ? &out->base : NULL;
@@ -48,6 +49,7 @@ static inline cy_subject_reader_t* intrusive_subject_reader_new(guarded_heap_t*
static inline void intrusive_subject_reader_extent_set(cy_subject_reader_t* const reader, const size_t extent)
{
intrusive_test_subject_reader_t* const r = (intrusive_test_subject_reader_t*)reader;
+ reader->extent = extent;
r->extent = extent;
}
diff --git a/tools/ci_example_smoke.py b/tools/ci_example_smoke.py
index 62541c6..278ff7f 100644
--- a/tools/ci_example_smoke.py
+++ b/tools/ci_example_smoke.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""
-Smoke-test a pair of UDP examples in unattended CI:
-1) udp_echo subscribes to a topic.
-2) udp_time_pub publishes wall-clock timestamps onto that topic.
+Smoke-test a pair of examples in unattended CI:
+1) example_echo subscribes to a topic.
+2) example_time_pub publishes wall-clock timestamps onto that topic.
The test passes once echo output contains both topic name and timestamp text.
"""
@@ -12,6 +12,7 @@
import os
import pathlib
import re
+import shutil
import signal
import subprocess
import sys
@@ -30,6 +31,22 @@ class ProcessFiles:
stderr: pathlib.Path
+@dataclass
+class SmokeResult:
+ passed: bool
+ failure_reason: str
+ echo_files: ProcessFiles
+ pub_files: ProcessFiles
+
+
+@dataclass
+class VcanSetup:
+ iface_name: str
+ ip_cmd: str
+ privilege_prefix: list[str]
+ created: bool
+
+
def terminate_process(proc: subprocess.Popen[str] | None) -> None:
if proc is None:
return
@@ -55,25 +72,71 @@ def short(text: str, limit: int = 1200) -> str:
return text[-limit:]
-def main() -> int:
- parser = argparse.ArgumentParser()
- parser.add_argument("--build-dir", default="build", help="CMake build directory path")
- parser.add_argument("--timeout-sec", type=float, default=12.0, help="Overall smoke-test timeout")
- parser.add_argument("--publisher-delay-sec", type=float, default=1.0, help="Delay before starting publisher")
- args = parser.parse_args()
-
- build_dir = pathlib.Path(args.build_dir).resolve()
- echo_bin = build_dir / "examples" / "udp_echo"
- pub_bin = build_dir / "examples" / "udp_time_pub"
- for bin_path in (echo_bin, pub_bin):
- if not (bin_path.is_file() and os.access(bin_path, os.X_OK)):
- print(f"Missing executable: {bin_path}", file=sys.stderr)
- return 2
-
- tmp = build_dir
- tmp.mkdir(parents=True, exist_ok=True)
- echo_files = ProcessFiles(stdout=tmp / "example_smoke_echo.out", stderr=tmp / "example_smoke_echo.err")
- pub_files = ProcessFiles(stdout=tmp / "example_smoke_pub.out", stderr=tmp / "example_smoke_pub.err")
+def run_quiet(cmd: list[str]) -> subprocess.CompletedProcess[str]:
+ return subprocess.run(cmd, text=True, capture_output=True, check=False)
+
+
+def privileged_command_prefix() -> list[str] | None:
+ if not sys.platform.startswith("linux"):
+ return None
+ if getattr(os, "geteuid", lambda: 1)() == 0:
+ return []
+ sudo = shutil.which("sudo")
+ if sudo is None:
+ return None
+ return [sudo, "-n"] if run_quiet([sudo, "-n", "true"]).returncode == 0 else None
+
+
+def setup_vcan() -> tuple[VcanSetup | None, str]:
+ if not sys.platform.startswith("linux"):
+ return None, "non-Linux platform"
+ ip_cmd = shutil.which("ip")
+ if ip_cmd is None:
+ return None, "`ip` command is unavailable"
+ privilege_prefix = privileged_command_prefix()
+ if privilege_prefix is None:
+ return None, "passwordless sudo is unavailable"
+
+ iface_name = "vcan0"
+ created = False
+ if run_quiet([ip_cmd, "link", "show", "dev", iface_name]).returncode != 0:
+ modprobe = shutil.which("modprobe")
+ if modprobe is not None:
+ _ = run_quiet(privilege_prefix + [modprobe, "vcan"])
+ add_result = run_quiet(privilege_prefix + [ip_cmd, "link", "add", "dev", iface_name, "type", "vcan"])
+ if add_result.returncode != 0:
+ return None, f"unable to create {iface_name}: {short(add_result.stderr.strip())}"
+ created = True
+
+ up_result = run_quiet(privilege_prefix + [ip_cmd, "link", "set", "dev", iface_name, "up"])
+ if up_result.returncode != 0:
+ if created:
+ _ = run_quiet(privilege_prefix + [ip_cmd, "link", "delete", "dev", iface_name])
+ return None, f"unable to activate {iface_name}: {short(up_result.stderr.strip())}"
+ return VcanSetup(iface_name=iface_name, ip_cmd=ip_cmd, privilege_prefix=privilege_prefix, created=created), ""
+
+
+def cleanup_vcan(setup: VcanSetup | None) -> None:
+ if (setup is None) or (not setup.created):
+ return
+ _ = run_quiet(setup.privilege_prefix + [setup.ip_cmd, "link", "delete", "dev", setup.iface_name])
+
+
+def run_smoke_case(
+ build_dir: pathlib.Path,
+ echo_bin: pathlib.Path,
+ pub_bin: pathlib.Path,
+ timeout_sec: float,
+ publisher_delay_sec: float,
+ label: str,
+ echo_args: list[str],
+ pub_args: list[str],
+) -> SmokeResult:
+ suffix = label.replace("/", "_")
+ echo_files = ProcessFiles(stdout=build_dir / f"example_smoke_{suffix}_echo.out",
+ stderr=build_dir / f"example_smoke_{suffix}_echo.err")
+ pub_files = ProcessFiles(stdout=build_dir / f"example_smoke_{suffix}_pub.out",
+ stderr=build_dir / f"example_smoke_{suffix}_pub.err")
for f in (echo_files.stdout, echo_files.stderr, pub_files.stdout, pub_files.stderr):
f.write_text("", encoding="utf8")
@@ -86,20 +149,20 @@ def main() -> int:
with echo_files.stdout.open("w", encoding="utf8") as echo_out, echo_files.stderr.open(
"w", encoding="utf8"
) as echo_err:
- echo_proc = subprocess.Popen([str(echo_bin), TOPIC], stdout=echo_out, stderr=echo_err)
+ echo_proc = subprocess.Popen([str(echo_bin), *echo_args], stdout=echo_out, stderr=echo_err)
- time.sleep(args.publisher_delay_sec)
+ time.sleep(publisher_delay_sec)
with pub_files.stdout.open("w", encoding="utf8") as pub_out, pub_files.stderr.open("w", encoding="utf8") as pub_err:
- pub_proc = subprocess.Popen([str(pub_bin)], stdout=pub_out, stderr=pub_err)
+ pub_proc = subprocess.Popen([str(pub_bin), *pub_args], stdout=pub_out, stderr=pub_err)
- deadline = time.monotonic() + args.timeout_sec
+ deadline = time.monotonic() + timeout_sec
while time.monotonic() < deadline:
if (echo_proc.poll() is not None) and (echo_proc.returncode != 0):
- failure_reason = f"udp_echo exited early with code {echo_proc.returncode}"
+ failure_reason = f"example_echo exited early with code {echo_proc.returncode}"
break
if (pub_proc.poll() is not None) and (pub_proc.returncode != 0):
- failure_reason = f"udp_time_pub exited early with code {pub_proc.returncode}"
+ failure_reason = f"example_time_pub exited early with code {pub_proc.returncode}"
break
echo_stdout = read_text(echo_files.stdout)
@@ -113,17 +176,73 @@ def main() -> int:
terminate_process(pub_proc)
terminate_process(echo_proc)
- if passed:
- print("Example smoke test passed: observed echoed demo/time timestamp.")
+ return SmokeResult(passed=passed, failure_reason=failure_reason, echo_files=echo_files, pub_files=pub_files)
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--build-dir", default="build", help="CMake build directory path")
+ parser.add_argument("--timeout-sec", type=float, default=12.0, help="Overall smoke-test timeout")
+ parser.add_argument("--publisher-delay-sec", type=float, default=1.0, help="Delay before starting publisher")
+ args = parser.parse_args()
+
+ build_dir = pathlib.Path(args.build_dir).resolve()
+ echo_bin = build_dir / "examples" / "example_echo"
+ pub_bin = build_dir / "examples" / "example_time_pub"
+ for bin_path in (echo_bin, pub_bin):
+ if not (bin_path.is_file() and os.access(bin_path, os.X_OK)):
+ print(f"Missing executable: {bin_path}", file=sys.stderr)
+ return 2
+
+ build_dir.mkdir(parents=True, exist_ok=True)
+
+ udp_result = run_smoke_case(build_dir,
+ echo_bin,
+ pub_bin,
+ args.timeout_sec,
+ args.publisher_delay_sec,
+ "udp",
+ [TOPIC],
+ [])
+ if not udp_result.passed:
+ print(f"Example smoke test failed: {udp_result.failure_reason}", file=sys.stderr)
+ print("--- UDP example_echo stdout (tail) ---", file=sys.stderr)
+ print(short(read_text(udp_result.echo_files.stdout)), file=sys.stderr)
+ print("--- UDP example_echo stderr (tail) ---", file=sys.stderr)
+ print(short(read_text(udp_result.echo_files.stderr)), file=sys.stderr)
+ print("--- UDP example_time_pub stderr (tail) ---", file=sys.stderr)
+ print(short(read_text(udp_result.pub_files.stderr)), file=sys.stderr)
+ return 1
+
+ vcan_setup, vcan_skip_reason = setup_vcan()
+ if vcan_setup is None:
+ print(f"Example smoke test passed: observed echoed demo/time timestamp; vcan skipped ({vcan_skip_reason}).")
+ return 0
+
+ try:
+ iface_arg = f"iface=socketcan:{vcan_setup.iface_name}"
+ vcan_result = run_smoke_case(build_dir,
+ echo_bin,
+ pub_bin,
+ args.timeout_sec,
+ args.publisher_delay_sec,
+ "vcan",
+ [iface_arg, TOPIC],
+ [iface_arg])
+ finally:
+ cleanup_vcan(vcan_setup)
+
+ if vcan_result.passed:
+ print("Example smoke test passed: observed echoed demo/time timestamp over UDP and vcan.")
return 0
- print(f"Example smoke test failed: {failure_reason}", file=sys.stderr)
- print("--- udp_echo stdout (tail) ---", file=sys.stderr)
- print(short(read_text(echo_files.stdout)), file=sys.stderr)
- print("--- udp_echo stderr (tail) ---", file=sys.stderr)
- print(short(read_text(echo_files.stderr)), file=sys.stderr)
- print("--- udp_time_pub stderr (tail) ---", file=sys.stderr)
- print(short(read_text(pub_files.stderr)), file=sys.stderr)
+ print(f"Example smoke test failed: {vcan_result.failure_reason}", file=sys.stderr)
+ print("--- vcan example_echo stdout (tail) ---", file=sys.stderr)
+ print(short(read_text(vcan_result.echo_files.stdout)), file=sys.stderr)
+ print("--- vcan example_echo stderr (tail) ---", file=sys.stderr)
+ print(short(read_text(vcan_result.echo_files.stderr)), file=sys.stderr)
+ print("--- vcan example_time_pub stderr (tail) ---", file=sys.stderr)
+ print(short(read_text(vcan_result.pub_files.stderr)), file=sys.stderr)
return 1