@@ -109,6 +109,8 @@ const uint_least8_t canard_len_to_dlc[65] = {
109109
110110static size_t smaller (const size_t a , const size_t b ) { return (a < b ) ? a : b ; }
111111static size_t larger (const size_t a , const size_t b ) { return (a > b ) ? a : b ; }
112+ static uint64_t min_u64 (const uint64_t a , const uint64_t b ) { return (a < b ) ? a : b ; }
113+ static uint64_t max_u64 (const uint64_t a , const uint64_t b ) { return (a > b ) ? a : b ; }
112114static int64_t min_i64 (const int64_t a , const int64_t b ) { return (a < b ) ? a : b ; }
113115static int64_t max_i64 (const int64_t a , const int64_t b ) { return (a > b ) ? a : b ; }
114116static canard_us_t sooner (const canard_us_t a , const canard_us_t b ) { return min_i64 (a , b ); }
@@ -1171,17 +1173,6 @@ static byte_t rx_parse(const uint32_t can_id,
11711173 return (is_v0 ? 1U : 0U ) | (is_v1 ? 2U : 0U );
11721174}
11731175
1174- // f(2, 3)==31, f(2, 2)==0, f(2, 1)==1
1175- static byte_t rx_transfer_id_forward_difference (const byte_t a , const byte_t b )
1176- {
1177- CANARD_ASSERT ((a <= CANARD_TRANSFER_ID_MAX ) && (b <= CANARD_TRANSFER_ID_MAX ));
1178- int16_t diff = (int16_t )(((int16_t )a ) - ((int16_t )b ));
1179- if (diff < 0 ) {
1180- diff = (int16_t )(diff + (int16_t )(1U << CANARD_TRANSFER_ID_BIT_LENGTH ));
1181- }
1182- return (byte_t )diff ;
1183- }
1184-
11851176// Idle sessions are removed after this timeout even if reassembly is not finished.
11861177// This is not related to the transfer-ID timeout and does not affect the correctness;
11871178// it is only needed to improve the memory footprint when remotes cease sending messages.
@@ -1198,37 +1189,15 @@ static byte_t rx_transfer_id_forward_difference(const byte_t a, const byte_t b)
11981189typedef struct
11991190{
12001191 canard_us_t start_ts ;
1201- size_t total_size ; // The raw payload size before the implicit truncation and CRC removal.
1192+ uint32_t total_size ; // The raw payload size seen before the implicit truncation and CRC removal.
12021193 uint16_t crc ;
12031194 byte_t transfer_id : CANARD_TRANSFER_ID_BIT_LENGTH ;
1204- byte_t expected_toggle : 1 ;
12051195 byte_t iface_index : IFACE_INDEX_BIT_LENGTH ;
1196+ byte_t expected_toggle : 1 ;
12061197 byte_t payload []; // Extent-sized.
12071198} rx_slot_t ;
12081199#define RX_SLOT_OVERHEAD (offsetof(rx_slot_t, payload))
1209-
1210- // Up to libcanard v4 we used a fixed-capacity array of pointers for per-remote sessions for constant-time lookup,
1211- // but it was too costly on MCUs: with a 32-bit pointer it took 512 bytes for the array plus overheads,
1212- // resulting in 1 KiB o1heap blocks per session, very expensive. Here we use a much less RAM-heavy approach with
1213- // sparse nodes in a tree with log-time lookup.
1214- //
1215- // Core invariants:
1216- // - Only start-of-transfer may create/replace a slot.
1217- // - Non-start frames never create state.
1218- // - Session dedup state is updated on admitted start-of-transfer, not on transfer completion.
1219- // - Timeout is consulted only for start-of-transfer admission.
1220- // - Slot matching for continuation uses exact match: priority, transfer-ID, toggle, and iface.
1221- typedef struct
1222- {
1223- canard_tree_t index ;
1224- canard_listed_t list_animation ; // On update, session moved to the tail; oldest pushed to the head.
1225- canard_us_t last_admitted_start_ts ;
1226- rx_slot_t * slots [CANARD_PRIO_COUNT ]; // Indexed by priority level to allow preemption.
1227- canard_subscription_t * owner ;
1228- byte_t last_admitted_transfer_id ;
1229- byte_t node_id ;
1230- } rx_session_t ;
1231- static_assert ((sizeof (void * ) > 4 ) || (sizeof (rx_session_t ) <= 120 ), "too large" );
1200+ static_assert (RX_SLOT_OVERHEAD <= 16 , "unexpected layout" );
12321201
12331202static rx_slot_t * rx_slot_new (const canard_subscription_t * const sub ,
12341203 const canard_us_t start_ts ,
@@ -1252,6 +1221,84 @@ static void rx_slot_destroy(const canard_subscription_t* const sub, rx_slot_t* c
12521221 mem_free (sub -> owner -> mem .rx_payload , RX_SLOT_OVERHEAD + sub -> extent , slot );
12531222}
12541223
1224+ static void rx_slot_store (rx_slot_t * const slot , const size_t extent , const canard_bytes_t payload )
1225+ {
1226+ if (slot -> total_size < extent ) {
1227+ const size_t copy_size = smaller (payload .size , (size_t )(extent - slot -> total_size ));
1228+ (void )memcpy (& slot -> payload [slot -> total_size ], payload .data , copy_size );
1229+ }
1230+ slot -> total_size = (uint32_t )(slot -> total_size + payload .size ); // Before truncation.
1231+ }
1232+
1233+ // A compact representation is needed because we need to store an array of these in dynamic memory.
1234+ typedef struct
1235+ {
1236+ uint16_t limbs [3 ];
1237+ } rx_seqno_packed_t ;
1238+
1239+ static uint64_t rx_seqno_unpack (const rx_seqno_packed_t v )
1240+ {
1241+ return ((uint64_t )v .limbs [0 ]) | (((uint64_t )v .limbs [1 ]) << 16U ) | (((uint64_t )v .limbs [2 ]) << 32U );
1242+ }
1243+ static rx_seqno_packed_t rx_seqno_pack (const uint64_t v )
1244+ {
1245+ return (rx_seqno_packed_t ){
1246+ { (uint16_t )(v & 0xFFFFU ), (uint16_t )((v >> 16U ) & 0xFFFFU ), (uint16_t )((v >> 32U ) & 0xFFFFU ) }
1247+ };
1248+ }
1249+ static uint64_t rx_seqno_linearize (const uint64_t ref_seqno , const byte_t transfer_id )
1250+ {
1251+ CANARD_ASSERT (transfer_id <= CANARD_TRANSFER_ID_MAX );
1252+ const byte_t ref_tid = (byte_t )(ref_seqno & CANARD_TRANSFER_ID_MAX );
1253+ int16_t delta = ((int16_t )transfer_id ) - ((int16_t )ref_tid ); // NOLINT(*-narrowing-conversions)
1254+ // Select the nearest congruent seqno in the half-open interval [-16, +16) around the reference.
1255+ if (delta > (int16_t )(CANARD_TRANSFER_ID_MAX / 2U )) {
1256+ delta -= (int16_t )(CANARD_TRANSFER_ID_MAX + 1U );
1257+ } else if (delta < - ((int16_t )(CANARD_TRANSFER_ID_MAX / 2U ) + 1 )) {
1258+ delta += (int16_t )(CANARD_TRANSFER_ID_MAX + 1U );
1259+ }
1260+ // Near the origin the backward representative may underflow, so use the next forward one instead.
1261+ if ((delta < 0 ) && (ref_seqno < (uint64_t )(- delta ))) {
1262+ delta += (int16_t )(CANARD_TRANSFER_ID_MAX + 1U );
1263+ }
1264+ return (uint64_t )(((int64_t )ref_seqno ) + ((int64_t )delta ));
1265+ }
1266+
1267+ // Up to libcanard v4 we used a fixed-capacity array of pointers for per-remote sessions for constant-time lookup,
1268+ // but it was too costly on MCUs: with a 32-bit pointer it took 512 bytes for the array plus overheads,
1269+ // resulting in 1 KiB o1heap blocks per session, very expensive. Here we use a much less RAM-heavy approach with
1270+ // sparse nodes in a tree with log-time lookup.
1271+ //
1272+ // Design goals:
1273+ //
1274+ // - Admit frames from a single interface only, arbitrarily chosen, until it has been observed to be silent for
1275+ // at least one transfer-ID timeout period. This is because redundant interfaces may carry frames with a significant
1276+ // delay, which may cause a receiver to admit the same transfer multiple times without interface affinity.
1277+ //
1278+ // - Allow preemption of transfers by higher-priority ones, without loss of the preempted transfer's state.
1279+ //
1280+ // - The case of a zero transfer-ID timeout is a first-class use case. In this mode, duplication is tolerated by
1281+ // the application, but multi-frame transfers must still follow interface affinity to avoid incorrect reassembly.
1282+ //
1283+ // Core invariants:
1284+ // - Only start-of-transfer may create/replace a slot.
1285+ // - Non-start frames never create state.
1286+ // - Session dedup state is updated on admitted start-of-transfer, not on transfer completion.
1287+ // - Timeout is consulted only for start-of-transfer admission.
1288+ // - Slot matching for continuation uses exact match: priority, transfer-ID/seqno, toggle, and iface.
1289+ typedef struct
1290+ {
1291+ canard_tree_t index ;
1292+ canard_listed_t list_animation ; // On update, session moved to the tail; oldest pushed to the head.
1293+ canard_us_t last_admitted_start_ts ;
1294+ rx_slot_t * slots [CANARD_PRIO_COUNT ]; // Indexed by priority level to allow preemption.
1295+ canard_subscription_t * owner ;
1296+ rx_seqno_packed_t seqno_frontier [CANARD_PRIO_COUNT ];
1297+ byte_t iface_index ;
1298+ byte_t node_id ;
1299+ } rx_session_t ;
1300+ static_assert ((sizeof (void * ) > 4 ) || (sizeof (rx_session_t ) <= 120 ), "too large" );
1301+
12551302static int32_t rx_session_cavl_compare (const void * const user , const canard_tree_t * const node )
12561303{
12571304 return ((int32_t )(* (byte_t * )user )) - ((int32_t )CAVL2_TO_OWNER (node , rx_session_t , index )-> node_id );
@@ -1315,13 +1362,61 @@ static size_t rx_session_scan(rx_session_t* const ses, const canard_us_t now)
13151362 return n_slots ;
13161363}
13171364
1318- static void rx_slot_write_payload (const rx_session_t * const ses , rx_slot_t * const slot , const canard_bytes_t payload )
1365+ static void rx_session_record_admission (rx_session_t * const ses ,
1366+ const canard_prio_t priority ,
1367+ const uint64_t seqno ,
1368+ const canard_us_t ts ,
1369+ const byte_t iface_index )
13191370{
1320- if (slot -> total_size < ses -> owner -> extent ) {
1321- const size_t copy_size = smaller (payload .size , ses -> owner -> extent - slot -> total_size );
1322- (void )memcpy (& slot -> payload [slot -> total_size ], payload .data , copy_size );
1371+ ses -> seqno_frontier [priority ] = rx_seqno_pack (seqno ); // nothing older than this at this priority from now on
1372+ ses -> last_admitted_start_ts = ts ;
1373+ ses -> iface_index = iface_index ;
1374+ }
1375+
1376+ // Maximum seqno seen from the given highest priority level (numerically lowest, inclusive) and down.
1377+ static uint64_t rx_session_seqno_frontier (const rx_session_t * const ses , const canard_prio_t highest_priority )
1378+ {
1379+ uint64_t seqno = 0 ;
1380+ for (size_t i = (size_t )highest_priority ; i < CANARD_PRIO_COUNT ; i ++ ) {
1381+ seqno = max_u64 (seqno , rx_seqno_unpack (ses -> seqno_frontier [i ]));
1382+ }
1383+ return seqno ;
1384+ }
1385+
1386+ // Frame admittance state machine update. A complex piece, redesigned after v4 to support priority preemption.
1387+ // Key ideas: 1. Separate reassembly state per priority level. 2. TID is linearized into seqno.
1388+ // Once we admit a transfer at some priority with a certain seqno, we know that any older seqno at this or higher
1389+ // priority would be stale, since only higher priority transfers can preempt lower priority ones.
1390+ static bool rx_session_should_admit (const rx_session_t * const ses ,
1391+ const canard_us_t ts ,
1392+ const canard_prio_t priority ,
1393+ const bool start ,
1394+ const bool toggle ,
1395+ const uint64_t seqno ,
1396+ const byte_t iface_index )
1397+ {
1398+ if (!start ) {
1399+ // Continuation not resumed if seqno match is prevented by transfer-ID wraparound, even if the LSb would match.
1400+ // This is to mitigate the risk of frankentransfers, where distinct transfers are stitched together.
1401+ // Such old slots would eventually either time out or be replaced on next start frame.
1402+ const rx_slot_t * const slot = ses -> slots [priority ];
1403+ return (slot != NULL ) && (slot -> transfer_id == (seqno & CANARD_TRANSFER_ID_MAX )) &&
1404+ (slot -> iface_index == iface_index ) && (slot -> expected_toggle == toggle );
13231405 }
1324- slot -> total_size += payload .size ; // Before truncation.
1406+ // In the case of a single priority level, the seqno would be considered new if it is greater than last admitted.
1407+ // Priority preemption makes this simple condition insufficient, because a newer higher-priority transfer with
1408+ // a greater seqno may push older lower-priority transfers aside, causing the receiver to observe seqno going
1409+ // backward for new transfers. Our solution is to keep a dedicated seqno frontier per priority level.
1410+ //
1411+ // The lowest bound of a genuinely new seqno would then be located at the lowest priority level, since that level
1412+ // cannot preempt others. To decide if a given seqno is new, one needs to scan all priority levels from the
1413+ // current one (inclusive) down to the lowest level (numerically greater).
1414+ //
1415+ // Higher priority levels (numerically lesser) may have greater seqno frontiers which bear no relevance.
1416+ const bool seqno_new = seqno > rx_session_seqno_frontier (ses , priority );
1417+ const bool iface_match = ses -> iface_index == iface_index ;
1418+ const bool timed_out = ts > (ses -> last_admitted_start_ts + ses -> owner -> transfer_id_timeout );
1419+ return (seqno_new && iface_match ) || (iface_match && timed_out ) || (timed_out && seqno_new );
13251420}
13261421
13271422// Returns false on OOM, no other failure modes.
@@ -1333,6 +1428,7 @@ static bool rx_session_update(canard_subscription_t* const sub,
13331428 CANARD_ASSERT ((sub != NULL ) && (frame != NULL ) && (frame -> payload .data != NULL ) && (ts >= 0 ));
13341429 CANARD_ASSERT (frame -> end || (frame -> payload .size >= 7 ));
13351430
1431+ // Only start frames may create new states.
13361432 rx_session_factory_context_t factory_context = { .owner = sub , .node_id = frame -> src };
13371433 rx_session_t * const ses =
13381434 CAVL2_TO_OWNER (frame -> start ? cavl2_find_or_insert (& sub -> sessions , //
@@ -1348,20 +1444,11 @@ static bool rx_session_update(canard_subscription_t* const sub,
13481444 return !frame -> start ;
13491445 }
13501446
1351- // Frame admittance state machine. A highly complex piece, redesigned after v4 to support priority preemption.
1352- // TID forward difference illustration: f(2,3)==31, f(2,2)==0, f(2,1)==1
1353- const bool tid_new = rx_transfer_id_forward_difference (ses -> last_admitted_transfer_id , frame -> transfer_id ) > 1 ;
1354- const bool timed_out = ts > (ses -> last_admitted_start_ts + ses -> owner -> transfer_id_timeout );
1355- bool accept = false;
1356- const rx_slot_t * const slot = ses -> slots [frame -> priority ];
1357- if (!frame -> start ) {
1358- accept = (slot != NULL ) && (slot -> transfer_id == frame -> transfer_id ) && (slot -> iface_index == iface_index ) &&
1359- (slot -> expected_toggle == frame -> toggle );
1360- } else {
1361- accept = ((slot == NULL ) || (slot -> transfer_id != frame -> transfer_id )) && (tid_new || timed_out );
1362- }
1363- if (!accept ) {
1364- return true; // Frame not needed; not a failure to accept.
1447+ // Decide admit or drop.
1448+ const uint64_t seqno =
1449+ rx_seqno_linearize (rx_session_seqno_frontier (ses , canard_prio_exceptional ), frame -> transfer_id );
1450+ if (!rx_session_should_admit (ses , ts , frame -> priority , frame -> start , frame -> toggle , seqno , iface_index )) {
1451+ return true; // Rejection is not a failure.
13651452 }
13661453
13671454 // The frame must be accepted. If this is the start of a new transfer, we must update state.
@@ -1376,13 +1463,12 @@ static bool rx_session_update(canard_subscription_t* const sub,
13761463 return false;
13771464 }
13781465 }
1379- ses -> last_admitted_start_ts = ts ;
1380- ses -> last_admitted_transfer_id = frame -> transfer_id ;
1466+ rx_session_record_admission (ses , frame -> priority , seqno , ts , iface_index );
13811467 }
13821468
1383- // TODO acceptance
1469+ // TODO acceptance is not yet implemented.
13841470
1385- return false ;
1471+ return true ;
13861472}
13871473
13881474// --------------------------------------------- MISC ---------------------------------------------
@@ -1396,10 +1482,10 @@ bool canard_new(canard_t* const self,
13961482 const size_t filter_count ,
13971483 canard_filter_t * const filter_storage )
13981484{
1399- bool ok = (self != NULL ) && (vtable != NULL ) && (vtable -> now != NULL ) && (vtable -> tx != NULL ) &&
1400- (vtable -> filter != NULL ) && mem_valid (memory .tx_transfer ) && mem_valid (memory .tx_frame ) &&
1401- mem_valid (memory .rx_session ) && mem_valid (memory .rx_payload ) &&
1402- ((filter_count == 0U ) || (filter_storage != NULL )) && (node_id <= CANARD_NODE_ID_MAX );
1485+ const bool ok = (self != NULL ) && (vtable != NULL ) && (vtable -> now != NULL ) && (vtable -> tx != NULL ) &&
1486+ (vtable -> filter != NULL ) && mem_valid (memory .tx_transfer ) && mem_valid (memory .tx_frame ) &&
1487+ mem_valid (memory .rx_session ) && mem_valid (memory .rx_payload ) &&
1488+ ((filter_count == 0U ) || (filter_storage != NULL )) && (node_id <= CANARD_NODE_ID_MAX );
14031489 if (ok ) {
14041490 (void )memset (self , 0 , sizeof (* self ));
14051491 self -> node_id = (node_id <= CANARD_NODE_ID_MAX ) ? node_id : CANARD_NODE_ID_ANONYMOUS ;
0 commit comments