diff --git a/MIGRATION_v3.x_to_v4.0.md b/MIGRATION_v3.x_to_v4.0.md index 852653c..669ffd0 100644 --- a/MIGRATION_v3.x_to_v4.0.md +++ b/MIGRATION_v3.x_to_v4.0.md @@ -30,6 +30,24 @@ These changes do not affect wire compatibility. ``` - **Usage**: After popping a transmission queue item using `canardTxPop`, use `canardTxFree` to deallocate its memory. +- **`canardTxPoll`**: + - **Description**: A helper function simplifies the transmission process by combining frame retrieval, transmission, and cleanup into a single function. + - **Prototype**: + ```c + int8_t canardTxPoll( + struct CanardTxQueue* const que, + const struct CanardInstance* const ins, + const CanardMicrosecond now_usec, + void* const user_reference, + const CanardTxFrameHandler frame_handler + ``` + - **Purpose**: Streamlines the process of handling frames from the TX queue. + - **Functionality**: + - Retrieves the next frame to be transmitted. + - Invokes a user-provided `frame_handler` to transmit the frame. + - Manages frame cleanup based on the handler's return value. + - Automatically drops timed-out frames if `now_usec` is provided. + ### Modified Functions Several functions have updated prototypes and usage patterns: @@ -49,7 +67,7 @@ Several functions have updated prototypes and usage patterns: - **Changes**: - Replaces `CanardMemoryAllocate` and `CanardMemoryFree` function pointers with a `CanardMemoryResource` struct. -2. **`canardTxInit`**: +1. **`canardTxInit`**: - **Old Prototype**: ```c CanardTxQueue canardTxInit( @@ -66,7 +84,7 @@ Several functions have updated prototypes and usage patterns: - **Changes**: - Adds a `CanardMemoryResource` parameter for memory allocation of payload data. -3. **`canardTxPush`**: +1. **`canardTxPush`**: - **Old Prototype**: ```c int32_t canardTxPush( @@ -84,12 +102,17 @@ Several functions have updated prototypes and usage patterns: const struct CanardInstance* const ins, const CanardMicrosecond tx_deadline_usec, const struct CanardTransferMetadata* const metadata, - const struct CanardPayload payload); + const struct CanardPayload payload, + const CanardMicrosecond now_usec); ``` - **Changes**: - Replaces `payload_size` and `payload` with a single `CanardPayload` struct. + - Adds a `now_usec` parameter for handling timed-out frames. + - **Purpose**: Allows the library to automatically drop frames that have exceeded their transmission deadlines (`tx_deadline_usec`). + - **Behavior**: If `now_usec` is greater than `tx_deadline_usec`, the frames already in the TX queue will be dropped, and the `dropped_frames` counter in `CanardTxQueueStats` will be incremented. + - **Optional Feature**: Passing `0` for `now_usec` disables automatic dropping, maintaining previous behavior. -4. **`canardTxPeek`** and **`canardTxPop`**: +1. **`canardTxPeek`** and **`canardTxPop`**: - The functions now return and accept pointers to mutable `struct CanardTxQueueItem` instead of const pointers. ### Removed Functions @@ -122,6 +145,16 @@ Several functions have updated prototypes and usage patterns: struct CanardInstance { ... }; ``` +- **Function pointers**: + - **Added** `CanardTxFrameHandler` function pointer type. + ```c + typedef int8_t (*CanardTxFrameHandler)( + void* const user_reference, + const CanardMicrosecond deadline_usec, + struct CanardMutableFrame* const frame + ); + ``` + ### Struct Modifications - **`CanardFrame`**: @@ -152,6 +185,7 @@ Several functions have updated prototypes and usage patterns: - **`CanardTxQueue`**: - Includes a `CanardMemoryResource` for payload data allocation. + - Includes a `CanardTxQueueStats` for tracking number of dropped frames. - **`CanardMemoryResource`** and **`CanardMemoryDeleter`**: - New structs to encapsulate memory allocation and deallocation functions along with user references. @@ -177,17 +211,37 @@ Several functions have updated prototypes and usage patterns: void* const pointer); ``` +## Automatic Dropping of Timed-Out Frames + +#### Description + +Frames in the TX queue that have exceeded their `tx_deadline_usec` can now be automatically dropped when `now_usec` is provided to `canardTxPush()` or `canardTxPoll()`. + +- **Benefit**: Reduces the worst-case peak memory footprint. +- **Optional**: Feature can be disabled by passing `0` for `now_usec`. + +#### Migration Steps + +1. **Enable or Disable Automatic Dropping**: + + - **Enable**: Provide the current time to `now_usec` in both `canardTxPush()` and `canardTxPoll()`. + - **Disable**: Pass `0` to `now_usec` to retain manual control. + +2. **Adjust Application Logic**: + + - Monitor the `dropped_frames` counter in `CanardTxQueueStats` if tracking of dropped frames is required. + ## Migration Steps 1. **Update Type Definitions**: - Replace all `typedef`-based enum and struct types with direct `struct` and `enum` declarations. - For example, change `CanardInstance` to `struct CanardInstance`. -2. **Adjust Memory Management Code**: +1. **Adjust Memory Management Code**: - Replace separate memory allocation and deallocation function pointers with `CanardMemoryResource` and `CanardMemoryDeleter` structs. - Update function calls and definitions accordingly. -3. **Modify Function Calls**: +1. **Modify Function Calls**: - Update all function calls to match the new prototypes. - **`canardInit`**: - Before: @@ -223,30 +277,58 @@ Several functions have updated prototypes and usage patterns: .size = payload_size, .data = payload }; - canardTxPush(que, ins, tx_deadline_usec, metadata, payload_struct); + canardTxPush(que, ins, tx_deadline_usec, metadata, payload_struct, now_usec); ``` -4. **Handle New Functions**: +1. **Handle New Functions**: - Use `canardTxFree` to deallocate transmission queue items after popping them. - - Example: - ```c - struct CanardTxQueueItem* item = canardTxPeek(&tx_queue); - while (item != NULL) { - // Transmit the frame... - canardTxPop(&tx_queue, item); - canardTxFree(&tx_queue, &canard_instance, item); - item = canardTxPeek(&tx_queue); - } - ``` + - Example: + ```c + struct CanardTxQueueItem* item = canardTxPeek(&tx_queue); + while (item != NULL) { + // Transmit the frame... + canardTxPop(&tx_queue, item); + canardTxFree(&tx_queue, &canard_instance, item); + item = canardTxPeek(&tx_queue); + } + ``` -5. **Update Struct Field Access**: + - If currently using `canardTxPeek()`, `canardTxPop()`, and `canardTxFree()`, consider replacing that logic with `canardTxPoll()` for simplicity. + - Define a function matching the `CanardTxFrameHandler` signature: + ```c + int8_t myFrameHandler( + void* const user_reference, + const CanardMicrosecond deadline_usec, + struct CanardMutableFrame* frame + ) { + // Implement transmission logic here + // Return positive value on success - the frame will be released + // Return zero to retry later - the frame will stay in the TX queue + // Return negative value on failure - whole transfer (including this frame) will be dropped + } + ``` + - Example: + ```c + // Before + struct CanardTxQueueItem* item = canardTxPeek(queue); + if (item != NULL) { + // Handle deadline + // Transmit item->frame + item = canardTxPop(queue, item); + canardTxFree(queue, instance, item); + } + + // After + int8_t result = canardTxPoll(queue, instance, now_usec, user_reference, myFrameHandler); + ``` +1. **Update Struct Field Access**: - Adjust your code to access struct fields directly, considering the changes in struct definitions. - For example, access `payload.size` instead of `payload_size`. -6. **Adjust Memory Allocation Logic**: +1. **Adjust Memory Allocation Logic**: - Ensure that your memory allocation and deallocation functions conform to the new prototypes. - Pay attention to the additional `size` parameter in the deallocation function. -7. **Test Thoroughly**: +1. **Test Thoroughly**: - After making the changes, thoroughly test your application to ensure that it functions correctly with the new library version. - Pay special attention to memory management and potential leaks. diff --git a/libcanard/canard.c b/libcanard/canard.c index 098d362..563025c 100644 --- a/libcanard/canard.c +++ b/libcanard/canard.c @@ -592,16 +592,44 @@ CANARD_PRIVATE int32_t txPushMultiFrame(struct CanardTxQueue* const que, return out; } +CANARD_PRIVATE void txPopAndFreeTransfer(struct CanardTxQueue* const que, + const struct CanardInstance* const ins, + struct CanardTxQueueItem* const tx_item, + const bool drop_whole_transfer) +{ + CANARD_ASSERT(que != NULL); + CANARD_ASSERT(ins != NULL); + CANARD_ASSERT(tx_item != NULL); + + struct CanardTxQueueItem* next_tx_item = tx_item; + struct CanardTxQueueItem* tx_item_to_free = canardTxPop(que, next_tx_item); + while (NULL != tx_item_to_free) + { + next_tx_item = tx_item_to_free->next_in_transfer; + canardTxFree(que, ins, tx_item_to_free); + + if (!drop_whole_transfer) + { + break; + } + que->stats.dropped_frames++; + + tx_item_to_free = canardTxPop(que, next_tx_item); + } +} + /// Flushes expired transfers by comparing deadline timestamps of the pending transfers with the current time. CANARD_PRIVATE void txFlushExpiredTransfers(struct CanardTxQueue* const que, const struct CanardInstance* const ins, const CanardMicrosecond now_usec) { - struct CanardTxQueueItem* tx_item = NULL; - while (NULL != (tx_item = MUTABLE_CONTAINER_OF( // - struct CanardTxQueueItem, - cavlFindExtremum(que->deadline_root, false), - deadline_base))) + CANARD_ASSERT(que != NULL); + CANARD_ASSERT(ins != NULL); + CANARD_ASSERT(now_usec > 0); + + struct CanardTreeNode* tx_node = cavlFindExtremum(que->deadline_root, false); + struct CanardTxQueueItem* tx_item = MUTABLE_CONTAINER_OF(struct CanardTxQueueItem, tx_node, deadline_base); + while (NULL != tx_item) { if (now_usec <= tx_item->tx_deadline_usec) { @@ -609,15 +637,11 @@ CANARD_PRIVATE void txFlushExpiredTransfers(struct CanardTxQueue* const q break; } - // All frames of the transfer are released at once b/c they all have the same deadline. - struct CanardTxQueueItem* tx_item_to_free = NULL; - while (NULL != (tx_item_to_free = canardTxPop(que, tx_item))) - { - tx_item = tx_item_to_free->next_in_transfer; - canardTxFree(que, ins, tx_item_to_free); + // All frames of the transfer are dropped at once b/c they all have the same deadline. + txPopAndFreeTransfer(que, ins, tx_item, true); // drop the whole transfer - que->stats.dropped_frames++; - } + tx_node = cavlFindExtremum(que->deadline_root, false); + tx_item = MUTABLE_CONTAINER_OF(struct CanardTxQueueItem, tx_node, deadline_base); } } @@ -1157,8 +1181,6 @@ int32_t canardTxPush(struct CanardTxQueue* const que, txFlushExpiredTransfers(que, ins, now_usec); } - (void) now_usec; - int32_t out = -CANARD_ERROR_INVALID_ARGUMENT; if ((ins != NULL) && (que != NULL) && (metadata != NULL) && ((payload.data != NULL) || (0U == payload.size))) { @@ -1202,8 +1224,6 @@ struct CanardTxQueueItem* canardTxPeek(const struct CanardTxQueue* const que) struct CanardTxQueueItem* out = NULL; if (que != NULL) { - // Paragraph 6.7.2.1.15 of the C standard says: - // A pointer to a structure object, suitably converted, points to its initial member, and vice versa. struct CanardTreeNode* const priority_node = cavlFindExtremum(que->priority_root, false); out = MUTABLE_CONTAINER_OF(struct CanardTxQueueItem, priority_node, priority_base); } @@ -1229,7 +1249,7 @@ void canardTxFree(struct CanardTxQueue* const que, const struct CanardInstance* const ins, struct CanardTxQueueItem* item) { - if (item != NULL) + if ((que != NULL) && (ins != NULL) && (item != NULL)) { if (item->frame.payload.data != NULL) { @@ -1242,6 +1262,53 @@ void canardTxFree(struct CanardTxQueue* const que, } } +int8_t canardTxPoll(struct CanardTxQueue* const que, + const struct CanardInstance* const ins, + const CanardMicrosecond now_usec, + void* const user_reference, + const CanardTxFrameHandler frame_handler) +{ + int8_t out = -CANARD_ERROR_INVALID_ARGUMENT; + if ((que != NULL) && (ins != NULL) && (frame_handler != NULL)) + { + // Before peeking a frame to transmit, we need to try to flush any expired transfers. + // This will not only ensure ASAP freeing of the queue capacity, but also makes sure that the following + // `canardTxPeek` will return a not expired item (if any), so we don't need to check the deadline again. + // The flushing is done by comparing deadline timestamps of the pending transfers with the current time, + // which makes sense only if the current time is known (bigger than zero). + if (now_usec > 0) + { + txFlushExpiredTransfers(que, ins, now_usec); + } + + struct CanardTxQueueItem* const tx_item = canardTxPeek(que); + if (tx_item != NULL) + { + // No need to check the deadline again, as we have already flushed all expired transfers. + out = frame_handler(user_reference, tx_item->tx_deadline_usec, &tx_item->frame); + + // We gonna release (pop and free) the frame if the handler returned: + // - either a positive value - the frame has been successfully accepted by the handler; + // - or a negative value - the frame has been rejected by the handler due to a failure. + // Zero value means that the handler cannot accept the frame at the moment, so we keep it in the queue. + if (out != 0) + { + // In case of a failure, it makes sense to drop the whole transfer immediately + // b/c at least this frame has been rejected, so the whole transfer is useless. + const bool drop_whole_transfer = (out < 0); + txPopAndFreeTransfer(que, ins, tx_item, drop_whole_transfer); + } + } + else + { + out = 0; // No frames to transmit. + } + } + + CANARD_ASSERT(out <= 1); + return out; +} + int8_t canardRxAccept(struct CanardInstance* const ins, const CanardMicrosecond timestamp_usec, const struct CanardFrame* const frame, @@ -1261,13 +1328,12 @@ int8_t canardRxAccept(struct CanardInstance* const ins, // This is the reason the function has a logarithmic time complexity of the number of subscriptions. // Note also that this one of the two variable-complexity operations in the RX pipeline; the other one // is memcpy(). Excepting these two cases, the entire RX pipeline contains neither loops nor recursion. - struct CanardRxSubscription* const sub = MUTABLE_CONTAINER_OF( // - struct CanardRxSubscription, - cavlSearch(&ins->rx_subscriptions[(size_t) model.transfer_kind], - &model.port_id, - &rxSubscriptionPredicateOnPortID, - NULL), - base); + struct CanardTreeNode* const sub_node = cavlSearch(&ins->rx_subscriptions[(size_t) model.transfer_kind], + &model.port_id, + &rxSubscriptionPredicateOnPortID, + NULL); + struct CanardRxSubscription* const sub = + MUTABLE_CONTAINER_OF(struct CanardRxSubscription, sub_node, base); if (out_subscription != NULL) { *out_subscription = sub; // Expose selected instance to the caller. @@ -1345,13 +1411,15 @@ int8_t canardRxUnsubscribe(struct CanardInstance* const ins, { CanardPortID port_id_mutable = port_id; - struct CanardRxSubscription* const sub = MUTABLE_CONTAINER_OF( // - struct CanardRxSubscription, - cavlSearch(&ins->rx_subscriptions[tk], &port_id_mutable, &rxSubscriptionPredicateOnPortID, NULL), - base); - if (sub != NULL) + struct CanardTreeNode* const sub_node = cavlSearch( // + &ins->rx_subscriptions[tk], + &port_id_mutable, + &rxSubscriptionPredicateOnPortID, + NULL); + if (sub_node != NULL) { - cavlRemove(&ins->rx_subscriptions[tk], &sub->base); + struct CanardRxSubscription* const sub = MUTABLE_CONTAINER_OF(struct CanardRxSubscription, sub_node, base); + cavlRemove(&ins->rx_subscriptions[tk], sub_node); CANARD_ASSERT(sub->port_id == port_id); out = 1; for (size_t i = 0; i < RX_SESSIONS_PER_SUBSCRIPTION; i++) @@ -1386,12 +1454,14 @@ int8_t canardRxGetSubscription(struct CanardInstance* const ins, { CanardPortID port_id_mutable = port_id; - struct CanardRxSubscription* const sub = MUTABLE_CONTAINER_OF( // - struct CanardRxSubscription, - cavlSearch(&ins->rx_subscriptions[tk], &port_id_mutable, &rxSubscriptionPredicateOnPortID, NULL), - base); - if (sub != NULL) + struct CanardTreeNode* const sub_node = cavlSearch( // + &ins->rx_subscriptions[tk], + &port_id_mutable, + &rxSubscriptionPredicateOnPortID, + NULL); + if (sub_node != NULL) { + struct CanardRxSubscription* const sub = MUTABLE_CONTAINER_OF(struct CanardRxSubscription, sub_node, base); CANARD_ASSERT(sub->port_id == port_id); if (out_subscription != NULL) { diff --git a/libcanard/canard.h b/libcanard/canard.h index 8ef472e..170806e 100644 --- a/libcanard/canard.h +++ b/libcanard/canard.h @@ -313,6 +313,25 @@ struct CanardTxQueueStats size_t dropped_frames; }; +/// The handler function is intended to be invoked from Canard TX polling (see details for the `canardTxPoll()`). +/// +/// The user reference parameter what was passed to canardTxPoll. +/// The return result of the handling operation: +/// - Any positive value: the frame was successfully handled. +/// This indicates that the frame payload was accepted (and its payload ownership could be potentially moved, +/// see `canardTxPeek` for the details), and the frame can be safely removed from the queue. +/// - Zero: the frame was not handled, and so the frame should be kept in the queue. +/// It will be retried on some future `canardTxPoll()` call according to the queue state in the future. +/// This case is useful when TX hardware is busy, and the frame should be retried later. +/// - Any negative value: the frame was rejected due to an unrecoverable failure. +/// This indicates to the caller (`canardTxPoll`) that the frame should be dropped from the queue, +/// as well as all other frames belonging to the same transfer. The `dropped_frames` counter in the TX queue stats +/// will be incremented for each frame dropped in this way. +/// +typedef int8_t (*CanardTxFrameHandler)(void* const user_reference, + const CanardMicrosecond deadline_usec, + struct CanardMutableFrame* const frame); + /// Prioritized transmission queue that keeps CAN frames destined for transmission via one CAN interface. /// Applications with redundant interfaces are expected to have one instance of this type per interface. /// Applications that are not interested in transmission may have zero queues. @@ -523,6 +542,11 @@ struct CanardTxQueue canardTxInit(const size_t capacity, /// be dropped (incrementing `CanardTxQueueStats::dropped_frames` field per such a frame). /// If this timeout behavior is not needed, the timestamp value can be set to zero. /// +/// The described above automatic dropping of timed-out frames was added in the v4 of the library as an optional +/// feature. It is applied only to the frames that are already in the TX queue (not the new ones that are being pushed +/// in this call). The feature can be disabled by passing zero time in the `now_usec` parameter, +/// so that it will be up to the application to track the `tx_deadline_usec` (see `canardTxPeek`). +/// /// The function returns the number of frames enqueued into the prioritized TX queue (which is always a positive /// number) in case of success (so that the application can track the number of items in the TX queue if necessary). /// In case of failure, the function returns a negated error code: either invalid argument or out-of-memory. @@ -567,8 +591,8 @@ int32_t canardTxPush(struct CanardTxQueue* const que, /// /// The timestamp values of returned frames are initialized with tx_deadline_usec from canardTxPush(). /// Timestamps are used to specify the transmission deadline. It is up to the application and/or the media layer -/// to implement the discardment of timed-out transport frames. The library does not check it, so a frame that is -/// already timed out may be returned here. +/// to implement the discardment of timed-out transport frames. The library does not check it in this call, +/// so a frame that is already timed out may be returned here. /// /// If the queue is empty or if the argument is NULL, the returned value is NULL. /// @@ -613,6 +637,31 @@ void canardTxFree(struct CanardTxQueue* const que, const struct CanardInstance* const ins, struct CanardTxQueueItem* const item); +/// This is a helper that combines several Canard TX calls (`canardTxPeek`, `canardTxPop` and `canardTxFree`) +/// into one "polling" algorithm. It simplifies the whole process of transmitting frames to just two function calls: +/// - `canardTxPush` to enqueue the frames +/// - `canardTxPoll` to dequeue, transmit and free a single frame +/// +/// The algorithm implements a typical pattern of de-queuing, transmitting and freeing a TX queue item, +/// as well as handling transmission failures, retries, and deadline timeouts. +/// +/// The function is intended to be periodically called, most probably on a signal that the previous TX frame +/// transmission has been completed, and so the next TX frame (if any) could be polled from the TX queue. +/// +/// The current time is used to determine if the frame has timed out. Use zero value to disable automatic dropping +/// of timed-out frames. The user reference will be passed to the frame handler (see CanardTxFrameHandler), which +/// will be called to transmit the frame. +/// +/// Return value is zero if the queue is empty, +/// or `-CANARD_ERROR_INVALID_ARGUMENT` if there is no (NULL) queue, instance or handler. +/// Otherwise, the value will be from the result of the frame handler call (see CanardTxFrameHandler). +/// +int8_t canardTxPoll(struct CanardTxQueue* const que, + const struct CanardInstance* const ins, + const CanardMicrosecond now_usec, + void* const user_reference, + const CanardTxFrameHandler frame_handler); + /// This function implements the transfer reassembly logic. It accepts a transport frame from any of the redundant /// interfaces, locates the appropriate subscription state, and, if found, updates it. If the frame completed a /// transfer, the return value is 1 (one) and the out_transfer pointer is populated with the parameters of the diff --git a/tests/helpers.hpp b/tests/helpers.hpp index 94d2c26..70fb99d 100644 --- a/tests/helpers.hpp +++ b/tests/helpers.hpp @@ -9,10 +9,14 @@ #include #include #include +#include #include #include +#include +#include #include -#include +#include +#include #include #include @@ -87,7 +91,7 @@ class TestAllocator std::uint8_t* p = nullptr; if ((amount > 0U) && ((getTotalAllocatedAmount() + amount) <= ceiling_)) { - const auto amount_with_canaries = amount + canary_.size() * 2U; + const auto amount_with_canaries = amount + (canary_.size() * 2U); // Clang-tidy complains about manual memory management. Suppressed because we need it for testing purposes. p = static_cast(std::malloc(amount_with_canaries)); // NOLINT if (p == nullptr) @@ -127,7 +131,7 @@ class TestAllocator std::to_string(reinterpret_cast(user_pointer))); } std::generate_n(p - canary_.size(), // Damage the memory to make sure it's not used after deallocation. - amount + canary_.size() * 2U, + amount + (canary_.size() * 2U), []() { return getRandomNatural(256U); }); std::free(p - canary_.size()); // NOLINT we require manual memory management here. allocated_.erase(it); @@ -345,6 +349,26 @@ class TxQueue void freeItem(Instance& ins, CanardTxQueueItem* const item) { canardTxFree(&que_, &ins.getInstance(), item); } + using FrameHandler = std::function; + + [[nodiscard]] auto poll(Instance& ins, const CanardMicrosecond now_usec, FrameHandler frame_handler) + { + if (!frame_handler) + { + return canardTxPoll(&que_, &ins.getInstance(), now_usec, nullptr, nullptr); + } + + return canardTxPoll(&que_, + &ins.getInstance(), + now_usec, + &frame_handler, + [](auto* user_reference, const auto deadline_usec, auto* const frame) -> std::int8_t { + // + const auto* const handler_ptr = static_cast(user_reference); + return (*handler_ptr)(deadline_usec, *frame); + }); + } + [[nodiscard]] auto getSize() const { std::size_t out = 0; diff --git a/tests/test_public_tx.cpp b/tests/test_public_tx.cpp index 3047cfd..b253860 100644 --- a/tests/test_public_tx.cpp +++ b/tests/test_public_tx.cpp @@ -815,7 +815,7 @@ TEST_CASE("TxPayloadOwnership") } } -TEST_CASE("TxFlushExpired") +TEST_CASE("TxPushFlushExpired") { helpers::Instance ins; helpers::TxQueue que{2, CANARD_MTU_CAN_FD}; // Limit capacity at 2 frames. @@ -930,3 +930,333 @@ TEST_CASE("TxFlushExpired") REQUIRE(nullptr == que.peek()); } } + +TEST_CASE("TxPollSingleFrame") +{ + helpers::Instance ins; + helpers::TxQueue que{2, CANARD_MTU_CAN_FD}; // Limit capacity at 2 frames. + + que.setMTU(8); + ins.setNodeID(42); + + auto& tx_alloc = que.getAllocator(); + auto& ins_alloc = ins.getAllocator(); + + std::array payload{}; + std::iota(payload.begin(), payload.end(), 0U); + + REQUIRE(42 == ins.getNodeID()); + REQUIRE(CANARD_MTU_CAN_CLASSIC == que.getMTU()); + REQUIRE(0 == que.getSize()); + REQUIRE(0 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(0 == ins_alloc.getNumAllocatedFragments()); + + CanardMicrosecond now = 10'000'000ULL; // 10s + constexpr CanardMicrosecond deadline = 1'000'000ULL; // 1s + + CanardTransferMetadata meta{}; + + // 1. Push single frame @ 10s + // + meta.priority = CanardPriorityNominal; + meta.transfer_kind = CanardTransferKindMessage; + meta.port_id = 321; + meta.remote_node_id = CANARD_NODE_ID_UNSET; + meta.transfer_id = 21; + REQUIRE(1 == que.push(&ins.getInstance(), now + deadline, meta, {7, payload.data()}, now)); + REQUIRE(1 == que.getSize()); + REQUIRE(1 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(8 == tx_alloc.getTotalAllocatedAmount()); + REQUIRE(1 == ins_alloc.getNumAllocatedFragments()); + REQUIRE(sizeof(CanardTxQueueItem) * 1 == ins_alloc.getTotalAllocatedAmount()); + + // 2. Poll with invalid arguments. + // + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == // null queue + canardTxPoll(nullptr, &ins.getInstance(), 0, nullptr, [](auto*, auto, auto*) -> std::int8_t { return 0; })); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == // null instance + canardTxPoll(&que.getInstance(), nullptr, 0, nullptr, [](auto*, auto, auto*) -> std::int8_t { return 0; })); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == // null handler + canardTxPoll(&que.getInstance(), &ins.getInstance(), 0, nullptr, nullptr)); + + // 3. Poll; emulate media is busy @ 10s + 100us + // + std::size_t total_handler_calls = 0; + REQUIRE(0 == que.poll(ins, now + 100, [&](auto deadline_usec, auto& frame) -> std::int8_t { + // + ++total_handler_calls; + REQUIRE(deadline_usec == now + deadline); + REQUIRE(frame.payload.size == 8); + REQUIRE(frame.payload.allocated_size == 8); + REQUIRE(0 == std::memcmp(frame.payload.data, payload.data(), 7)); + return 0; // Emulate that TX media is busy. + })); + REQUIRE(1 == total_handler_calls); + REQUIRE(1 == que.getSize()); + REQUIRE(1 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(8 == tx_alloc.getTotalAllocatedAmount()); + REQUIRE(1 == ins_alloc.getNumAllocatedFragments()); + REQUIRE(sizeof(CanardTxQueueItem) * 1 == ins_alloc.getTotalAllocatedAmount()); + REQUIRE(0 == que.getInstance().stats.dropped_frames); + + // 4. Poll; emulate media is ready @ 10s + 200us + // + REQUIRE(1 == que.poll(ins, now + 200, [&](auto deadline_usec, auto& frame) -> std::int8_t { + // + ++total_handler_calls; + REQUIRE(deadline_usec == now + deadline); + REQUIRE(frame.payload.size == 8); + REQUIRE(frame.payload.allocated_size == 8); + REQUIRE(0 == std::memcmp(frame.payload.data, payload.data(), 7)); + return 1; // Emulate that TX media accepted the frame. + })); + REQUIRE(2 == total_handler_calls); + REQUIRE(0 == que.getSize()); + REQUIRE(0 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(0 == tx_alloc.getTotalAllocatedAmount()); + REQUIRE(0 == ins_alloc.getNumAllocatedFragments()); + REQUIRE(sizeof(CanardTxQueueItem) * 0 == ins_alloc.getTotalAllocatedAmount()); + REQUIRE(0 == que.getInstance().stats.dropped_frames); + + // 3. Poll when queue is empty @ 10s + 300us + // + REQUIRE(0 == que.poll(ins, now + 300, [&](auto, auto&) -> std::int8_t { + // + ++total_handler_calls; + FAIL("This should not be called."); + return -1; + })); + REQUIRE(2 == total_handler_calls); + REQUIRE(0 == que.getSize()); +} + +TEST_CASE("TxPollMultiFrame") +{ + helpers::Instance ins; + helpers::TxQueue que{2, CANARD_MTU_CAN_FD}; // Limit capacity at 2 frames. + + que.setMTU(8); + ins.setNodeID(42); + + auto& tx_alloc = que.getAllocator(); + auto& ins_alloc = ins.getAllocator(); + + std::array payload{}; + std::iota(payload.begin(), payload.end(), 0U); + + REQUIRE(42 == ins.getNodeID()); + REQUIRE(CANARD_MTU_CAN_CLASSIC == que.getMTU()); + REQUIRE(0 == que.getSize()); + REQUIRE(0 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(0 == ins_alloc.getNumAllocatedFragments()); + + CanardMicrosecond now = 10'000'000ULL; // 10s + constexpr CanardMicrosecond deadline = 1'000'000ULL; // 1s + + CanardTransferMetadata meta{}; + + // 1. Push two frames @ 10s + // + meta.priority = CanardPriorityNominal; + meta.transfer_kind = CanardTransferKindMessage; + meta.port_id = 321; + meta.remote_node_id = CANARD_NODE_ID_UNSET; + meta.transfer_id = 21; + REQUIRE(2 == que.push(&ins.getInstance(), now + deadline, meta, {8, payload.data()}, now)); + REQUIRE(2 == que.getSize()); + REQUIRE(2 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(8 + 4 == tx_alloc.getTotalAllocatedAmount()); + REQUIRE(2 == ins_alloc.getNumAllocatedFragments()); + REQUIRE(sizeof(CanardTxQueueItem) * 2 == ins_alloc.getTotalAllocatedAmount()); + + // 2. Poll 1st frame @ 10s + 100us + // + std::size_t total_handler_calls = 0; + REQUIRE(1 == que.poll(ins, now + 100, [&](auto deadline_usec, auto& frame) -> std::int8_t { + // + ++total_handler_calls; + REQUIRE(deadline_usec == now + deadline); + REQUIRE(frame.payload.size == 8); + REQUIRE(frame.payload.allocated_size == 8); + REQUIRE(0 == std::memcmp(frame.payload.data, payload.data(), 7)); + return 1; + })); + REQUIRE(1 == total_handler_calls); + REQUIRE(1 == que.getSize()); + REQUIRE(1 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(4 == tx_alloc.getTotalAllocatedAmount()); + REQUIRE(1 == ins_alloc.getNumAllocatedFragments()); + REQUIRE(sizeof(CanardTxQueueItem) * 1 == ins_alloc.getTotalAllocatedAmount()); + REQUIRE(0 == que.getInstance().stats.dropped_frames); + + // 3. Poll 2nd frame @ 10s + 200us + // + REQUIRE(1 == que.poll(ins, now + 200, [&](auto deadline_usec, auto& frame) -> std::int8_t { + // + ++total_handler_calls; + REQUIRE(deadline_usec == now + deadline); + REQUIRE(frame.payload.size == 4); + REQUIRE(frame.payload.allocated_size == 4); + REQUIRE(0 == std::memcmp(frame.payload.data, payload.data() + 7, 1)); + return 1; + })); + REQUIRE(2 == total_handler_calls); + REQUIRE(0 == que.getSize()); + REQUIRE(0 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(0 == tx_alloc.getTotalAllocatedAmount()); + REQUIRE(0 == ins_alloc.getNumAllocatedFragments()); + REQUIRE(sizeof(CanardTxQueueItem) * 0 == ins_alloc.getTotalAllocatedAmount()); + REQUIRE(0 == que.getInstance().stats.dropped_frames); +} + +TEST_CASE("TxPollDropFrameOnFailure") +{ + helpers::Instance ins; + helpers::TxQueue que{2, CANARD_MTU_CAN_FD}; // Limit capacity at 2 frames. + + que.setMTU(8); + ins.setNodeID(42); + + auto& tx_alloc = que.getAllocator(); + auto& ins_alloc = ins.getAllocator(); + + std::array payload{}; + std::iota(payload.begin(), payload.end(), 0U); + + REQUIRE(42 == ins.getNodeID()); + REQUIRE(CANARD_MTU_CAN_CLASSIC == que.getMTU()); + REQUIRE(0 == que.getSize()); + REQUIRE(0 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(0 == ins_alloc.getNumAllocatedFragments()); + + constexpr CanardMicrosecond now = 10'000'000ULL; // 10s + constexpr CanardMicrosecond deadline = 1'000'000ULL; // 1s + + CanardTransferMetadata meta{}; + + // 1. Push two frames @ 10s + // + meta.priority = CanardPriorityNominal; + meta.transfer_kind = CanardTransferKindMessage; + meta.port_id = 321; + meta.remote_node_id = CANARD_NODE_ID_UNSET; + meta.transfer_id = 21; + REQUIRE(2 == que.push(&ins.getInstance(), now + deadline, meta, {8, payload.data()}, now)); + REQUIRE(2 == que.getSize()); + REQUIRE(2 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(8 + 4 == tx_alloc.getTotalAllocatedAmount()); + REQUIRE(2 == ins_alloc.getNumAllocatedFragments()); + REQUIRE(sizeof(CanardTxQueueItem) * 2 == ins_alloc.getTotalAllocatedAmount()); + + // 2. Poll 1st frame; emulate media failure @ 10s + 100us + // + std::size_t total_handler_calls = 0; + REQUIRE(-1 == que.poll(ins, now + 100, [&](auto deadline_usec, auto& frame) -> std::int8_t { + // + ++total_handler_calls; + REQUIRE(deadline_usec == now + deadline); + REQUIRE(frame.payload.size == 8); + REQUIRE(frame.payload.allocated_size == 8); + REQUIRE(0 == std::memcmp(frame.payload.data, payload.data(), 7)); + return -1; + })); + REQUIRE(1 == total_handler_calls); + REQUIRE(0 == que.getSize()); + REQUIRE(0 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(0 == tx_alloc.getTotalAllocatedAmount()); + REQUIRE(0 == ins_alloc.getNumAllocatedFragments()); + REQUIRE(sizeof(CanardTxQueueItem) * 0 == ins_alloc.getTotalAllocatedAmount()); + REQUIRE(2 == que.getInstance().stats.dropped_frames); +} + +TEST_CASE("TxPollDropExpired") +{ + helpers::Instance ins; + helpers::TxQueue que{2, CANARD_MTU_CAN_FD}; // Limit capacity at 2 frames. + + que.setMTU(8); + ins.setNodeID(42); + + auto& tx_alloc = que.getAllocator(); + auto& ins_alloc = ins.getAllocator(); + + std::array payload{}; + std::iota(payload.begin(), payload.end(), 0U); + + REQUIRE(42 == ins.getNodeID()); + REQUIRE(CANARD_MTU_CAN_CLASSIC == que.getMTU()); + REQUIRE(0 == que.getSize()); + REQUIRE(0 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(0 == ins_alloc.getNumAllocatedFragments()); + + CanardMicrosecond now = 10'000'000ULL; // 10s + constexpr CanardMicrosecond deadline = 1'000'000ULL; // 1s + + CanardTransferMetadata meta{}; + + // 1. Push nominal priority frame @ 10s + // + meta.priority = CanardPriorityNominal; + meta.transfer_kind = CanardTransferKindMessage; + meta.port_id = 321; + meta.remote_node_id = CANARD_NODE_ID_UNSET; + meta.transfer_id = 21; + REQUIRE(1 == que.push(&ins.getInstance(), now + deadline, meta, {7, payload.data()}, now)); + REQUIRE(1 == que.getSize()); + REQUIRE(1 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(8 == tx_alloc.getTotalAllocatedAmount()); + REQUIRE(1 == ins_alloc.getNumAllocatedFragments()); + REQUIRE(sizeof(CanardTxQueueItem) * 1 == ins_alloc.getTotalAllocatedAmount()); + + // 2. Push high priority frame @ 10s + 1'000us + // + meta.priority = CanardPriorityHigh; + meta.transfer_kind = CanardTransferKindMessage; + meta.port_id = 321; + meta.transfer_id = 22; + REQUIRE(1 == que.push(&ins.getInstance(), now + deadline - 1, meta, {7, payload.data() + 100}, now + 1'000)); + REQUIRE(2 == que.getSize()); + REQUIRE(2 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(8 + 8 == tx_alloc.getTotalAllocatedAmount()); + REQUIRE(2 == ins_alloc.getNumAllocatedFragments()); + REQUIRE(sizeof(CanardTxQueueItem) * 2 == ins_alloc.getTotalAllocatedAmount()); + + // 3. Poll a frame (should be the high priority one); emulate media is busy @ 10s + 2'000us + // + std::size_t total_handler_calls = 0; + REQUIRE(0 == que.poll(ins, now + 2'000, [&](auto deadline_usec, auto& frame) -> std::int8_t { + // + ++total_handler_calls; + REQUIRE(deadline_usec == now + deadline - 1); + REQUIRE(frame.payload.size == 8); + REQUIRE(frame.payload.allocated_size == 8); + REQUIRE(0 == std::memcmp(frame.payload.data, payload.data() + 100, 7)); + return 0; + })); + REQUIRE(1 == total_handler_calls); + REQUIRE(2 == que.getSize()); + REQUIRE(2 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(8 + 8 == tx_alloc.getTotalAllocatedAmount()); + REQUIRE(2 == ins_alloc.getNumAllocatedFragments()); + REQUIRE(sizeof(CanardTxQueueItem) * 2 == ins_alloc.getTotalAllocatedAmount()); + REQUIRE(0 == que.getInstance().stats.dropped_frames); + + // 3. Poll a frame (should be nominal priority one b/c the high has been expired) @ 10s + deadline + // + REQUIRE(1 == que.poll(ins, now + deadline, [&](auto deadline_usec, auto& frame) -> std::int8_t { + // + ++total_handler_calls; + REQUIRE(deadline_usec == now + deadline); + REQUIRE(frame.payload.size == 8); + REQUIRE(frame.payload.allocated_size == 8); + REQUIRE(0 == std::memcmp(frame.payload.data, payload.data(), 7)); + return 1; + })); + REQUIRE(2 == total_handler_calls); + REQUIRE(0 == que.getSize()); + REQUIRE(0 == tx_alloc.getNumAllocatedFragments()); + REQUIRE(0 == tx_alloc.getTotalAllocatedAmount()); + REQUIRE(0 == ins_alloc.getNumAllocatedFragments()); + REQUIRE(sizeof(CanardTxQueueItem) * 0 == ins_alloc.getTotalAllocatedAmount()); + REQUIRE(1 == que.getInstance().stats.dropped_frames); +}