From 6546f6a2d8680fa31a53d80518804ad30e3e15be Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Wed, 1 Apr 2026 23:41:42 +0300 Subject: [PATCH 01/17] Initial implementation of cy_can, WIP, first stab --- .gitmodules | 4 + .idea/dictionaries/project.xml | 1 + CMakeLists.txt | 2 + cy_can/CMakeLists.txt | 51 +++ cy_can/cy_can.c | 796 +++++++++++++++++++++++++++++++++ cy_can/cy_can.h | 101 +++++ cy_can/cy_can_socketcan.c | 300 +++++++++++++ cy_can/cy_can_socketcan.h | 33 ++ lib/libcanard | 1 + 9 files changed, 1289 insertions(+) create mode 100644 cy_can/CMakeLists.txt create mode 100644 cy_can/cy_can.c create mode 100644 cy_can/cy_can.h create mode 100644 cy_can/cy_can_socketcan.c create mode 100644 cy_can/cy_can_socketcan.h create mode 160000 lib/libcanard 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..f31b89e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ 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_can/*.[ch] ) 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 +106,6 @@ endif () # SUBDIRECTORIES add_subdirectory(cy_udp_posix) +add_subdirectory(cy_can) add_subdirectory(examples) add_subdirectory(tests) diff --git a/cy_can/CMakeLists.txt b/cy_can/CMakeLists.txt new file mode 100644 index 0000000..4eea1e8 --- /dev/null +++ b/cy_can/CMakeLists.txt @@ -0,0 +1,51 @@ +# 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 "" +) + +# Shared configuration for cy_can targets. +function(configure_cy_can_target target) + target_link_libraries(${target} PUBLIC canard) + 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() + +# Platform-agnostic CAN transport (cy_can.c + cy.c). +set(cy_can_sources cy_can.c ../cy/cy.c) + +add_library(cy_can STATIC ${cy_can_sources}) +configure_cy_can_target(cy_can) + +# With Linux SocketCAN support. +add_library(cy_can_socketcan STATIC ${cy_can_sources} cy_can_socketcan.c) +configure_cy_can_target(cy_can_socketcan) + +# Same but with CY_CONFIG_TRACE=1 for examples and tests. +add_library(cy_can_socketcan_trace STATIC ${cy_can_sources} cy_can_socketcan.c) +target_compile_definitions(cy_can_socketcan_trace PUBLIC CY_CONFIG_TRACE=1) +configure_cy_can_target(cy_can_socketcan_trace) diff --git a/cy_can/cy_can.c b/cy_can/cy_can.c new file mode 100644 index 0000000..d260c56 --- /dev/null +++ b/cy_can/cy_can.c @@ -0,0 +1,796 @@ +// ____ ______ __ __ +// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / / +// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ / +// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / / +// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/ +// /_/ /____/_/ +// +// Copyright (c) Pavel Kirienko + +#include "cy_can.h" +#include +#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 subject_writer_t subject_writer_t; +typedef struct subject_reader_t subject_reader_t; +typedef struct subject_reader_pinned_t subject_reader_pinned_t; + +// ===================================================================================================================== + +typedef struct +{ + cy_platform_t base; // Must be first (upcast pattern). + const cy_can_vtable_t* vtable; + void* user; + uint_least8_t iface_count; + uint64_t prng_state; + + 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]; + + subject_reader_t* tombstone_head; // Deferred destruction list to avoid reentrancy issues. + + size_t subject_writer_count; + size_t subject_reader_count; + uint64_t v10_rx_count; + uint64_t v11_rx_count; + uint64_t oom_count; // cy_can-level OOM (message wrapper allocation failures, etc.). +} cy_can_t; + +struct subject_writer_t +{ + cy_subject_writer_t base; + uint_least8_t next_tid_13b; + uint_least8_t next_tid_16b; +}; + +struct subject_reader_t +{ + cy_subject_reader_t base; + canard_subscription_t sub_16b; + cy_can_t* owner; + subject_reader_t* next_tombstone; +}; + +// The runtime type is determined as pinned=subject_id<=CY_SUBJECT_ID_PINNED_MAX. +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. +}; + +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 canard_bytes_chain_t cy_bytes_to_canard(const cy_bytes_t message) +{ + 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), ""); + 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 const canard_vtable_t canard_vtbl = { .now = v_canard_now, .tx = v_canard_tx, .filter = NULL }; + +// ===================================================================================================================== +// 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); + owner->v11_rx_count++; + + 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_pinned_t* const pinned = (subject_reader_pinned_t*)self->user_context; + cy_can_t* const owner = pinned->base.owner; + const bool multiframe = (payload.origin.data != NULL); + owner->v10_rx_count++; + + 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; + } + const uint32_t sid = pinned->base.base.subject_id; + deliver(owner, &sid, 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; + owner->subject_writer_count++; + } + CY_TRACE(owner->base.cy, "CAN writer S%08jx n=%zu", (uintmax_t)subject_id, owner->subject_writer_count); + 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; + assert(owner->subject_writer_count > 0); + owner->subject_writer_count--; + 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; + 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)); +} + +/// 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) +{ + 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); + } + assert(owner->subject_reader_count > 0); + owner->subject_reader_count--; + 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 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. + const bool ok_16b = canard_subscribe_16b(&owner->canard, + &self->sub_16b, + (uint16_t)subject_id, + extent, + CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_us, + &sub_vtable_16b); + assert(ok_16b); + (void)ok_16b; + 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; + const bool ok_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(ok_13b); + (void)ok_13b; + p->sub_13b.user_context = p; + build_phony_header(p, subject_id); + } + + owner->subject_reader_count++; + 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; + self->next_tombstone = owner->tombstone_head; + owner->tombstone_head = 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; + 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; + } +} + +// ===================================================================================================================== +// 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); + cy_us_t now = owner->vtable->now(owner->user); + + while (now < deadline) { + canard_poll(&owner->canard, ibm); + + // O(1) tombstone cleanup: finalize all deferred reader destructions. + while (owner->tombstone_head != NULL) { + subject_reader_t* const rd = owner->tombstone_head; + owner->tombstone_head = rd->next_tombstone; + reader_finalize(owner, rd); + } + + cy_can_frame_t frame; + (void)memset(&frame, 0, sizeof(frame)); + if (owner->vtable->rx(owner->user, &frame, deadline)) { + now = owner->vtable->now(owner->user); + const canard_bytes_t can_data = { .size = frame.len, .data = frame.data }; + (void)canard_ingest_frame(&owner->canard, now, frame.iface_index, frame.can_id, can_data); + } + now = owner->vtable->now(owner->user); + } + canard_poll(&owner->canard, ibm); + 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 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->prng_state = vtable->random(user); + + self->base.subject_id_modulus = CY_SUBJECT_ID_MODULUS_16bit; + self->base.vtable = &platform_vtable; + + const bool ok = canard_new(&self->canard, &canard_vtbl, make_mem_set(self), tx_queue_capacity, self->prng_state, 0); + if (!ok) { + vtable->realloc(user, self, 0); + return NULL; + } + self->canard.tx.fd = (vtable->tx_fd != NULL); + self->canard.user_context = self; + + const bool ok_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 (!ok_uni) { + canard_destroy(&self->canard); + vtable->realloc(user, self, 0); + return NULL; + } + self->unicast_sub.user_context = self; + return &self->base; +} + +cy_can_stats_t cy_can_stats(const cy_platform_t* const base) +{ + const cy_can_t* const owner = (const cy_can_t*)base; + return (cy_can_stats_t){ + .subject_writer_count = owner->subject_writer_count, + .subject_reader_count = owner->subject_reader_count, + .v10_rx_count = owner->v10_rx_count, + .v11_rx_count = owner->v11_rx_count, + .oom_count = owner->oom_count, + .canard_err_oom = owner->canard.err.oom, + .canard_err_tx_capacity = owner->canard.err.tx_capacity, + .canard_err_tx_sacrifice = owner->canard.err.tx_sacrifice, + .canard_err_tx_expiration = owner->canard.err.tx_expiration, + .canard_err_rx_frame = owner->canard.err.rx_frame, + .canard_err_rx_transfer = owner->canard.err.rx_transfer, + .canard_err_collision = owner->canard.err.collision, + }; +} + +void* cy_can_user(const cy_platform_t* const base) +{ + const cy_can_t* const owner = (const cy_can_t*)base; + return owner->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 = owner->tombstone_head; + owner->tombstone_head = rd->next_tombstone; + 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..06c286c --- /dev/null +++ b/cy_can/cy_can.h @@ -0,0 +1,101 @@ +// ____ ______ __ __ +// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / / +// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ / +// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / / +// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/ +// /_/ /____/_/ +// +// 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 + +#ifdef __cplusplus +extern "C" +{ +#endif + +/// A received CAN frame (classic or FD). +typedef struct +{ + 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_frame_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. + bool (*rx)(void* user, cy_can_frame_t* out_frame, cy_us_t deadline); + + /// 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; + +/// Diagnostic statistics sampled from both cy_can and the underlying libcanard instance. +typedef struct +{ + size_t subject_writer_count; + size_t subject_reader_count; + uint64_t v10_rx_count; ///< 13-bit (v1.0) transfers received. + uint64_t v11_rx_count; ///< 16-bit (v1.1) transfers received. + uint64_t oom_count; ///< cy_can-level OOM (message wrapper allocation failures, etc.). + // Canard-level counters. + uint64_t canard_err_oom; + uint64_t canard_err_tx_capacity; + uint64_t canard_err_tx_sacrifice; + uint64_t canard_err_tx_expiration; + uint64_t canard_err_rx_frame; + uint64_t canard_err_rx_transfer; + uint64_t canard_err_collision; +} cy_can_stats_t; + +/// Create a new CAN platform instance. Node-ID is allocated automatically via libcanard occupancy tracking. +/// 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 cy_can_vtable_t* const vtable, + void* const user); + +cy_can_stats_t cy_can_stats(const cy_platform_t* const base); + +/// 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..f624d9e --- /dev/null +++ b/cy_can/cy_can_socketcan.c @@ -0,0 +1,300 @@ +// ____ ______ __ __ +// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / / +// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ / +// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / / +// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/ +// /_/ /____/_/ +// +// Copyright (c) Pavel Kirienko + +// Feature test macros must come before any system headers. +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "cy_can_socketcan.h" +#include +#include + +#define RAPIDHASH_COMPACT +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#if CY_CONFIG_TRACE +#include +#include + +void cy_trace(cy_t* const cy, + const char* const file, + const uint_fast16_t line, + const char* const func, + const char* const format, + ...) +{ + (void)cy; + struct timespec ts; + (void)clock_gettime(CLOCK_MONOTONIC, &ts); + const char* fn = strrchr(file, '/'); + fn = (fn != NULL) ? (fn + 1) : file; + (void)fprintf( + stderr, "CY_CAN %05ld.%06ld %s:%u:%s ", (long)ts.tv_sec, ts.tv_nsec / 1000L, fn, (unsigned)line, func); + va_list args; + va_start(args, format); + (void)vfprintf(stderr, format, args); + va_end(args); + (void)fputc('\n', stderr); + (void)fflush(stderr); +} +#endif + +// ===================================================================================================================== + +typedef struct +{ + int sock_fd[CANARD_IFACE_COUNT]; + uint_least8_t iface_count; + bool fd_capable; + uint64_t prng_state; +} socketcan_t; + +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_frame_t* const out_frame, const cy_us_t deadline) +{ + 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; + } + 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; + 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 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, + .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, + .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* 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, 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..35e52c9 --- /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* iface_names[], + const size_t tx_queue_capacity); + +void cy_can_socketcan_destroy(cy_platform_t* const base); + +#ifdef __cplusplus +} +#endif diff --git a/lib/libcanard b/lib/libcanard new file mode 160000 index 0000000..e7f7733 --- /dev/null +++ b/lib/libcanard @@ -0,0 +1 @@ +Subproject commit e7f773324f21b1387595f89daa93f8c2be4fcef4 From 4c49d97a26faee48acb92178a8c8740293575b19 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Thu, 2 Apr 2026 00:12:26 +0300 Subject: [PATCH 02/17] cleanup --- cy_can/cy_can.c | 120 +++++++++++++++++--------------------- cy_can/cy_can.h | 7 ++- cy_can/cy_can_socketcan.c | 9 ++- 3 files changed, 68 insertions(+), 68 deletions(-) diff --git a/cy_can/cy_can.c b/cy_can/cy_can.c index d260c56..7eed89e 100644 --- a/cy_can/cy_can.c +++ b/cy_can/cy_can.c @@ -32,12 +32,6 @@ /// Default extent for incoming unicast messages; will grow as needed. #define UNICAST_EXTENT_INITIAL (HEADER_BYTES + 1024U) -typedef struct subject_writer_t subject_writer_t; -typedef struct subject_reader_t subject_reader_t; -typedef struct subject_reader_pinned_t subject_reader_pinned_t; - -// ===================================================================================================================== - typedef struct { cy_platform_t base; // Must be first (upcast pattern). @@ -53,7 +47,7 @@ typedef struct // Per-remote unicast transfer-ID counters (one per possible CAN node-ID). uint_least8_t unicast_tid[CANARD_NODE_ID_CAPACITY]; - subject_reader_t* tombstone_head; // Deferred destruction list to avoid reentrancy issues. + struct subject_reader_t* tombstone_head; // Deferred destruction list to avoid reentrancy issues. size_t subject_writer_count; size_t subject_reader_count; @@ -62,29 +56,29 @@ typedef struct uint64_t oom_count; // cy_can-level OOM (message wrapper allocation failures, etc.). } cy_can_t; -struct subject_writer_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; -struct subject_reader_t +typedef struct subject_reader_t { - cy_subject_reader_t base; - canard_subscription_t sub_16b; - cy_can_t* owner; - subject_reader_t* next_tombstone; -}; + cy_subject_reader_t base; + canard_subscription_t sub_16b; + cy_can_t* owner; + struct subject_reader_t* next_tombstone; +} subject_reader_t; // The runtime type is determined as pinned=subject_id<=CY_SUBJECT_ID_PINNED_MAX. -struct subject_reader_pinned_t +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) { @@ -463,7 +457,7 @@ static cy_err_t v_subject_writer_send(cy_platform_t* const platform, const uint64_t e_oom = owner->canard.err.oom; const uint64_t e_cap = owner->canard.err.tx_capacity; - bool ok; + 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; @@ -647,28 +641,30 @@ 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); - cy_us_t now = owner->vtable->now(owner->user); - - while (now < deadline) { + while (true) { canard_poll(&owner->canard, ibm); - // O(1) tombstone cleanup: finalize all deferred reader destructions. - while (owner->tombstone_head != NULL) { + // O(1) tombstone cleanup: finalize deferred reader destructions iteratively. + if (owner->tombstone_head != NULL) { subject_reader_t* const rd = owner->tombstone_head; owner->tombstone_head = rd->next_tombstone; reader_finalize(owner, rd); } - cy_can_frame_t frame; - (void)memset(&frame, 0, sizeof(frame)); - if (owner->vtable->rx(owner->user, &frame, deadline)) { - now = owner->vtable->now(owner->user); + 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, now, frame.iface_index, frame.can_id, can_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; + } } - now = owner->vtable->now(owner->user); } - canard_poll(&owner->canard, ibm); return CY_OK; } @@ -693,20 +689,18 @@ static uint64_t v_random(cy_platform_t* const 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, -}; +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 @@ -760,27 +754,21 @@ cy_platform_t* cy_can_new(const uint_least8_t iface_count, cy_can_stats_t cy_can_stats(const cy_platform_t* const base) { const cy_can_t* const owner = (const cy_can_t*)base; - return (cy_can_stats_t){ - .subject_writer_count = owner->subject_writer_count, - .subject_reader_count = owner->subject_reader_count, - .v10_rx_count = owner->v10_rx_count, - .v11_rx_count = owner->v11_rx_count, - .oom_count = owner->oom_count, - .canard_err_oom = owner->canard.err.oom, - .canard_err_tx_capacity = owner->canard.err.tx_capacity, - .canard_err_tx_sacrifice = owner->canard.err.tx_sacrifice, - .canard_err_tx_expiration = owner->canard.err.tx_expiration, - .canard_err_rx_frame = owner->canard.err.rx_frame, - .canard_err_rx_transfer = owner->canard.err.rx_transfer, - .canard_err_collision = owner->canard.err.collision, - }; -} - -void* cy_can_user(const cy_platform_t* const base) -{ - const cy_can_t* const owner = (const cy_can_t*)base; - return owner->user; -} + return (cy_can_stats_t){ .subject_writer_count = owner->subject_writer_count, + .subject_reader_count = owner->subject_reader_count, + .v10_rx_count = owner->v10_rx_count, + .v11_rx_count = owner->v11_rx_count, + .oom_count = owner->oom_count, + .canard_err_oom = owner->canard.err.oom, + .canard_err_tx_capacity = owner->canard.err.tx_capacity, + .canard_err_tx_sacrifice = owner->canard.err.tx_sacrifice, + .canard_err_tx_expiration = owner->canard.err.tx_expiration, + .canard_err_rx_frame = owner->canard.err.rx_frame, + .canard_err_rx_transfer = owner->canard.err.rx_transfer, + .canard_err_collision = owner->canard.err.collision }; +} + +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) { @@ -790,6 +778,8 @@ void cy_can_destroy(cy_platform_t* const base) owner->tombstone_head = rd->next_tombstone; reader_finalize(owner, rd); } + assert(owner->subject_reader_count == 0); // caller must clean up + assert(owner->subject_writer_count == 0); // caller must clean up 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 index 06c286c..c12a9f1 100644 --- a/cy_can/cy_can.h +++ b/cy_can/cy_can.h @@ -26,12 +26,13 @@ extern "C" /// 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_frame_t; +} 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. @@ -51,7 +52,9 @@ typedef struct /// 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. - bool (*rx)(void* user, cy_can_frame_t* out_frame, cy_us_t deadline); + /// 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); /// Returns the current monotonic time in microseconds. Must be non-negative and non-decreasing. cy_us_t (*now)(void* user); diff --git a/cy_can/cy_can_socketcan.c b/cy_can/cy_can_socketcan.c index f624d9e..b150f28 100644 --- a/cy_can/cy_can_socketcan.c +++ b/cy_can/cy_can_socketcan.c @@ -112,7 +112,10 @@ static bool v_tx_fd(void* const user, return res == (ssize_t)sizeof(frame); } -static bool v_rx(void* const user, cy_can_frame_t* const out_frame, const cy_us_t deadline) +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]; @@ -120,6 +123,9 @@ static bool v_rx(void* const user, cy_can_frame_t* const out_frame, const cy_us_ 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; @@ -132,6 +138,7 @@ static bool v_rx(void* const user, cy_can_frame_t* const out_frame, const cy_us_ 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)); From aa80c4bd78c01fd3ecb24942711596f08ecdba40 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Thu, 2 Apr 2026 04:50:59 +0300 Subject: [PATCH 03/17] add clarification comments to cy_udp_posix regarding tombstoning --- cy_udp_posix/cy_udp_posix.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cy_udp_posix/cy_udp_posix.c b/cy_udp_posix/cy_udp_posix.c index 11e196c..4346542 100644 --- a/cy_udp_posix/cy_udp_posix.c +++ b/cy_udp_posix/cy_udp_posix.c @@ -490,6 +490,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]); } @@ -655,6 +658,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++) { From 87925c3b3c2678e6e4a494b4c94e1fe4d3dee0e3 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Thu, 2 Apr 2026 16:48:14 +0300 Subject: [PATCH 04/17] advance CAN --- cy/cy.c | 3 +- cy/cy_platform.h | 3 +- cy_can/cy_can.c | 192 +++++++--- cy_can/cy_can_socketcan.c | 2 + lib/libcanard | 2 +- tests/CMakeLists.txt | 39 ++ tests/src/intrusive_fixture_utils.h | 2 + .../src/test_api_can_subscription_revival.cpp | 332 ++++++++++++++++++ 8 files changed, 522 insertions(+), 53 deletions(-) create mode 100644 tests/src/test_api_can_subscription_revival.cpp 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/cy_can.c b/cy_can/cy_can.c index 7eed89e..fd05c54 100644 --- a/cy_can/cy_can.c +++ b/cy_can/cy_can.c @@ -48,6 +48,7 @@ typedef struct 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; size_t subject_writer_count; size_t subject_reader_count; @@ -68,6 +69,7 @@ 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; @@ -346,9 +348,12 @@ static void v_on_msg_13b(canard_subscription_t* const self, const canard_payload_t payload) { (void)transfer_id; - subject_reader_pinned_t* const pinned = (subject_reader_pinned_t*)self->user_context; - cy_can_t* const owner = pinned->base.owner; - const bool multiframe = (payload.origin.data != NULL); + 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); owner->v10_rx_count++; const size_t inline_size = HEADER_BYTES + (multiframe ? 0 : payload.view.size); @@ -376,8 +381,7 @@ static void v_on_msg_13b(canard_subscription_t* const self, } msg->seg0_len += payload.view.size; } - const uint32_t sid = pinned->base.base.subject_id; - deliver(owner, &sid, source_node_id, priority, timestamp, msg); + deliver(owner, &reader->base.subject_id, source_node_id, priority, timestamp, msg); } /// Service-ID 511 subscription callback for unicast transfers. @@ -508,9 +512,90 @@ static void build_phony_header(subject_reader_pinned_t* const self, const uint32 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 bool is_tombstoned(const cy_can_t* const owner, const subject_reader_t* const self) +{ + return (self->next_tombstone != NULL) || (self->prev_tombstone != NULL) || (owner->tombstone_head == self) || + (owner->tombstone_tail == self); +} + +static void tombstone_remove(cy_can_t* const owner, subject_reader_t* const self) +{ + assert((owner != NULL) && (self != NULL) && is_tombstoned(owner, self)); + 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) && !is_tombstoned(owner, self)); + 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) && + is_tombstoned(owner, self)); + 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) && !is_tombstoned(owner, self)); canard_unsubscribe(&owner->canard, &self->sub_16b); subject_reader_pinned_t* const pinned = as_pinned(self); if (pinned != NULL) { @@ -525,10 +610,16 @@ 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 self = (subject_reader_t*)owner->vtable->realloc(owner->user, NULL, sz); + 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; } @@ -538,29 +629,37 @@ static cy_subject_reader_t* v_subject_reader_new(cy_platform_t* const base, self->owner = owner; // The extent from Cy already includes the header overhead. - const bool ok_16b = canard_subscribe_16b(&owner->canard, - &self->sub_16b, - (uint16_t)subject_id, - extent, - CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_us, - &sub_vtable_16b); - assert(ok_16b); - (void)ok_16b; + 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; - const bool ok_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(ok_13b); - (void)ok_13b; - p->sub_13b.user_context = p; + 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); } @@ -574,8 +673,7 @@ static void v_subject_reader_destroy(cy_platform_t* const platform, cy_subject_r { cy_can_t* const owner = (cy_can_t*)platform; subject_reader_t* const self = (subject_reader_t*)base; - self->next_tombstone = owner->tombstone_head; - owner->tombstone_head = self; + tombstone_enqueue(owner, self); } static void v_subject_reader_extent_set(cy_platform_t* const base, @@ -583,12 +681,8 @@ static void v_subject_reader_extent_set(cy_platform_t* const base, const size_t extent) { (void)base; - subject_reader_t* const self = (subject_reader_t*)reader_base; - 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; - } + subject_reader_t* const self = (subject_reader_t*)reader_base; + reader_set_extent(self, extent); } // ===================================================================================================================== @@ -643,14 +737,12 @@ static cy_err_t v_spin(cy_platform_t* const base, const cy_us_t deadline) const uint_least8_t ibm = (uint_least8_t)((1U << owner->iface_count) - 1U); while (true) { canard_poll(&owner->canard, ibm); - - // O(1) tombstone cleanup: finalize deferred reader destructions iteratively. - if (owner->tombstone_head != NULL) { - subject_reader_t* const rd = owner->tombstone_head; - owner->tombstone_head = rd->next_tombstone; - reader_finalize(owner, rd); + { + 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)) { @@ -736,13 +828,13 @@ cy_platform_t* cy_can_new(const uint_least8_t iface_count, self->canard.tx.fd = (vtable->tx_fd != NULL); self->canard.user_context = self; - const bool ok_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 (!ok_uni) { + 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; @@ -774,8 +866,8 @@ 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 = owner->tombstone_head; - owner->tombstone_head = rd->next_tombstone; + subject_reader_t* const rd = tombstone_pop(owner); + assert(rd != NULL); reader_finalize(owner, rd); } assert(owner->subject_reader_count == 0); // caller must clean up diff --git a/cy_can/cy_can_socketcan.c b/cy_can/cy_can_socketcan.c index b150f28..4afce7a 100644 --- a/cy_can/cy_can_socketcan.c +++ b/cy_can/cy_can_socketcan.c @@ -8,9 +8,11 @@ // 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 diff --git a/lib/libcanard b/lib/libcanard index e7f7733..c719fae 160000 --- a/lib/libcanard +++ b/lib/libcanard @@ -1 +1 @@ -Subproject commit e7f773324f21b1387595f89daa93f8c2be4fcef4 +Subproject commit c719fae8d89c9d2b58df7386aeac7bd631186e12 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 203e475..942c505 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -155,6 +155,42 @@ function(cy_gen_test_api_matrix name definitions unique_sources shared_c_sources endforeach () endfunction() +# API test matrix with cy_can + libcanard sources built per-arch so the transport integration can be exercised +# under the same x64/x32 settings without reusing the host-arch convenience library targets. +function(cy_gen_test_api_can_matrix name definitions unique_sources) + foreach (arch IN ITEMS x64 x32) + if (arch STREQUAL "x64") + set(arch_flags "-m64") + else () + set(arch_flags "-m32") + endif () + set(target_name "${name}_${arch}_c11") + + add_library("${target_name}_canard" STATIC "${CMAKE_SOURCE_DIR}/lib/libcanard/libcanard/canard.c") + target_compile_options("${target_name}_canard" PRIVATE ${arch_flags} -Wno-cast-align) + target_link_options("${target_name}_canard" PRIVATE ${arch_flags}) + target_include_directories("${target_name}_canard" SYSTEM INTERFACE ${CMAKE_SOURCE_DIR}/lib/libcanard/libcanard) + set_target_properties( + "${target_name}_canard" + PROPERTIES + C_STANDARD 11 + C_STANDARD_REQUIRED ON + C_EXTENSIONS OFF + COMPILE_WARNING_AS_ERROR OFF + C_CLANG_TIDY "" + CXX_CLANG_TIDY "" + C_CPPCHECK "" + CXX_CPPCHECK "" + ) + + cy_gen_test("${target_name}" "${arch_flags}" "${arch_flags}" "11" "${definitions}" + "${unique_sources};${CMAKE_SOURCE_DIR}/cy_can/cy_can.c;${CMAKE_SOURCE_DIR}/cy/cy.c;src/cy_trace.c;platform_stubs/guarded_heap.c" + ) + target_include_directories(${target_name} PRIVATE ${CMAKE_SOURCE_DIR}/cy_can) + target_link_libraries(${target_name} PRIVATE "${target_name}_canard") + endforeach () +endfunction() + # -------------------------------------------------------------------------------------------------- # INTRUSIVE TESTS. # These tests have access to the library internals and are written in C. @@ -246,6 +282,9 @@ cy_gen_test_api_matrix(test_api_gossip_sim "CY_CONFIG_TRACE=1" "src/test_api_gossip_sim.cpp" "${cy_api_shared_c_sources}" "" ) +cy_gen_test_api_can_matrix(test_api_can_subscription_revival "CY_CONFIG_TRACE=1" + "src/test_api_can_subscription_revival.cpp" +) # -------------------------------------------------------------------------------------------------- # END-TO-END API TESTS. 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/tests/src/test_api_can_subscription_revival.cpp b/tests/src/test_api_can_subscription_revival.cpp new file mode 100644 index 0000000..2c654af --- /dev/null +++ b/tests/src/test_api_can_subscription_revival.cpp @@ -0,0 +1,332 @@ +#include +#include +#include "guarded_heap.h" +#include +#include +#include +#include + +namespace { + +constexpr cy_us_t spin_slice_us = 10'000; +constexpr std::size_t queue_capacity = 512U; + +struct queued_frame_t +{ + std::uint32_t can_id{ 0U }; + std::uint_least8_t len{ 0U }; + bool fd{ false }; + std::array data{}; +}; + +struct test_platform_t +{ + guarded_heap_t heap{}; + std::uint64_t random_state{ UINT64_C(0x123456789ABCDEF0) }; + cy_us_t now{ 1000 }; + std::size_t tx_classic_count{ 0U }; + std::size_t rx_call_count{ 0U }; + std::size_t queue_head{ 0U }; + std::size_t queue_tail{ 0U }; + std::size_t queued_count{ 0U }; + std::array queue{}; +}; + +struct fixture_t +{ + test_platform_t io{}; + cy_can_vtable_t vtable{}; + cy_platform_t* platform{ nullptr }; + cy_t* cy{ nullptr }; +}; + +template +std::array make_payload(const unsigned char seed) +{ + std::array out{}; + for (std::size_t i = 0U; i < out.size(); i++) { + out.at(i) = static_cast(seed + static_cast(i)); + } + return out; +} + +void enqueue_frame(test_platform_t* const self, + const std::uint32_t can_id, + const bool fd, + const void* const data, + const std::uint_least8_t len) +{ + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_TRUE(self->queued_count < self->queue.size()); + queued_frame_t& out = self->queue.at(self->queue_tail); + out.can_id = can_id; + out.len = len; + out.fd = fd; + if (len > 0U) { + (void)std::memcpy(out.data.data(), data, len); + } + self->queue_tail = (self->queue_tail + 1U) % self->queue.size(); + self->queued_count++; +} + +bool dequeue_frame(test_platform_t* const self, cy_can_rx_t* const out_frame) +{ + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_NOT_NULL(out_frame); + if (self->queued_count == 0U) { + return false; + } + const queued_frame_t& in = self->queue.at(self->queue_head); + out_frame->timestamp = self->now++; + out_frame->can_id = in.can_id; + out_frame->iface_index = 0U; + out_frame->len = in.len; + out_frame->fd = in.fd; + if (in.len > 0U) { + (void)std::memcpy(out_frame->data, in.data.data(), in.len); + } + self->queue_head = (self->queue_head + 1U) % self->queue.size(); + self->queued_count--; + return true; +} + +extern "C" bool platform_tx_classic(void* const user, + const std::uint_least8_t iface_index, + const std::uint32_t can_id, + const void* const data, + const std::uint_least8_t len) +{ + auto* const self = static_cast(user); + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_EQUAL_UINT8(0U, iface_index); + TEST_ASSERT_TRUE(len <= 8U); + self->tx_classic_count++; + enqueue_frame(self, can_id, false, data, len); + return true; +} + +extern "C" bool platform_rx(void* const user, + cy_can_rx_t* const out_frame, + const cy_us_t deadline, + const std::uint_least8_t tx_pending_iface_bitmap) +{ + (void)tx_pending_iface_bitmap; + auto* const self = static_cast(user); + TEST_ASSERT_NOT_NULL(self); + self->rx_call_count++; + if (dequeue_frame(self, out_frame)) { + return true; + } + if (self->now <= deadline) { + self->now = deadline + 1; + } + return false; +} + +extern "C" cy_us_t platform_now(void* const user) +{ + const auto* const self = static_cast(user); + TEST_ASSERT_NOT_NULL(self); + return self->now; +} + +extern "C" void* platform_realloc(void* const user, void* const ptr, const std::size_t size) +{ + return guarded_heap_realloc(user, ptr, size); +} + +extern "C" std::uint64_t platform_random(void* const user) +{ + auto* const self = static_cast(user); + TEST_ASSERT_NOT_NULL(self); + self->random_state = (self->random_state * UINT64_C(6364136223846793005)) + UINT64_C(1); + return self->random_state; +} + +void fixture_init(fixture_t* const self) +{ + TEST_ASSERT_NOT_NULL(self); + *self = fixture_t{}; + guarded_heap_init(&self->io.heap, UINT64_C(0xA5A55A5ADEADBEEF)); + self->io.now = 1000; + + self->vtable.tx_classic = platform_tx_classic; + self->vtable.tx_fd = nullptr; + self->vtable.rx = platform_rx; + self->vtable.now = platform_now; + self->vtable.realloc = platform_realloc; + self->vtable.random = platform_random; + + self->platform = cy_can_new(1U, 128U, &self->vtable, &self->io); + TEST_ASSERT_NOT_NULL(self->platform); + + self->cy = cy_new(self->platform, cy_str("test_can"), cy_str_t{ 0U, nullptr }); + TEST_ASSERT_NOT_NULL(self->cy); +} + +void fixture_deinit(fixture_t* const self) +{ + TEST_ASSERT_NOT_NULL(self); + if (self->cy != nullptr) { + cy_destroy(self->cy); + self->cy = nullptr; + } + if (self->platform != nullptr) { + cy_can_destroy(self->platform); + self->platform = nullptr; + } + TEST_ASSERT_EQUAL_size_t(0U, guarded_heap_allocated_fragments(&self->io.heap)); + TEST_ASSERT_EQUAL_size_t(0U, guarded_heap_allocated_bytes(&self->io.heap)); +} + +void spin_until_done(fixture_t* const self, cy_future_t* const future) +{ + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_NOT_NULL(future); + for (std::size_t i = 0U; (i < 16U) && !cy_future_done(future); i++) { + TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(self->cy, cy_now(self->cy) + spin_slice_us)); + } + TEST_ASSERT_TRUE(cy_future_done(future)); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(future)); +} + +void spin_once(fixture_t* const self) +{ + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(self->cy, cy_now(self->cy))); +} + +template +void publish_and_expect_payload(fixture_t* const self, + cy_publisher_t* const pub, + cy_future_t* const sub, + const std::array& payload) +{ + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_NOT_NULL(pub); + TEST_ASSERT_NOT_NULL(sub); + self->io.tx_classic_count = 0U; + + const cy_bytes_t msg = { payload.size(), payload.data(), nullptr }; + TEST_ASSERT_EQUAL_INT(CY_OK, cy_publish(pub, cy_now(self->cy) + (10U * spin_slice_us), msg)); + spin_until_done(self, sub); + TEST_ASSERT_TRUE(self->io.tx_classic_count > 0U); + TEST_ASSERT_EQUAL_UINT64(1U, cy_arrival_count(sub)); + + const cy_arrival_t arrival = cy_arrival_borrow(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + TEST_ASSERT_EQUAL_size_t(payload.size(), cy_message_size(arrival.message.content)); + + std::array restored{}; + TEST_ASSERT_EQUAL_size_t(payload.size(), + cy_message_read(arrival.message.content, 0U, restored.size(), restored.data())); + TEST_ASSERT_EQUAL_UINT8_ARRAY(payload.data(), restored.data(), payload.size()); +} + +void test_api_can_revives_verbatim_reader_and_preserves_extent() +{ + fixture_t fix{}; + fixture_init(&fix); + + static const char* const topic_name = "test/can/revive/verbatim"; + constexpr std::size_t extent_old = 128U; + constexpr std::size_t extent_new = 64U; + const auto payload = make_payload<96U>(0x10U); + + const std::size_t base_readers = cy_can_stats(fix.platform).subject_reader_count; + TEST_ASSERT_EQUAL_size_t(1U, base_readers); + + cy_future_t* const first = cy_subscribe(fix.cy, cy_str(topic_name), extent_old); + TEST_ASSERT_NOT_NULL(first); + const std::size_t readers_after_first = cy_can_stats(fix.platform).subject_reader_count; + TEST_ASSERT_TRUE(readers_after_first > base_readers); + + cy_future_destroy(first); + spin_once(&fix); + TEST_ASSERT_EQUAL_size_t(readers_after_first, cy_can_stats(fix.platform).subject_reader_count); + + cy_future_t* const second = cy_subscribe(fix.cy, cy_str(topic_name), extent_new); + TEST_ASSERT_NOT_NULL(second); + TEST_ASSERT_EQUAL_size_t(readers_after_first, cy_can_stats(fix.platform).subject_reader_count); + + cy_publisher_t* const pub = cy_advertise(fix.cy, cy_str(topic_name)); + TEST_ASSERT_NOT_NULL(pub); + publish_and_expect_payload(&fix, pub, second, payload); + + cy_unadvertise(pub); + cy_future_destroy(second); + spin_once(&fix); + + fixture_deinit(&fix); +} + +void test_api_can_revives_pinned_reader_and_preserves_extent() +{ + fixture_t fix{}; + fixture_init(&fix); + + static const char* const topic_name = "123#123"; + constexpr std::size_t extent_old = 128U; + constexpr std::size_t extent_new = 64U; + const auto payload = make_payload<96U>(0x40U); + + const std::size_t base_readers = cy_can_stats(fix.platform).subject_reader_count; + TEST_ASSERT_EQUAL_size_t(1U, base_readers); + + cy_future_t* const first = cy_subscribe(fix.cy, cy_str(topic_name), extent_old); + TEST_ASSERT_NOT_NULL(first); + const std::size_t readers_after_first = cy_can_stats(fix.platform).subject_reader_count; + TEST_ASSERT_TRUE(readers_after_first > base_readers); + + cy_future_destroy(first); + spin_once(&fix); + TEST_ASSERT_EQUAL_size_t(readers_after_first, cy_can_stats(fix.platform).subject_reader_count); + + cy_future_t* const second = cy_subscribe(fix.cy, cy_str(topic_name), extent_new); + TEST_ASSERT_NOT_NULL(second); + TEST_ASSERT_EQUAL_size_t(readers_after_first, cy_can_stats(fix.platform).subject_reader_count); + + cy_publisher_t* const pub = cy_advertise(fix.cy, cy_str(topic_name)); + TEST_ASSERT_NOT_NULL(pub); + publish_and_expect_payload(&fix, pub, second, payload); + + cy_unadvertise(pub); + cy_future_destroy(second); + spin_once(&fix); + + fixture_deinit(&fix); +} + +void test_api_can_reuses_tombstoned_broadcast_reader_after_cy_destroy() +{ + fixture_t fix{}; + fixture_init(&fix); + + const std::size_t base_readers = cy_can_stats(fix.platform).subject_reader_count; + TEST_ASSERT_EQUAL_size_t(1U, base_readers); + + cy_destroy(fix.cy); + fix.cy = nullptr; + TEST_ASSERT_EQUAL_size_t(base_readers, cy_can_stats(fix.platform).subject_reader_count); + + fix.cy = cy_new(fix.platform, cy_str("test_can"), cy_str_t{ 0U, nullptr }); + TEST_ASSERT_NOT_NULL(fix.cy); + TEST_ASSERT_EQUAL_size_t(base_readers, cy_can_stats(fix.platform).subject_reader_count); + + fixture_deinit(&fix); +} + +} // namespace + +extern "C" void setUp() {} + +extern "C" void tearDown() {} + +int main() +{ + UNITY_BEGIN(); + RUN_TEST(test_api_can_revives_verbatim_reader_and_preserves_extent); + RUN_TEST(test_api_can_revives_pinned_reader_and_preserves_extent); + RUN_TEST(test_api_can_reuses_tombstoned_broadcast_reader_after_cy_destroy); + return UNITY_END(); +} From 780f11487ee6848f7758ecaf37a9c175e3e97bf5 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Thu, 2 Apr 2026 20:40:01 +0300 Subject: [PATCH 05/17] scaffolding tests for cy_can --- CMakeLists.txt | 4 ++ cy_can/CMakeLists.txt | 2 + .../test_api_can_subscription_revival.cpp | 0 tests/CMakeLists.txt | 39 ------------------- 4 files changed, 6 insertions(+), 39 deletions(-) rename {tests/src => cy_can/tests}/test_api_can_subscription_revival.cpp (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index f31b89e..add8bd7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,7 +54,11 @@ 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_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}) diff --git a/cy_can/CMakeLists.txt b/cy_can/CMakeLists.txt index 4eea1e8..e621a54 100644 --- a/cy_can/CMakeLists.txt +++ b/cy_can/CMakeLists.txt @@ -49,3 +49,5 @@ configure_cy_can_target(cy_can_socketcan) add_library(cy_can_socketcan_trace STATIC ${cy_can_sources} cy_can_socketcan.c) target_compile_definitions(cy_can_socketcan_trace PUBLIC CY_CONFIG_TRACE=1) configure_cy_can_target(cy_can_socketcan_trace) + +# TODO build the tests diff --git a/tests/src/test_api_can_subscription_revival.cpp b/cy_can/tests/test_api_can_subscription_revival.cpp similarity index 100% rename from tests/src/test_api_can_subscription_revival.cpp rename to cy_can/tests/test_api_can_subscription_revival.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 942c505..203e475 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -155,42 +155,6 @@ function(cy_gen_test_api_matrix name definitions unique_sources shared_c_sources endforeach () endfunction() -# API test matrix with cy_can + libcanard sources built per-arch so the transport integration can be exercised -# under the same x64/x32 settings without reusing the host-arch convenience library targets. -function(cy_gen_test_api_can_matrix name definitions unique_sources) - foreach (arch IN ITEMS x64 x32) - if (arch STREQUAL "x64") - set(arch_flags "-m64") - else () - set(arch_flags "-m32") - endif () - set(target_name "${name}_${arch}_c11") - - add_library("${target_name}_canard" STATIC "${CMAKE_SOURCE_DIR}/lib/libcanard/libcanard/canard.c") - target_compile_options("${target_name}_canard" PRIVATE ${arch_flags} -Wno-cast-align) - target_link_options("${target_name}_canard" PRIVATE ${arch_flags}) - target_include_directories("${target_name}_canard" SYSTEM INTERFACE ${CMAKE_SOURCE_DIR}/lib/libcanard/libcanard) - set_target_properties( - "${target_name}_canard" - PROPERTIES - C_STANDARD 11 - C_STANDARD_REQUIRED ON - C_EXTENSIONS OFF - COMPILE_WARNING_AS_ERROR OFF - C_CLANG_TIDY "" - CXX_CLANG_TIDY "" - C_CPPCHECK "" - CXX_CPPCHECK "" - ) - - cy_gen_test("${target_name}" "${arch_flags}" "${arch_flags}" "11" "${definitions}" - "${unique_sources};${CMAKE_SOURCE_DIR}/cy_can/cy_can.c;${CMAKE_SOURCE_DIR}/cy/cy.c;src/cy_trace.c;platform_stubs/guarded_heap.c" - ) - target_include_directories(${target_name} PRIVATE ${CMAKE_SOURCE_DIR}/cy_can) - target_link_libraries(${target_name} PRIVATE "${target_name}_canard") - endforeach () -endfunction() - # -------------------------------------------------------------------------------------------------- # INTRUSIVE TESTS. # These tests have access to the library internals and are written in C. @@ -282,9 +246,6 @@ cy_gen_test_api_matrix(test_api_gossip_sim "CY_CONFIG_TRACE=1" "src/test_api_gossip_sim.cpp" "${cy_api_shared_c_sources}" "" ) -cy_gen_test_api_can_matrix(test_api_can_subscription_revival "CY_CONFIG_TRACE=1" - "src/test_api_can_subscription_revival.cpp" -) # -------------------------------------------------------------------------------------------------- # END-TO-END API TESTS. From a6fa7cb4910e898184f67d40f9deb6ad3705c4b2 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 7 Apr 2026 13:37:09 +0300 Subject: [PATCH 06/17] simplify cycan --- cy_can/cy_can.c | 40 ++++++---------------------------------- cy_can/cy_can.h | 23 ++--------------------- 2 files changed, 8 insertions(+), 55 deletions(-) diff --git a/cy_can/cy_can.c b/cy_can/cy_can.c index fd05c54..80b081b 100644 --- a/cy_can/cy_can.c +++ b/cy_can/cy_can.c @@ -38,7 +38,6 @@ typedef struct const cy_can_vtable_t* vtable; void* user; uint_least8_t iface_count; - uint64_t prng_state; canard_t canard; @@ -50,10 +49,6 @@ typedef struct struct subject_reader_t* tombstone_head; // Deferred destruction list to avoid reentrancy issues. struct subject_reader_t* tombstone_tail; - size_t subject_writer_count; - size_t subject_reader_count; - uint64_t v10_rx_count; - uint64_t v11_rx_count; uint64_t oom_count; // cy_can-level OOM (message wrapper allocation failures, etc.). } cy_can_t; @@ -317,7 +312,6 @@ static void v_on_msg_16b(canard_subscription_t* const self, 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); - owner->v11_rx_count++; can_message_t* const msg = make_message(owner, multiframe ? 0 : payload.view.size); if (msg == NULL) { @@ -354,7 +348,6 @@ static void v_on_msg_13b(canard_subscription_t* const self, subject_reader_pinned_t* const pinned = as_pinned(reader); assert((pinned != NULL) && (owner != NULL)); const bool multiframe = (payload.origin.data != NULL); - owner->v10_rx_count++; const size_t inline_size = HEADER_BYTES + (multiframe ? 0 : payload.view.size); can_message_t* const msg = make_message(owner, inline_size); @@ -429,7 +422,6 @@ static cy_subject_writer_t* v_subject_writer_new(cy_platform_t* const base, cons if (self != NULL) { (void)memset(self, 0, sizeof(*self)); self->base.subject_id = subject_id; - owner->subject_writer_count++; } CY_TRACE(owner->base.cy, "CAN writer S%08jx n=%zu", (uintmax_t)subject_id, owner->subject_writer_count); return (cy_subject_writer_t*)self; @@ -439,8 +431,6 @@ static void v_subject_writer_destroy(cy_platform_t* const platform, cy_subject_w { cy_can_t* const owner = (cy_can_t*)platform; subject_writer_t* const self = (subject_writer_t*)base; - assert(owner->subject_writer_count > 0); - owner->subject_writer_count--; owner->vtable->realloc(owner->user, self, 0); } @@ -601,8 +591,6 @@ static void reader_finalize(cy_can_t* const owner, subject_reader_t* const self) if (pinned != NULL) { canard_unsubscribe(&owner->canard, &pinned->sub_13b); } - assert(owner->subject_reader_count > 0); - owner->subject_reader_count--; owner->vtable->realloc(owner->user, self, 0); } @@ -663,7 +651,6 @@ static cy_subject_reader_t* v_subject_reader_new(cy_platform_t* const base, build_phony_header(p, subject_id); } - owner->subject_reader_count++; 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; } @@ -815,12 +802,16 @@ cy_platform_t* cy_can_new(const uint_least8_t iface_count, self->vtable = vtable; self->user = user; self->iface_count = iface_count; - self->prng_state = vtable->random(user); self->base.subject_id_modulus = CY_SUBJECT_ID_MODULUS_16bit; self->base.vtable = &platform_vtable; - const bool ok = canard_new(&self->canard, &canard_vtbl, make_mem_set(self), tx_queue_capacity, self->prng_state, 0); + const bool ok = canard_new(&self->canard, // + &canard_vtbl, + make_mem_set(self), + tx_queue_capacity, + vtable->random(user), + 0); if (!ok) { vtable->realloc(user, self, 0); return NULL; @@ -843,23 +834,6 @@ cy_platform_t* cy_can_new(const uint_least8_t iface_count, return &self->base; } -cy_can_stats_t cy_can_stats(const cy_platform_t* const base) -{ - const cy_can_t* const owner = (const cy_can_t*)base; - return (cy_can_stats_t){ .subject_writer_count = owner->subject_writer_count, - .subject_reader_count = owner->subject_reader_count, - .v10_rx_count = owner->v10_rx_count, - .v11_rx_count = owner->v11_rx_count, - .oom_count = owner->oom_count, - .canard_err_oom = owner->canard.err.oom, - .canard_err_tx_capacity = owner->canard.err.tx_capacity, - .canard_err_tx_sacrifice = owner->canard.err.tx_sacrifice, - .canard_err_tx_expiration = owner->canard.err.tx_expiration, - .canard_err_rx_frame = owner->canard.err.rx_frame, - .canard_err_rx_transfer = owner->canard.err.rx_transfer, - .canard_err_collision = owner->canard.err.collision }; -} - 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) @@ -870,8 +844,6 @@ void cy_can_destroy(cy_platform_t* const base) assert(rd != NULL); reader_finalize(owner, rd); } - assert(owner->subject_reader_count == 0); // caller must clean up - assert(owner->subject_writer_count == 0); // caller must clean up 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 index c12a9f1..ee98fd0 100644 --- a/cy_can/cy_can.h +++ b/cy_can/cy_can.h @@ -66,33 +66,14 @@ typedef struct uint64_t (*random)(void* user); } cy_can_vtable_t; -/// Diagnostic statistics sampled from both cy_can and the underlying libcanard instance. -typedef struct -{ - size_t subject_writer_count; - size_t subject_reader_count; - uint64_t v10_rx_count; ///< 13-bit (v1.0) transfers received. - uint64_t v11_rx_count; ///< 16-bit (v1.1) transfers received. - uint64_t oom_count; ///< cy_can-level OOM (message wrapper allocation failures, etc.). - // Canard-level counters. - uint64_t canard_err_oom; - uint64_t canard_err_tx_capacity; - uint64_t canard_err_tx_sacrifice; - uint64_t canard_err_tx_expiration; - uint64_t canard_err_rx_frame; - uint64_t canard_err_rx_transfer; - uint64_t canard_err_collision; -} cy_can_stats_t; - -/// Create a new CAN platform instance. Node-ID is allocated automatically via libcanard occupancy tracking. +/// Create a new CAN platform instance. The node-ID will be allocated automatically by libcanard. +/// The constructor will invoke vtable random() and realloc() immediately. /// 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 cy_can_vtable_t* const vtable, void* const user); -cy_can_stats_t cy_can_stats(const cy_platform_t* const base); - /// Returns the user context pointer that was passed to cy_can_new(). void* cy_can_user(const cy_platform_t* const base); From 59698127a95ae44065bcb7cedfb17fca5ae217e8 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 7 Apr 2026 14:11:20 +0300 Subject: [PATCH 07/17] CAN filtering --- cy_can/cy_can.c | 31 +++++++++----- cy_can/cy_can.h | 9 +++++ cy_can/cy_can_socketcan.c | 40 ++++++++++++++++++- .../test_api_can_subscription_revival.cpp | 2 +- 4 files changed, 70 insertions(+), 12 deletions(-) diff --git a/cy_can/cy_can.c b/cy_can/cy_can.c index 80b081b..30d72b0 100644 --- a/cy_can/cy_can.c +++ b/cy_can/cy_can.c @@ -9,7 +9,6 @@ #include "cy_can.h" #include -#include #define RAPIDHASH_COMPACT #include @@ -34,10 +33,8 @@ typedef struct { - cy_platform_t base; // Must be first (upcast pattern). - const cy_can_vtable_t* vtable; - void* user; - uint_least8_t iface_count; + cy_platform_t base; // Must be first (upcast pattern). + uint_least8_t iface_count; canard_t canard; @@ -50,6 +47,9 @@ typedef struct 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 @@ -279,7 +279,15 @@ static bool v_canard_tx(canard_t* const self, return owner->vtable->tx_classic(owner->user, iface_index, extended_can_id, can_data.data, len); } -static const canard_vtable_t canard_vtbl = { .now = v_canard_now, .tx = v_canard_tx, .filter = NULL }; +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 @@ -423,7 +431,7 @@ static cy_subject_writer_t* v_subject_writer_new(cy_platform_t* const base, cons (void)memset(self, 0, sizeof(*self)); self->base.subject_id = subject_id; } - CY_TRACE(owner->base.cy, "CAN writer S%08jx n=%zu", (uintmax_t)subject_id, owner->subject_writer_count); + CY_TRACE(owner->base.cy, "CAN writer S%08jx", (uintmax_t)subject_id); return (cy_subject_writer_t*)self; } @@ -786,6 +794,7 @@ static const cy_platform_vtable_t platform_vtable = { .subject_writer_new 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) { @@ -806,12 +815,14 @@ cy_platform_t* cy_can_new(const uint_least8_t iface_count, self->base.subject_id_modulus = CY_SUBJECT_ID_MODULUS_16bit; self->base.vtable = &platform_vtable; - const bool ok = canard_new(&self->canard, // - &canard_vtbl, + 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), - 0); + filtering_enabled ? filter_count : 0U); if (!ok) { vtable->realloc(user, self, 0); return NULL; diff --git a/cy_can/cy_can.h b/cy_can/cy_can.h index ee98fd0..9b62aa8 100644 --- a/cy_can/cy_can.h +++ b/cy_can/cy_can.h @@ -17,6 +17,7 @@ #pragma once #include +#include #ifdef __cplusplus extern "C" @@ -56,6 +57,11 @@ typedef struct /// 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); @@ -68,9 +74,12 @@ typedef struct /// 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); diff --git a/cy_can/cy_can_socketcan.c b/cy_can/cy_can_socketcan.c index 4afce7a..598b7f3 100644 --- a/cy_can/cy_can_socketcan.c +++ b/cy_can/cy_can_socketcan.c @@ -72,6 +72,8 @@ typedef struct uint64_t prng_state; } socketcan_t; +static const size_t socketcan_filter_count = 511U; + static cy_us_t socketcan_now(void) { struct timespec ts; @@ -173,6 +175,40 @@ static bool v_rx(void* const user, 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; @@ -199,12 +235,14 @@ static uint64_t v_random(void* const user) 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 }; @@ -277,7 +315,7 @@ cy_platform_t* cy_can_socketcan_new(const uint_least8_t iface_count, } 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, vtable, self); + cy_platform_t* const result = cy_can_new(iface_count, tx_queue_capacity, socketcan_filter_count, vtable, self); if (result == NULL) { goto fail; } diff --git a/cy_can/tests/test_api_can_subscription_revival.cpp b/cy_can/tests/test_api_can_subscription_revival.cpp index 2c654af..5493bda 100644 --- a/cy_can/tests/test_api_can_subscription_revival.cpp +++ b/cy_can/tests/test_api_can_subscription_revival.cpp @@ -157,7 +157,7 @@ void fixture_init(fixture_t* const self) self->vtable.realloc = platform_realloc; self->vtable.random = platform_random; - self->platform = cy_can_new(1U, 128U, &self->vtable, &self->io); + self->platform = cy_can_new(1U, 128U, 0U, &self->vtable, &self->io); TEST_ASSERT_NOT_NULL(self->platform); self->cy = cy_new(self->platform, cy_str("test_can"), cy_str_t{ 0U, nullptr }); From d2d93e31f918497b61e91dfa9639d3fe401c49ec Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 7 Apr 2026 14:52:40 +0300 Subject: [PATCH 08/17] restructure platform glue libraries and examples --- README.md | 4 ++ cy_can/CMakeLists.txt | 39 +++++------------ cy_can/cy_can_socketcan.c | 29 ------------- cy_udp_posix/CMakeLists.txt | 31 +++----------- examples/CMakeLists.txt | 45 +++++++++++++------- {cy_udp_posix => examples}/cy_trace_stderr.c | 6 ++- 6 files changed, 55 insertions(+), 99 deletions(-) rename {cy_udp_posix => examples}/cy_trace_stderr.c (99%) diff --git a/README.md b/README.md index a207d06..6038bae 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/`. diff --git a/cy_can/CMakeLists.txt b/cy_can/CMakeLists.txt index e621a54..a10b516 100644 --- a/cy_can/CMakeLists.txt +++ b/cy_can/CMakeLists.txt @@ -21,33 +21,16 @@ set_target_properties( CXX_CPPCHECK "" ) -# Shared configuration for cy_can targets. -function(configure_cy_can_target target) - target_link_libraries(${target} PUBLIC canard) - 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() - -# Platform-agnostic CAN transport (cy_can.c + cy.c). -set(cy_can_sources cy_can.c ../cy/cy.c) - -add_library(cy_can STATIC ${cy_can_sources}) -configure_cy_can_target(cy_can) - -# With Linux SocketCAN support. -add_library(cy_can_socketcan STATIC ${cy_can_sources} cy_can_socketcan.c) -configure_cy_can_target(cy_can_socketcan) - -# Same but with CY_CONFIG_TRACE=1 for examples and tests. -add_library(cy_can_socketcan_trace STATIC ${cy_can_sources} cy_can_socketcan.c) -target_compile_definitions(cy_can_socketcan_trace PUBLIC CY_CONFIG_TRACE=1) -configure_cy_can_target(cy_can_socketcan_trace) +# 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. +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) # TODO build the tests diff --git a/cy_can/cy_can_socketcan.c b/cy_can/cy_can_socketcan.c index 598b7f3..584585e 100644 --- a/cy_can/cy_can_socketcan.c +++ b/cy_can/cy_can_socketcan.c @@ -35,35 +35,6 @@ #include #include -#if CY_CONFIG_TRACE -#include -#include - -void cy_trace(cy_t* const cy, - const char* const file, - const uint_fast16_t line, - const char* const func, - const char* const format, - ...) -{ - (void)cy; - struct timespec ts; - (void)clock_gettime(CLOCK_MONOTONIC, &ts); - const char* fn = strrchr(file, '/'); - fn = (fn != NULL) ? (fn + 1) : file; - (void)fprintf( - stderr, "CY_CAN %05ld.%06ld %s:%u:%s ", (long)ts.tv_sec, ts.tv_nsec / 1000L, fn, (unsigned)line, func); - va_list args; - va_start(args, format); - (void)vfprintf(stderr, format, args); - va_end(args); - (void)fputc('\n', stderr); - (void)fflush(stderr); -} -#endif - -// ===================================================================================================================== - typedef struct { int sock_fd[CANARD_IFACE_COUNT]; diff --git a/cy_udp_posix/CMakeLists.txt b/cy_udp_posix/CMakeLists.txt index 6a6fc00..de6b52c 100644 --- a/cy_udp_posix/CMakeLists.txt +++ b/cy_udp_posix/CMakeLists.txt @@ -21,27 +21,10 @@ 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() - -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) +# 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) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 27efa83..51487ae 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -6,24 +6,37 @@ enable_testing() include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/lib") +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}) +endfunction() + # 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) +cy_add_example(udp_pub cy_udp_posix main_udp_pub.c) +cy_add_example(udp_sub cy_udp_posix main_udp_sub.c) +cy_add_example(udp_time_pub cy_udp_posix main_udp_time_pub.c) +cy_add_example(udp_echo cy_udp_posix main_udp_echo.c) # 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) +cy_add_example(udp_file_server cy_udp_posix main_udp_file_server.c) +cy_add_example(udp_file_client cy_udp_posix main_udp_file_client.c) # 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) +cy_add_example(udp_streaming_server cy_udp_posix main_udp_streaming_server.c) +cy_add_example(udp_streaming_client cy_udp_posix main_udp_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, From 83f5219562b649625df6f9e401da3903edccb3df Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 7 Apr 2026 15:01:26 +0300 Subject: [PATCH 09/17] README --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6038bae..b5f31c6 100644 --- a/README.md +++ b/README.md @@ -53,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) { @@ -62,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. From e172091d88c5ff52acc47baf90ff49c5927b7787 Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 7 Apr 2026 16:11:08 +0300 Subject: [PATCH 10/17] Add independent cy_can API test suite --- CMakeLists.txt | 1 + cy_can/CMakeLists.txt | 2 +- cy_can/cy_can.c | 7 +- cy_can/cy_can_socketcan.c | 2 +- cy_can/cy_can_socketcan.h | 2 +- cy_can/tests/CMakeLists.txt | 118 +++++ cy_can/tests/test_api_can_constructor.c | 161 ++++++ cy_can/tests/test_api_can_failures.c | 115 ++++ cy_can/tests/test_api_can_pubsub.c | 235 +++++++++ .../tests/test_api_can_redundancy_lifecycle.c | 219 ++++++++ cy_can/tests/test_api_can_reliable_rpc.c | 261 +++++++++ cy_can/tests/test_api_can_socketcan_e2e.c | 228 ++++++++ .../test_api_can_subscription_revival.cpp | 332 ------------ cy_can/tests/test_support.c | 498 ++++++++++++++++++ cy_can/tests/test_support.h | 114 ++++ 15 files changed, 1957 insertions(+), 338 deletions(-) create mode 100644 cy_can/tests/CMakeLists.txt create mode 100644 cy_can/tests/test_api_can_constructor.c create mode 100644 cy_can/tests/test_api_can_failures.c create mode 100644 cy_can/tests/test_api_can_pubsub.c create mode 100644 cy_can/tests/test_api_can_redundancy_lifecycle.c create mode 100644 cy_can/tests/test_api_can_reliable_rpc.c create mode 100644 cy_can/tests/test_api_can_socketcan_e2e.c delete mode 100644 cy_can/tests/test_api_can_subscription_revival.cpp create mode 100644 cy_can/tests/test_support.c create mode 100644 cy_can/tests/test_support.h diff --git a/CMakeLists.txt b/CMakeLists.txt index add8bd7..103fa00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ 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_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] diff --git a/cy_can/CMakeLists.txt b/cy_can/CMakeLists.txt index a10b516..08038d8 100644 --- a/cy_can/CMakeLists.txt +++ b/cy_can/CMakeLists.txt @@ -33,4 +33,4 @@ 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) -# TODO build the tests +add_subdirectory(tests) diff --git a/cy_can/cy_can.c b/cy_can/cy_can.c index 30d72b0..9f32037 100644 --- a/cy_can/cy_can.c +++ b/cy_can/cy_can.c @@ -242,11 +242,12 @@ static can_message_t* make_message(cy_can_t* const owner, const size_t inline_ex // ===================================================================================================================== // 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) { - 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), ""); return (canard_bytes_chain_t){ .bytes = { .size = message.size, .data = message.data }, .next = (const canard_bytes_chain_t*)message.next }; } diff --git a/cy_can/cy_can_socketcan.c b/cy_can/cy_can_socketcan.c index 584585e..0dce28f 100644 --- a/cy_can/cy_can_socketcan.c +++ b/cy_can/cy_can_socketcan.c @@ -222,7 +222,7 @@ static const cy_can_vtable_t socketcan_vtable_classic = { .tx_classic = v_tx_cla // PUBLIC API cy_platform_t* cy_can_socketcan_new(const uint_least8_t iface_count, - const char* iface_names[], + const char* const iface_names[], const size_t tx_queue_capacity) { if ((iface_count == 0) || (iface_count > CANARD_IFACE_COUNT) || (iface_names == NULL)) { diff --git a/cy_can/cy_can_socketcan.h b/cy_can/cy_can_socketcan.h index 35e52c9..ef60f25 100644 --- a/cy_can/cy_can_socketcan.h +++ b/cy_can/cy_can_socketcan.h @@ -23,7 +23,7 @@ extern "C" /// 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* iface_names[], + const char* const iface_names[], const size_t tx_queue_capacity); void cy_can_socketcan_destroy(cy_platform_t* const base); diff --git a/cy_can/tests/CMakeLists.txt b/cy_can/tests/CMakeLists.txt new file mode 100644 index 0000000..12b3628 --- /dev/null +++ b/cy_can/tests/CMakeLists.txt @@ -0,0 +1,118 @@ +# 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_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_subscription_revival.cpp b/cy_can/tests/test_api_can_subscription_revival.cpp deleted file mode 100644 index 5493bda..0000000 --- a/cy_can/tests/test_api_can_subscription_revival.cpp +++ /dev/null @@ -1,332 +0,0 @@ -#include -#include -#include "guarded_heap.h" -#include -#include -#include -#include - -namespace { - -constexpr cy_us_t spin_slice_us = 10'000; -constexpr std::size_t queue_capacity = 512U; - -struct queued_frame_t -{ - std::uint32_t can_id{ 0U }; - std::uint_least8_t len{ 0U }; - bool fd{ false }; - std::array data{}; -}; - -struct test_platform_t -{ - guarded_heap_t heap{}; - std::uint64_t random_state{ UINT64_C(0x123456789ABCDEF0) }; - cy_us_t now{ 1000 }; - std::size_t tx_classic_count{ 0U }; - std::size_t rx_call_count{ 0U }; - std::size_t queue_head{ 0U }; - std::size_t queue_tail{ 0U }; - std::size_t queued_count{ 0U }; - std::array queue{}; -}; - -struct fixture_t -{ - test_platform_t io{}; - cy_can_vtable_t vtable{}; - cy_platform_t* platform{ nullptr }; - cy_t* cy{ nullptr }; -}; - -template -std::array make_payload(const unsigned char seed) -{ - std::array out{}; - for (std::size_t i = 0U; i < out.size(); i++) { - out.at(i) = static_cast(seed + static_cast(i)); - } - return out; -} - -void enqueue_frame(test_platform_t* const self, - const std::uint32_t can_id, - const bool fd, - const void* const data, - const std::uint_least8_t len) -{ - TEST_ASSERT_NOT_NULL(self); - TEST_ASSERT_TRUE(self->queued_count < self->queue.size()); - queued_frame_t& out = self->queue.at(self->queue_tail); - out.can_id = can_id; - out.len = len; - out.fd = fd; - if (len > 0U) { - (void)std::memcpy(out.data.data(), data, len); - } - self->queue_tail = (self->queue_tail + 1U) % self->queue.size(); - self->queued_count++; -} - -bool dequeue_frame(test_platform_t* const self, cy_can_rx_t* const out_frame) -{ - TEST_ASSERT_NOT_NULL(self); - TEST_ASSERT_NOT_NULL(out_frame); - if (self->queued_count == 0U) { - return false; - } - const queued_frame_t& in = self->queue.at(self->queue_head); - out_frame->timestamp = self->now++; - out_frame->can_id = in.can_id; - out_frame->iface_index = 0U; - out_frame->len = in.len; - out_frame->fd = in.fd; - if (in.len > 0U) { - (void)std::memcpy(out_frame->data, in.data.data(), in.len); - } - self->queue_head = (self->queue_head + 1U) % self->queue.size(); - self->queued_count--; - return true; -} - -extern "C" bool platform_tx_classic(void* const user, - const std::uint_least8_t iface_index, - const std::uint32_t can_id, - const void* const data, - const std::uint_least8_t len) -{ - auto* const self = static_cast(user); - TEST_ASSERT_NOT_NULL(self); - TEST_ASSERT_EQUAL_UINT8(0U, iface_index); - TEST_ASSERT_TRUE(len <= 8U); - self->tx_classic_count++; - enqueue_frame(self, can_id, false, data, len); - return true; -} - -extern "C" bool platform_rx(void* const user, - cy_can_rx_t* const out_frame, - const cy_us_t deadline, - const std::uint_least8_t tx_pending_iface_bitmap) -{ - (void)tx_pending_iface_bitmap; - auto* const self = static_cast(user); - TEST_ASSERT_NOT_NULL(self); - self->rx_call_count++; - if (dequeue_frame(self, out_frame)) { - return true; - } - if (self->now <= deadline) { - self->now = deadline + 1; - } - return false; -} - -extern "C" cy_us_t platform_now(void* const user) -{ - const auto* const self = static_cast(user); - TEST_ASSERT_NOT_NULL(self); - return self->now; -} - -extern "C" void* platform_realloc(void* const user, void* const ptr, const std::size_t size) -{ - return guarded_heap_realloc(user, ptr, size); -} - -extern "C" std::uint64_t platform_random(void* const user) -{ - auto* const self = static_cast(user); - TEST_ASSERT_NOT_NULL(self); - self->random_state = (self->random_state * UINT64_C(6364136223846793005)) + UINT64_C(1); - return self->random_state; -} - -void fixture_init(fixture_t* const self) -{ - TEST_ASSERT_NOT_NULL(self); - *self = fixture_t{}; - guarded_heap_init(&self->io.heap, UINT64_C(0xA5A55A5ADEADBEEF)); - self->io.now = 1000; - - self->vtable.tx_classic = platform_tx_classic; - self->vtable.tx_fd = nullptr; - self->vtable.rx = platform_rx; - self->vtable.now = platform_now; - self->vtable.realloc = platform_realloc; - self->vtable.random = platform_random; - - self->platform = cy_can_new(1U, 128U, 0U, &self->vtable, &self->io); - TEST_ASSERT_NOT_NULL(self->platform); - - self->cy = cy_new(self->platform, cy_str("test_can"), cy_str_t{ 0U, nullptr }); - TEST_ASSERT_NOT_NULL(self->cy); -} - -void fixture_deinit(fixture_t* const self) -{ - TEST_ASSERT_NOT_NULL(self); - if (self->cy != nullptr) { - cy_destroy(self->cy); - self->cy = nullptr; - } - if (self->platform != nullptr) { - cy_can_destroy(self->platform); - self->platform = nullptr; - } - TEST_ASSERT_EQUAL_size_t(0U, guarded_heap_allocated_fragments(&self->io.heap)); - TEST_ASSERT_EQUAL_size_t(0U, guarded_heap_allocated_bytes(&self->io.heap)); -} - -void spin_until_done(fixture_t* const self, cy_future_t* const future) -{ - TEST_ASSERT_NOT_NULL(self); - TEST_ASSERT_NOT_NULL(future); - for (std::size_t i = 0U; (i < 16U) && !cy_future_done(future); i++) { - TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(self->cy, cy_now(self->cy) + spin_slice_us)); - } - TEST_ASSERT_TRUE(cy_future_done(future)); - TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(future)); -} - -void spin_once(fixture_t* const self) -{ - TEST_ASSERT_NOT_NULL(self); - TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(self->cy, cy_now(self->cy))); -} - -template -void publish_and_expect_payload(fixture_t* const self, - cy_publisher_t* const pub, - cy_future_t* const sub, - const std::array& payload) -{ - TEST_ASSERT_NOT_NULL(self); - TEST_ASSERT_NOT_NULL(pub); - TEST_ASSERT_NOT_NULL(sub); - self->io.tx_classic_count = 0U; - - const cy_bytes_t msg = { payload.size(), payload.data(), nullptr }; - TEST_ASSERT_EQUAL_INT(CY_OK, cy_publish(pub, cy_now(self->cy) + (10U * spin_slice_us), msg)); - spin_until_done(self, sub); - TEST_ASSERT_TRUE(self->io.tx_classic_count > 0U); - TEST_ASSERT_EQUAL_UINT64(1U, cy_arrival_count(sub)); - - const cy_arrival_t arrival = cy_arrival_borrow(sub); - TEST_ASSERT_NOT_NULL(arrival.message.content); - TEST_ASSERT_EQUAL_size_t(payload.size(), cy_message_size(arrival.message.content)); - - std::array restored{}; - TEST_ASSERT_EQUAL_size_t(payload.size(), - cy_message_read(arrival.message.content, 0U, restored.size(), restored.data())); - TEST_ASSERT_EQUAL_UINT8_ARRAY(payload.data(), restored.data(), payload.size()); -} - -void test_api_can_revives_verbatim_reader_and_preserves_extent() -{ - fixture_t fix{}; - fixture_init(&fix); - - static const char* const topic_name = "test/can/revive/verbatim"; - constexpr std::size_t extent_old = 128U; - constexpr std::size_t extent_new = 64U; - const auto payload = make_payload<96U>(0x10U); - - const std::size_t base_readers = cy_can_stats(fix.platform).subject_reader_count; - TEST_ASSERT_EQUAL_size_t(1U, base_readers); - - cy_future_t* const first = cy_subscribe(fix.cy, cy_str(topic_name), extent_old); - TEST_ASSERT_NOT_NULL(first); - const std::size_t readers_after_first = cy_can_stats(fix.platform).subject_reader_count; - TEST_ASSERT_TRUE(readers_after_first > base_readers); - - cy_future_destroy(first); - spin_once(&fix); - TEST_ASSERT_EQUAL_size_t(readers_after_first, cy_can_stats(fix.platform).subject_reader_count); - - cy_future_t* const second = cy_subscribe(fix.cy, cy_str(topic_name), extent_new); - TEST_ASSERT_NOT_NULL(second); - TEST_ASSERT_EQUAL_size_t(readers_after_first, cy_can_stats(fix.platform).subject_reader_count); - - cy_publisher_t* const pub = cy_advertise(fix.cy, cy_str(topic_name)); - TEST_ASSERT_NOT_NULL(pub); - publish_and_expect_payload(&fix, pub, second, payload); - - cy_unadvertise(pub); - cy_future_destroy(second); - spin_once(&fix); - - fixture_deinit(&fix); -} - -void test_api_can_revives_pinned_reader_and_preserves_extent() -{ - fixture_t fix{}; - fixture_init(&fix); - - static const char* const topic_name = "123#123"; - constexpr std::size_t extent_old = 128U; - constexpr std::size_t extent_new = 64U; - const auto payload = make_payload<96U>(0x40U); - - const std::size_t base_readers = cy_can_stats(fix.platform).subject_reader_count; - TEST_ASSERT_EQUAL_size_t(1U, base_readers); - - cy_future_t* const first = cy_subscribe(fix.cy, cy_str(topic_name), extent_old); - TEST_ASSERT_NOT_NULL(first); - const std::size_t readers_after_first = cy_can_stats(fix.platform).subject_reader_count; - TEST_ASSERT_TRUE(readers_after_first > base_readers); - - cy_future_destroy(first); - spin_once(&fix); - TEST_ASSERT_EQUAL_size_t(readers_after_first, cy_can_stats(fix.platform).subject_reader_count); - - cy_future_t* const second = cy_subscribe(fix.cy, cy_str(topic_name), extent_new); - TEST_ASSERT_NOT_NULL(second); - TEST_ASSERT_EQUAL_size_t(readers_after_first, cy_can_stats(fix.platform).subject_reader_count); - - cy_publisher_t* const pub = cy_advertise(fix.cy, cy_str(topic_name)); - TEST_ASSERT_NOT_NULL(pub); - publish_and_expect_payload(&fix, pub, second, payload); - - cy_unadvertise(pub); - cy_future_destroy(second); - spin_once(&fix); - - fixture_deinit(&fix); -} - -void test_api_can_reuses_tombstoned_broadcast_reader_after_cy_destroy() -{ - fixture_t fix{}; - fixture_init(&fix); - - const std::size_t base_readers = cy_can_stats(fix.platform).subject_reader_count; - TEST_ASSERT_EQUAL_size_t(1U, base_readers); - - cy_destroy(fix.cy); - fix.cy = nullptr; - TEST_ASSERT_EQUAL_size_t(base_readers, cy_can_stats(fix.platform).subject_reader_count); - - fix.cy = cy_new(fix.platform, cy_str("test_can"), cy_str_t{ 0U, nullptr }); - TEST_ASSERT_NOT_NULL(fix.cy); - TEST_ASSERT_EQUAL_size_t(base_readers, cy_can_stats(fix.platform).subject_reader_count); - - fixture_deinit(&fix); -} - -} // namespace - -extern "C" void setUp() {} - -extern "C" void tearDown() {} - -int main() -{ - UNITY_BEGIN(); - RUN_TEST(test_api_can_revives_verbatim_reader_and_preserves_extent); - RUN_TEST(test_api_can_revives_pinned_reader_and_preserves_extent); - RUN_TEST(test_api_can_reuses_tombstoned_broadcast_reader_after_cy_destroy); - 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 From a67e98f6ed18d9f7593c37243870f96c7a389c60 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 7 Apr 2026 16:18:56 +0300 Subject: [PATCH 11/17] remove is_tombstoned() --- cy_can/cy_can.c | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/cy_can/cy_can.c b/cy_can/cy_can.c index 9f32037..ea220b1 100644 --- a/cy_can/cy_can.c +++ b/cy_can/cy_can.c @@ -521,15 +521,9 @@ static void reader_set_extent(subject_reader_t* const self, const size_t extent) } } -static bool is_tombstoned(const cy_can_t* const owner, const subject_reader_t* const self) -{ - return (self->next_tombstone != NULL) || (self->prev_tombstone != NULL) || (owner->tombstone_head == self) || - (owner->tombstone_tail == self); -} - static void tombstone_remove(cy_can_t* const owner, subject_reader_t* const self) { - assert((owner != NULL) && (self != NULL) && is_tombstoned(owner, self)); + assert((owner != NULL) && (self != NULL)); if (self->prev_tombstone != NULL) { self->prev_tombstone->next_tombstone = self->next_tombstone; } else { @@ -546,7 +540,7 @@ static void tombstone_remove(cy_can_t* const owner, subject_reader_t* const self static void tombstone_enqueue(cy_can_t* const owner, subject_reader_t* const self) { - assert((owner != NULL) && (self != NULL) && !is_tombstoned(owner, self)); + assert((owner != NULL) && (self != NULL)); self->prev_tombstone = owner->tombstone_tail; self->next_tombstone = NULL; if (owner->tombstone_tail != NULL) { @@ -574,8 +568,7 @@ static subject_reader_t* reader_try_revive(cy_can_t* const owner, const uint32_t 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) && - is_tombstoned(owner, self)); + 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); @@ -594,7 +587,7 @@ static subject_reader_t* reader_try_revive(cy_can_t* const owner, const uint32_t /// 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) && !is_tombstoned(owner, 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) { From 987213dc9cfd462dd5cb9c4c7a6e0356c7a8b477 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 7 Apr 2026 16:47:08 +0300 Subject: [PATCH 12/17] update libcanard --- lib/libcanard | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/libcanard b/lib/libcanard index c719fae..f5a00fc 160000 --- a/lib/libcanard +++ b/lib/libcanard @@ -1 +1 @@ -Subproject commit c719fae8d89c9d2b58df7386aeac7bd631186e12 +Subproject commit f5a00fc64ba9898948a4a7504bb1d16d87a946d6 From 65ad5ea123bec42d4df2ff60136b3354c7eb7fc7 Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 7 Apr 2026 18:18:40 +0300 Subject: [PATCH 13/17] Cy UDP POSIX tests --- CMakeLists.txt | 1 + cy_udp_posix/CMakeLists.txt | 2 + cy_udp_posix/cy_udp_posix.c | 21 ++- cy_udp_posix/cy_udp_posix.h | 2 +- cy_udp_posix/eui64.h | 45 +---- cy_udp_posix/tests/CMakeLists.txt | 94 ++++++++++ cy_udp_posix/tests/test_api_udp_posix_basic.c | 93 ++++++++++ .../tests/test_api_udp_posix_default_ctor.c | 52 ++++++ .../tests/test_api_udp_posix_pubsub.c | 136 +++++++++++++++ .../tests/test_api_udp_posix_rpc_lifecycle.c | 161 ++++++++++++++++++ cy_udp_posix/tests/test_support.c | 134 +++++++++++++++ cy_udp_posix/tests/test_support.h | 43 +++++ 12 files changed, 740 insertions(+), 44 deletions(-) create mode 100644 cy_udp_posix/tests/CMakeLists.txt create mode 100644 cy_udp_posix/tests/test_api_udp_posix_basic.c create mode 100644 cy_udp_posix/tests/test_api_udp_posix_default_ctor.c create mode 100644 cy_udp_posix/tests/test_api_udp_posix_pubsub.c create mode 100644 cy_udp_posix/tests/test_api_udp_posix_rpc_lifecycle.c create mode 100644 cy_udp_posix/tests/test_support.c create mode 100644 cy_udp_posix/tests/test_support.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 103fa00..158ccdc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ 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 diff --git a/cy_udp_posix/CMakeLists.txt b/cy_udp_posix/CMakeLists.txt index de6b52c..f37c58b 100644 --- a/cy_udp_posix/CMakeLists.txt +++ b/cy_udp_posix/CMakeLists.txt @@ -28,3 +28,5 @@ 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) + +add_subdirectory(tests) diff --git a/cy_udp_posix/cy_udp_posix.c b/cy_udp_posix/cy_udp_posix.c index 4346542..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. @@ -511,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, @@ -519,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. 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 From 58b86f3450c71eccc618f563b50b2cb405cde46d Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 7 Apr 2026 18:30:09 +0300 Subject: [PATCH 14/17] Add CAN tombstone revival tests --- cy_can/tests/CMakeLists.txt | 1 + cy_can/tests/test_api_can_tombstone_revival.c | 222 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 cy_can/tests/test_api_can_tombstone_revival.c diff --git a/cy_can/tests/CMakeLists.txt b/cy_can/tests/CMakeLists.txt index 12b3628..557caef 100644 --- a/cy_can/tests/CMakeLists.txt +++ b/cy_can/tests/CMakeLists.txt @@ -83,6 +83,7 @@ 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") 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(); +} From 5737193fdf2f608580648ca6e0ee9994558b9ff6 Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 7 Apr 2026 22:42:59 +0300 Subject: [PATCH 15/17] examples: support runtime transport selection --- .github/workflows/main.yml | 2 +- README.md | 4 +- examples/CMakeLists.txt | 24 +-- examples/{main_udp_echo.c => example_echo.c} | 17 +- ...dp_file_client.c => example_file_client.c} | 20 +- ...dp_file_server.c => example_file_server.c} | 14 +- examples/example_platform.h | 140 +++++++++++++ examples/{main_udp_pub.c => example_pub.c} | 51 ++--- ...ng_client.c => example_streaming_client.c} | 17 +- ...ng_server.c => example_streaming_server.c} | 13 +- examples/{main_udp_sub.c => example_sub.c} | 48 +---- ...main_udp_time_pub.c => example_time_pub.c} | 12 +- tools/ci_example_smoke.py | 193 ++++++++++++++---- 13 files changed, 381 insertions(+), 174 deletions(-) rename examples/{main_udp_echo.c => example_echo.c} (85%) rename examples/{main_udp_file_client.c => example_file_client.c} (92%) rename examples/{main_udp_file_server.c => example_file_server.c} (91%) create mode 100644 examples/example_platform.h rename examples/{main_udp_pub.c => example_pub.c} (71%) rename examples/{main_udp_streaming_client.c => example_streaming_client.c} (92%) rename examples/{main_udp_streaming_server.c => example_streaming_server.c} (96%) rename examples/{main_udp_sub.c => example_sub.c} (73%) rename examples/{main_udp_time_pub.c => example_time_pub.c} (87%) 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/README.md b/README.md index b5f31c6..e514e0e 100644 --- a/README.md +++ b/README.md @@ -159,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 @@ -264,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/examples/CMakeLists.txt b/examples/CMakeLists.txt index 51487ae..6e5d0f4 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -24,19 +24,19 @@ endif () function(cy_add_example target platform_target) add_executable(${target} ${ARGN}) - target_link_libraries(${target} PRIVATE cy_examples_support ${platform_target}) + target_link_libraries(${target} PRIVATE cy_examples_support cy_udp_posix cy_can_socketcan) endfunction() -# UDP node examples. -cy_add_example(udp_pub cy_udp_posix main_udp_pub.c) -cy_add_example(udp_sub cy_udp_posix main_udp_sub.c) -cy_add_example(udp_time_pub cy_udp_posix main_udp_time_pub.c) -cy_add_example(udp_echo cy_udp_posix main_udp_echo.c) +# 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) -# UDP file transfer examples. -cy_add_example(udp_file_server cy_udp_posix main_udp_file_server.c) -cy_add_example(udp_file_client cy_udp_posix main_udp_file_client.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) -# UDP streaming examples. -cy_add_example(udp_streaming_server cy_udp_posix main_udp_streaming_server.c) -cy_add_example(udp_streaming_client cy_udp_posix main_udp_streaming_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/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..bf629c8 --- /dev/null +++ b/examples/example_platform.h @@ -0,0 +1,140 @@ +/// A simple helper that constructs a cy_platform_t instance based on the command-line arguments. + +#pragma once + +#include +#include +#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 }; + const char* can_iface_name[CANARD_IFACE_COUNT] = { 0 }; + 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) { + 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 { + 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) { + out.platform = cy_can_socketcan_new((uint_least8_t)iface_count, can_iface_name, 1000U); + out.destroy = cy_can_socketcan_destroy; + } 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..c1a98c4 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,21 +90,17 @@ 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"); return 1; @@ -153,10 +129,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, 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..319d369 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,21 +113,17 @@ 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"); return 1; 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/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 From f18c8f8a34c19d5b532656d4d2ce51e77beecafc Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 7 Apr 2026 22:56:13 +0300 Subject: [PATCH 16/17] examples: free config arrays on early exit --- examples/example_pub.c | 3 +++ examples/example_sub.c | 3 +++ 2 files changed, 6 insertions(+) diff --git a/examples/example_pub.c b/examples/example_pub.c index c1a98c4..3716909 100644 --- a/examples/example_pub.c +++ b/examples/example_pub.c @@ -103,6 +103,7 @@ int main(int argc, char* argv[]) 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; } @@ -112,6 +113,7 @@ int main(int argc, char* 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; } } @@ -144,5 +146,6 @@ int main(int argc, char* argv[]) next_publish_at += 5 * MEGA; } } + free(cfg.pubs); return 0; } diff --git a/examples/example_sub.c b/examples/example_sub.c index 319d369..0b9c5b8 100644 --- a/examples/example_sub.c +++ b/examples/example_sub.c @@ -126,6 +126,7 @@ int main(int argc, char* argv[]) 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; } @@ -136,6 +137,7 @@ int main(int argc, char* 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); @@ -149,5 +151,6 @@ int main(int argc, char* argv[]) break; } } + free(cfg.subs); return 0; } From 1da0538891c737374c4319c746e5fd9db146d6be Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Tue, 7 Apr 2026 23:42:57 +0300 Subject: [PATCH 17/17] Guard SocketCAN behind Linux check #yolo cy_can_socketcan is Linux-only but was built/linked unconditionally, breaking configuration on non-Linux hosts. Guard the CMake target with a platform check, use the platform_target parameter in cy_add_example, and wrap SocketCAN references in example_platform.h with #if __linux__. Co-Authored-By: Claude Opus 4.6 (1M context) --- cy_can/CMakeLists.txt | 8 +++++--- examples/CMakeLists.txt | 5 ++++- examples/example_platform.h | 23 +++++++++++++++++------ 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/cy_can/CMakeLists.txt b/cy_can/CMakeLists.txt index 08038d8..00a3db1 100644 --- a/cy_can/CMakeLists.txt +++ b/cy_can/CMakeLists.txt @@ -29,8 +29,10 @@ target_include_directories(cy_can PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOU 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. -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) +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/examples/CMakeLists.txt b/examples/CMakeLists.txt index 6e5d0f4..158a825 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -24,7 +24,10 @@ endif () function(cy_add_example target platform_target) add_executable(${target} ${ARGN}) - target_link_libraries(${target} PRIVATE cy_examples_support cy_udp_posix cy_can_socketcan) + 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. diff --git a/examples/example_platform.h b/examples/example_platform.h index bf629c8..b63b1ce 100644 --- a/examples/example_platform.h +++ b/examples/example_platform.h @@ -3,7 +3,9 @@ #pragma once #include +#if defined(__linux__) #include +#endif #include #include @@ -51,12 +53,14 @@ static inline example_platform_t example_platform_make(int* const argc, char** a return out; } - uint32_t udp_iface_address[CY_UDP_POSIX_IFACE_COUNT_MAX] = { 0 }; - const char* can_iface_name[CANARD_IFACE_COUNT] = { 0 }; - size_t iface_count = 0U; - bool use_udp = false; - bool use_can = false; - int dst = 1; + 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)) { @@ -66,6 +70,7 @@ static inline example_platform_t example_platform_make(int* const argc, char** a 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); @@ -87,6 +92,10 @@ static inline example_platform_t example_platform_make(int* const argc, char** a } 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) { @@ -118,8 +127,10 @@ static inline example_platform_t example_platform_make(int* const argc, char** a *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) {