@@ -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,93 @@ 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_write_payload (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.
1241+ // Using 64-bit values is wasteful because it leaves 2*history_size wasted bytes, which is a lot for dynamic memory.
1242+ typedef struct
1243+ {
1244+ uint16_t limbs [3 ];
1245+ } rx_seqno_packed_t ;
1246+
1247+ static uint64_t rx_seqno_unpack (const rx_seqno_packed_t v )
1248+ {
1249+ return ((uint64_t )v .limbs [0 ]) | (((uint64_t )v .limbs [1 ]) << 16U ) | (((uint64_t )v .limbs [2 ]) << 32U );
1250+ }
1251+ static rx_seqno_packed_t rx_seqno_pack (const uint64_t v )
1252+ {
1253+ return (rx_seqno_packed_t ){
1254+ { (uint16_t )(v & 0xFFFFU ), (uint16_t )((v >> 16U ) & 0xFFFFU ), (uint16_t )((v >> 32U ) & 0xFFFFU ) }
1255+ };
1256+ }
1257+ static uint64_t rx_seqno_linearize (const uint64_t ref_seqno , const byte_t transfer_id )
1258+ {
1259+ CANARD_ASSERT (transfer_id <= CANARD_TRANSFER_ID_MAX );
1260+ const byte_t ref_tid = (byte_t )(ref_seqno & CANARD_TRANSFER_ID_MAX );
1261+ int16_t delta = ((int16_t )transfer_id ) - ((int16_t )ref_tid ); // NOLINT(*-narrowing-conversions)
1262+ // Select the nearest congruent seqno in the half-open interval [-16, +16) around the reference.
1263+ if (delta > (int16_t )(CANARD_TRANSFER_ID_MAX / 2U )) {
1264+ delta -= (int16_t )(CANARD_TRANSFER_ID_MAX + 1U );
1265+ } else if (delta < - ((int16_t )(CANARD_TRANSFER_ID_MAX / 2U ) + 1 )) {
1266+ delta += (int16_t )(CANARD_TRANSFER_ID_MAX + 1U );
1267+ }
1268+ // Near the origin the backward representative may underflow, so use the next forward one instead.
1269+ if ((delta < 0 ) && (ref_seqno < (uint64_t )(- delta ))) {
1270+ delta += (int16_t )(CANARD_TRANSFER_ID_MAX + 1U );
1271+ }
1272+ return (uint64_t )(((int64_t )ref_seqno ) + ((int64_t )delta ));
1273+ }
12091274
12101275// Up to libcanard v4 we used a fixed-capacity array of pointers for per-remote sessions for constant-time lookup,
12111276// but it was too costly on MCUs: with a 32-bit pointer it took 512 bytes for the array plus overheads,
@@ -1217,41 +1282,20 @@ typedef struct
12171282// - Non-start frames never create state.
12181283// - Session dedup state is updated on admitted start-of-transfer, not on transfer completion.
12191284// - Timeout is consulted only for start-of-transfer admission.
1220- // - Slot matching for continuation uses exact match: priority, transfer-ID, toggle, and iface.
1285+ // - Slot matching for continuation uses exact match: priority, transfer-ID/seqno , toggle, and iface.
12211286typedef struct
12221287{
12231288 canard_tree_t index ;
12241289 canard_listed_t list_animation ; // On update, session moved to the tail; oldest pushed to the head.
12251290 canard_us_t last_admitted_start_ts ;
12261291 rx_slot_t * slots [CANARD_PRIO_COUNT ]; // Indexed by priority level to allow preemption.
12271292 canard_subscription_t * owner ;
1228- byte_t last_admitted_transfer_id ;
1293+ rx_seqno_packed_t seqno_frontier [CANARD_PRIO_COUNT ];
1294+ byte_t iface_index ;
12291295 byte_t node_id ;
12301296} rx_session_t ;
12311297static_assert ((sizeof (void * ) > 4 ) || (sizeof (rx_session_t ) <= 120 ), "too large" );
12321298
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-
12551299static int32_t rx_session_cavl_compare (const void * const user , const canard_tree_t * const node )
12561300{
12571301 return ((int32_t )(* (byte_t * )user )) - ((int32_t )CAVL2_TO_OWNER (node , rx_session_t , index )-> node_id );
@@ -1315,13 +1359,61 @@ static size_t rx_session_scan(rx_session_t* const ses, const canard_us_t now)
13151359 return n_slots ;
13161360}
13171361
1318- static void rx_slot_write_payload (const rx_session_t * const ses , rx_slot_t * const slot , const canard_bytes_t payload )
1362+ static void rx_session_record_admission (rx_session_t * const ses ,
1363+ const canard_prio_t priority ,
1364+ const uint64_t seqno ,
1365+ const canard_us_t ts ,
1366+ const byte_t iface_index )
13191367{
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 );
1368+ ses -> seqno_frontier [priority ] = rx_seqno_pack (seqno ); // nothing older than this at this priority from now on
1369+ ses -> last_admitted_start_ts = ts ;
1370+ ses -> iface_index = iface_index ;
1371+ }
1372+
1373+ // Maximum seqno seen from the given highest priority level (numerically lowest, inclusive) and down.
1374+ static uint64_t rx_session_seqno_frontier (const rx_session_t * const ses , const canard_prio_t highest_priority )
1375+ {
1376+ uint64_t seqno = 0 ;
1377+ for (size_t i = (size_t )highest_priority ; i < CANARD_PRIO_COUNT ; i ++ ) {
1378+ seqno = max_u64 (seqno , rx_seqno_unpack (ses -> seqno_frontier [i ]));
1379+ }
1380+ return seqno ;
1381+ }
1382+
1383+ // Frame admittance state machine update. A complex piece, redesigned after v4 to support priority preemption.
1384+ // Key ideas: 1. Separate reassembly state per priority level. 2. TID is linearized into seqno.
1385+ // Once we admit a transfer at some priority with a certain seqno, we know that any older seqno at this or higher
1386+ // priority would be stale, since only higher priority transfers can preempt lower priority ones.
1387+ static bool rx_session_should_admit (const rx_session_t * const ses ,
1388+ const canard_us_t ts ,
1389+ const canard_prio_t priority ,
1390+ const bool start ,
1391+ const bool toggle ,
1392+ const uint64_t seqno ,
1393+ const byte_t iface_index )
1394+ {
1395+ const rx_slot_t * const slot = ses -> slots [priority ];
1396+ if (!start ) {
1397+ // Continuation not resumed if seqno match is prevented by transfer-ID wraparound, even if the LSb would match.
1398+ // This is to mitigate the risk of frankentransfers, where distinct transfers are stitched together.
1399+ // Such old slots would eventually either time out or be replaced on next start frame.
1400+ return (slot != NULL ) && (slot -> seqno == seqno ) && (slot -> iface_index == iface_index ) &&
1401+ (slot -> expected_toggle == toggle );
13231402 }
1324- slot -> total_size += payload .size ; // Before truncation.
1403+ // In the case of a single priority level, the seqno would be considered new if it is greater than last admitted.
1404+ // Priority preemption makes this simple condition insufficient, because a newer higher-priority transfer with
1405+ // a greater seqno may push older lower-priority transfers aside, causing the receiver to observe seqno going
1406+ // backward for new transfers. Our solution is to keep a dedicated seqno frontier per priority level.
1407+ //
1408+ // The lowest bound of a genuinely new seqno would then be located at the lowest priority level, since that level
1409+ // cannot preempt others. To decide if a given seqno is new, one needs to scan all priority levels from the
1410+ // current one (inclusive) down to the lowest level (numerically greater).
1411+ //
1412+ // Higher priority levels (numerically lesser) may have greater seqno frontiers which bear no relevance.
1413+ const bool seqno_new = rx_session_seqno_frontier (ses , priority ) >= seqno ;
1414+ const bool iface_match = ses -> iface_index == iface_index ;
1415+ const bool timed_out = ts > (ses -> last_admitted_start_ts + ses -> owner -> transfer_id_timeout );
1416+ return ((slot == NULL ) || (slot -> seqno < seqno )) && (seqno_new || timed_out ) && (iface_match || timed_out );
13251417}
13261418
13271419// Returns false on OOM, no other failure modes.
@@ -1333,6 +1425,7 @@ static bool rx_session_update(canard_subscription_t* const sub,
13331425 CANARD_ASSERT ((sub != NULL ) && (frame != NULL ) && (frame -> payload .data != NULL ) && (ts >= 0 ));
13341426 CANARD_ASSERT (frame -> end || (frame -> payload .size >= 7 ));
13351427
1428+ // Only start frames may create new states.
13361429 rx_session_factory_context_t factory_context = { .owner = sub , .node_id = frame -> src };
13371430 rx_session_t * const ses =
13381431 CAVL2_TO_OWNER (frame -> start ? cavl2_find_or_insert (& sub -> sessions , //
@@ -1348,20 +1441,11 @@ static bool rx_session_update(canard_subscription_t* const sub,
13481441 return !frame -> start ;
13491442 }
13501443
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.
1444+ // Decide admit or drop.
1445+ const uint64_t seqno =
1446+ rx_seqno_linearize (rx_session_seqno_frontier (ses , canard_prio_exceptional ), frame -> transfer_id );
1447+ if (!rx_session_should_admit (ses , ts , frame -> priority , frame -> start , frame -> toggle , seqno , iface_index )) {
1448+ return true; // Rejection is not a failure.
13651449 }
13661450
13671451 // The frame must be accepted. If this is the start of a new transfer, we must update state.
@@ -1370,17 +1454,16 @@ static bool rx_session_update(canard_subscription_t* const sub,
13701454 rx_slot_destroy (sub , ses -> slots [frame -> priority ]);
13711455 ses -> slots [frame -> priority ] = NULL ;
13721456 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 );
1457+ ses -> slots [frame -> priority ] = rx_slot_new (sub , ts , seqno , iface_index );
13741458 if (ses -> slots [frame -> priority ] == NULL ) {
13751459 sub -> owner -> err .oom ++ ;
13761460 return false;
13771461 }
13781462 }
1379- ses -> last_admitted_start_ts = ts ;
1380- ses -> last_admitted_transfer_id = frame -> transfer_id ;
1463+ rx_session_record_admission (ses , frame -> priority , seqno , ts , iface_index );
13811464 }
13821465
1383- // TODO acceptance
1466+ // TODO acceptance is not yet implemented.
13841467
13851468 return false;
13861469}
@@ -1396,10 +1479,10 @@ bool canard_new(canard_t* const self,
13961479 const size_t filter_count ,
13971480 canard_filter_t * const filter_storage )
13981481{
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 );
1482+ const bool ok = (self != NULL ) && (vtable != NULL ) && (vtable -> now != NULL ) && (vtable -> tx != NULL ) &&
1483+ (vtable -> filter != NULL ) && mem_valid (memory .tx_transfer ) && mem_valid (memory .tx_frame ) &&
1484+ mem_valid (memory .rx_session ) && mem_valid (memory .rx_payload ) &&
1485+ ((filter_count == 0U ) || (filter_storage != NULL )) && (node_id <= CANARD_NODE_ID_MAX );
14031486 if (ok ) {
14041487 (void )memset (self , 0 , sizeof (* self ));
14051488 self -> node_id = (node_id <= CANARD_NODE_ID_MAX ) ? node_id : CANARD_NODE_ID_ANONYMOUS ;
0 commit comments