@@ -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.
@@ -1193,19 +1184,92 @@ static byte_t rx_transfer_id_forward_difference(const byte_t a, const byte_t b)
11931184// by a transfer at the priority one higher, and so on up to the maximum.
11941185#define FOREACH_SLOT (i ) for (size_t i = 0; (i) < CANARD_PRIO_COUNT; (i)++)
11951186
1187+ // The seqno is the transfer-ID unrolled at the session layer. It is used to handle transfer-ID wraparound across
1188+ // priority levels. The minimum sufficient bit width is approx. 44, which given 10000 transfers per second will take
1189+ // approx. 55 years to overflow. We use bit fields for aggressive dynamic memory footprint optimization.
1190+ #define RX_SEQNO_BITS (48 - IFACE_INDEX_BIT_LENGTH - 1)
1191+ static_assert (RX_SEQNO_BITS >= 44 , "not enough bits for seqno" );
1192+ #define RX_SEQNO_MASK ((UINT64_C(1) << RX_SEQNO_BITS) - 1U)
1193+
11961194// Reassembly state at a specific priority level.
11971195// Maintaining separate state per priority level allows preemption of higher-priority transfers without loss.
11981196typedef struct
11991197{
12001198 canard_us_t start_ts ;
1201- size_t total_size ; // The raw payload size before the implicit truncation and CRC removal .
1202- uint16_t crc ;
1203- byte_t transfer_id : CANARD_TRANSFER_ID_BIT_LENGTH ;
1204- byte_t expected_toggle : 1 ;
1205- byte_t iface_index : IFACE_INDEX_BIT_LENGTH ;
1206- byte_t payload []; // Extent-sized.
1199+ uint64_t seqno : RX_SEQNO_BITS ; // Unrolled transfer-ID .
1200+ uint64_t iface_index : IFACE_INDEX_BIT_LENGTH ;
1201+ uint64_t expected_toggle : 1 ;
1202+ uint64_t crc : 16 ;
1203+ uint32_t total_size ; // The raw payload size seen before the implicit truncation and CRC removal.
1204+ byte_t payload []; // Extent-sized.
12071205} rx_slot_t ;
12081206#define RX_SLOT_OVERHEAD (offsetof(rx_slot_t, payload))
1207+ static_assert (RX_SLOT_OVERHEAD == 20 , "unexpected layout" );
1208+
1209+ static rx_slot_t * rx_slot_new (const canard_subscription_t * const sub ,
1210+ const canard_us_t start_ts ,
1211+ const uint64_t seqno ,
1212+ const byte_t iface_index )
1213+ {
1214+ rx_slot_t * const slot = mem_alloc (sub -> owner -> mem .rx_payload , RX_SLOT_OVERHEAD + sub -> extent );
1215+ if (slot != NULL ) {
1216+ memset (slot , 0 , RX_SLOT_OVERHEAD );
1217+ slot -> start_ts = start_ts ;
1218+ slot -> crc = sub -> crc_seed ;
1219+ slot -> seqno = seqno & RX_SEQNO_MASK ;
1220+ slot -> expected_toggle = kind_is_v1 (sub -> kind ) ? 1 : 0 ;
1221+ slot -> iface_index = iface_index & ((1U << IFACE_INDEX_BIT_LENGTH ) - 1U );
1222+ }
1223+ return slot ;
1224+ }
1225+
1226+ static void rx_slot_destroy (const canard_subscription_t * const sub , rx_slot_t * const slot )
1227+ {
1228+ mem_free (sub -> owner -> mem .rx_payload , RX_SLOT_OVERHEAD + sub -> extent , slot );
1229+ }
1230+
1231+ static void rx_slot_store (rx_slot_t * const slot , const size_t extent , const canard_bytes_t payload )
1232+ {
1233+ if (slot -> total_size < extent ) {
1234+ const size_t copy_size = smaller (payload .size , (size_t )(extent - slot -> total_size ));
1235+ (void )memcpy (& slot -> payload [slot -> total_size ], payload .data , copy_size );
1236+ }
1237+ slot -> total_size = (uint32_t )(slot -> total_size + payload .size ); // Before truncation.
1238+ }
1239+
1240+ // A compact representation is needed because we need to store an array of these in dynamic memory.
1241+ typedef struct
1242+ {
1243+ uint16_t limbs [3 ];
1244+ } rx_seqno_packed_t ;
1245+
1246+ static uint64_t rx_seqno_unpack (const rx_seqno_packed_t v )
1247+ {
1248+ return ((uint64_t )v .limbs [0 ]) | (((uint64_t )v .limbs [1 ]) << 16U ) | (((uint64_t )v .limbs [2 ]) << 32U );
1249+ }
1250+ static rx_seqno_packed_t rx_seqno_pack (const uint64_t v )
1251+ {
1252+ return (rx_seqno_packed_t ){
1253+ { (uint16_t )(v & 0xFFFFU ), (uint16_t )((v >> 16U ) & 0xFFFFU ), (uint16_t )((v >> 32U ) & 0xFFFFU ) }
1254+ };
1255+ }
1256+ static uint64_t rx_seqno_linearize (const uint64_t ref_seqno , const byte_t transfer_id )
1257+ {
1258+ CANARD_ASSERT (transfer_id <= CANARD_TRANSFER_ID_MAX );
1259+ const byte_t ref_tid = (byte_t )(ref_seqno & CANARD_TRANSFER_ID_MAX );
1260+ int16_t delta = ((int16_t )transfer_id ) - ((int16_t )ref_tid ); // NOLINT(*-narrowing-conversions)
1261+ // Select the nearest congruent seqno in the half-open interval [-16, +16) around the reference.
1262+ if (delta > (int16_t )(CANARD_TRANSFER_ID_MAX / 2U )) {
1263+ delta -= (int16_t )(CANARD_TRANSFER_ID_MAX + 1U );
1264+ } else if (delta < - ((int16_t )(CANARD_TRANSFER_ID_MAX / 2U ) + 1 )) {
1265+ delta += (int16_t )(CANARD_TRANSFER_ID_MAX + 1U );
1266+ }
1267+ // Near the origin the backward representative may underflow, so use the next forward one instead.
1268+ if ((delta < 0 ) && (ref_seqno < (uint64_t )(- delta ))) {
1269+ delta += (int16_t )(CANARD_TRANSFER_ID_MAX + 1U );
1270+ }
1271+ return (uint64_t )(((int64_t )ref_seqno ) + ((int64_t )delta ));
1272+ }
12091273
12101274// Up to libcanard v4 we used a fixed-capacity array of pointers for per-remote sessions for constant-time lookup,
12111275// but it was too costly on MCUs: with a 32-bit pointer it took 512 bytes for the array plus overheads,
@@ -1217,41 +1281,20 @@ typedef struct
12171281// - Non-start frames never create state.
12181282// - Session dedup state is updated on admitted start-of-transfer, not on transfer completion.
12191283// - Timeout is consulted only for start-of-transfer admission.
1220- // - Slot matching for continuation uses exact match: priority, transfer-ID, toggle, and iface.
1284+ // - Slot matching for continuation uses exact match: priority, transfer-ID/seqno , toggle, and iface.
12211285typedef struct
12221286{
12231287 canard_tree_t index ;
12241288 canard_listed_t list_animation ; // On update, session moved to the tail; oldest pushed to the head.
12251289 canard_us_t last_admitted_start_ts ;
12261290 rx_slot_t * slots [CANARD_PRIO_COUNT ]; // Indexed by priority level to allow preemption.
12271291 canard_subscription_t * owner ;
1228- byte_t last_admitted_transfer_id ;
1292+ rx_seqno_packed_t seqno_frontier [CANARD_PRIO_COUNT ];
1293+ byte_t iface_index ;
12291294 byte_t node_id ;
12301295} rx_session_t ;
12311296static_assert ((sizeof (void * ) > 4 ) || (sizeof (rx_session_t ) <= 120 ), "too large" );
12321297
1233- static rx_slot_t * rx_slot_new (const canard_subscription_t * const sub ,
1234- const canard_us_t start_ts ,
1235- const byte_t transfer_id ,
1236- const byte_t iface_index )
1237- {
1238- rx_slot_t * const slot = mem_alloc (sub -> owner -> mem .rx_payload , RX_SLOT_OVERHEAD + sub -> extent );
1239- if (slot != NULL ) {
1240- memset (slot , 0 , RX_SLOT_OVERHEAD );
1241- slot -> start_ts = start_ts ;
1242- slot -> crc = sub -> crc_seed ;
1243- slot -> transfer_id = transfer_id & CANARD_TRANSFER_ID_MAX ;
1244- slot -> expected_toggle = kind_is_v1 (sub -> kind ) ? 1 : 0 ;
1245- slot -> iface_index = iface_index & ((1U << IFACE_INDEX_BIT_LENGTH ) - 1U );
1246- }
1247- return slot ;
1248- }
1249-
1250- static void rx_slot_destroy (const canard_subscription_t * const sub , rx_slot_t * const slot )
1251- {
1252- mem_free (sub -> owner -> mem .rx_payload , RX_SLOT_OVERHEAD + sub -> extent , slot );
1253- }
1254-
12551298static int32_t rx_session_cavl_compare (const void * const user , const canard_tree_t * const node )
12561299{
12571300 return ((int32_t )(* (byte_t * )user )) - ((int32_t )CAVL2_TO_OWNER (node , rx_session_t , index )-> node_id );
@@ -1315,13 +1358,61 @@ static size_t rx_session_scan(rx_session_t* const ses, const canard_us_t now)
13151358 return n_slots ;
13161359}
13171360
1318- static void rx_slot_write_payload (const rx_session_t * const ses , rx_slot_t * const slot , const canard_bytes_t payload )
1361+ static void rx_session_record_admission (rx_session_t * const ses ,
1362+ const canard_prio_t priority ,
1363+ const uint64_t seqno ,
1364+ const canard_us_t ts ,
1365+ const byte_t iface_index )
13191366{
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 );
1367+ ses -> seqno_frontier [priority ] = rx_seqno_pack (seqno ); // nothing older than this at this priority from now on
1368+ ses -> last_admitted_start_ts = ts ;
1369+ ses -> iface_index = iface_index ;
1370+ }
1371+
1372+ // Maximum seqno seen from the given highest priority level (numerically lowest, inclusive) and down.
1373+ static uint64_t rx_session_seqno_frontier (const rx_session_t * const ses , const canard_prio_t highest_priority )
1374+ {
1375+ uint64_t seqno = 0 ;
1376+ for (size_t i = (size_t )highest_priority ; i < CANARD_PRIO_COUNT ; i ++ ) {
1377+ seqno = max_u64 (seqno , rx_seqno_unpack (ses -> seqno_frontier [i ]));
1378+ }
1379+ return seqno ;
1380+ }
1381+
1382+ // Frame admittance state machine update. A complex piece, redesigned after v4 to support priority preemption.
1383+ // Key ideas: 1. Separate reassembly state per priority level. 2. TID is linearized into seqno.
1384+ // Once we admit a transfer at some priority with a certain seqno, we know that any older seqno at this or higher
1385+ // priority would be stale, since only higher priority transfers can preempt lower priority ones.
1386+ static bool rx_session_should_admit (const rx_session_t * const ses ,
1387+ const canard_us_t ts ,
1388+ const canard_prio_t priority ,
1389+ const bool start ,
1390+ const bool toggle ,
1391+ const uint64_t seqno ,
1392+ const byte_t iface_index )
1393+ {
1394+ const rx_slot_t * const slot = ses -> slots [priority ];
1395+ if (!start ) {
1396+ // Continuation not resumed if seqno match is prevented by transfer-ID wraparound, even if the LSb would match.
1397+ // This is to mitigate the risk of frankentransfers, where distinct transfers are stitched together.
1398+ // Such old slots would eventually either time out or be replaced on next start frame.
1399+ return (slot != NULL ) && (slot -> seqno == seqno ) && (slot -> iface_index == iface_index ) &&
1400+ (slot -> expected_toggle == toggle );
13231401 }
1324- slot -> total_size += payload .size ; // Before truncation.
1402+ // In the case of a single priority level, the seqno would be considered new if it is greater than last admitted.
1403+ // Priority preemption makes this simple condition insufficient, because a newer higher-priority transfer with
1404+ // a greater seqno may push older lower-priority transfers aside, causing the receiver to observe seqno going
1405+ // backward for new transfers. Our solution is to keep a dedicated seqno frontier per priority level.
1406+ //
1407+ // The lowest bound of a genuinely new seqno would then be located at the lowest priority level, since that level
1408+ // cannot preempt others. To decide if a given seqno is new, one needs to scan all priority levels from the
1409+ // current one (inclusive) down to the lowest level (numerically greater).
1410+ //
1411+ // Higher priority levels (numerically lesser) may have greater seqno frontiers which bear no relevance.
1412+ const bool seqno_new = rx_session_seqno_frontier (ses , priority ) >= seqno ;
1413+ const bool iface_match = ses -> iface_index == iface_index ;
1414+ const bool timed_out = ts > (ses -> last_admitted_start_ts + ses -> owner -> transfer_id_timeout );
1415+ return (seqno_new && iface_match ) || (iface_match && timed_out ) || (timed_out && seqno_new );
13251416}
13261417
13271418// Returns false on OOM, no other failure modes.
@@ -1333,6 +1424,7 @@ static bool rx_session_update(canard_subscription_t* const sub,
13331424 CANARD_ASSERT ((sub != NULL ) && (frame != NULL ) && (frame -> payload .data != NULL ) && (ts >= 0 ));
13341425 CANARD_ASSERT (frame -> end || (frame -> payload .size >= 7 ));
13351426
1427+ // Only start frames may create new states.
13361428 rx_session_factory_context_t factory_context = { .owner = sub , .node_id = frame -> src };
13371429 rx_session_t * const ses =
13381430 CAVL2_TO_OWNER (frame -> start ? cavl2_find_or_insert (& sub -> sessions , //
@@ -1348,20 +1440,11 @@ static bool rx_session_update(canard_subscription_t* const sub,
13481440 return !frame -> start ;
13491441 }
13501442
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.
1443+ // Decide admit or drop.
1444+ const uint64_t seqno =
1445+ rx_seqno_linearize (rx_session_seqno_frontier (ses , canard_prio_exceptional ), frame -> transfer_id );
1446+ if (!rx_session_should_admit (ses , ts , frame -> priority , frame -> start , frame -> toggle , seqno , iface_index )) {
1447+ return true; // Rejection is not a failure.
13651448 }
13661449
13671450 // The frame must be accepted. If this is the start of a new transfer, we must update state.
@@ -1370,17 +1453,16 @@ static bool rx_session_update(canard_subscription_t* const sub,
13701453 rx_slot_destroy (sub , ses -> slots [frame -> priority ]);
13711454 ses -> slots [frame -> priority ] = NULL ;
13721455 if (!frame -> end ) { // more frames to follow, must store in-progress state
1373- ses -> slots [frame -> priority ] = rx_slot_new (sub , ts , frame -> transfer_id , iface_index );
1456+ ses -> slots [frame -> priority ] = rx_slot_new (sub , ts , seqno , iface_index );
13741457 if (ses -> slots [frame -> priority ] == NULL ) {
13751458 sub -> owner -> err .oom ++ ;
13761459 return false;
13771460 }
13781461 }
1379- ses -> last_admitted_start_ts = ts ;
1380- ses -> last_admitted_transfer_id = frame -> transfer_id ;
1462+ rx_session_record_admission (ses , frame -> priority , seqno , ts , iface_index );
13811463 }
13821464
1383- // TODO acceptance
1465+ // TODO acceptance is not yet implemented.
13841466
13851467 return false;
13861468}
@@ -1396,10 +1478,10 @@ bool canard_new(canard_t* const self,
13961478 const size_t filter_count ,
13971479 canard_filter_t * const filter_storage )
13981480{
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 );
1481+ const bool ok = (self != NULL ) && (vtable != NULL ) && (vtable -> now != NULL ) && (vtable -> tx != NULL ) &&
1482+ (vtable -> filter != NULL ) && mem_valid (memory .tx_transfer ) && mem_valid (memory .tx_frame ) &&
1483+ mem_valid (memory .rx_session ) && mem_valid (memory .rx_payload ) &&
1484+ ((filter_count == 0U ) || (filter_storage != NULL )) && (node_id <= CANARD_NODE_ID_MAX );
14031485 if (ok ) {
14041486 (void )memset (self , 0 , sizeof (* self ));
14051487 self -> node_id = (node_id <= CANARD_NODE_ID_MAX ) ? node_id : CANARD_NODE_ID_ANONYMOUS ;
0 commit comments