@@ -127,6 +127,29 @@ static byte_t popcount(const uint64_t x)
127127#endif
128128}
129129
130+ // The splitmix64 PRNG algorithm. Original work by Sebastiano Vigna, released under CC0-1.0 (public domain dedication).
131+ // Source http://xoshiro.di.unimi.it/splitmix64.c.
132+ static uint64_t splitmix64 (uint64_t * const state )
133+ {
134+ uint64_t z = (* state += 0x9E3779B97F4A7C15ULL );
135+ z = (z ^ (z >> 30U )) * 0xBF58476D1CE4E5B9ULL ;
136+ z = (z ^ (z >> 27U )) * 0x94D049BB133111EBULL ;
137+ return z ^ (z >> 31U );
138+ }
139+
140+ // Obtains a decent-quality pseudo-random number in [0, bound). Returns 0 if bound<=1
141+ static uint64_t random (canard_t * const self , const uint64_t bound )
142+ {
143+ return (bound > 0 ) ? (splitmix64 (& self -> prng_state ) % bound ) : 0 ;
144+ }
145+
146+ // Returns true with a probability of approximately 1/p_reciprocal. If p_reciprocal<=1, result is always true.
147+ static bool chance (canard_t * const self , const uint64_t p_reciprocal ) { return random (self , p_reciprocal ) == 0 ; }
148+
149+ // Least significant bit at limb #0, bit #0.
150+ static void bitmap_set (uint64_t * const b , const size_t i ) { b [i / 64U ] |= (UINT64_C (1 ) << (i % 64U )); }
151+ static bool bitmap_test (const uint64_t * const b , const size_t i ) { return (b [i / 64U ] & (1ULL << (i % 64U ))) != 0 ; }
152+
130153static void * mem_alloc (const canard_mem_t memory , const size_t size ) { return memory .vtable -> alloc (memory , size ); }
131154static void * mem_alloc_zero (const canard_mem_t memory , const size_t size )
132155{
@@ -842,6 +865,7 @@ static void tx_eject_pending(canard_t* const self, const byte_t iface_index)
842865// This is needed to allow safe node-ID change on collision detection, such that remotes don't reassemble multiframe
843866// frankentransfers from different nodes sharing the same ID.
844867// The complexity is linear in the number of enqueued transfers (not frames!).
868+ // This is safe to invoke from any context since it doesn't reach anywhere outside of the TX pipeline.
845869static void tx_purge_continuations (canard_t * const self )
846870{
847871 tx_transfer_t * tr = LIST_HEAD (self -> tx .agewise , tx_transfer_t , list_agewise );
@@ -1575,38 +1599,57 @@ static bool rx_filter_configure(const canard_t* const self)
15751599
15761600// --------------------------------------------- MISC ---------------------------------------------
15771601
1578- // The splitmix64 PRNG algorithm. Original work by Sebastiano Vigna, released under CC0-1.0 (public domain dedication).
1579- // Source http://xoshiro.di.unimi.it/splitmix64.c.
1580- static uint64_t splitmix64 (uint64_t * const state )
1581- {
1582- uint64_t z = (* state += 0x9E3779B97F4A7C15ULL );
1583- z = (z ^ (z >> 30U )) * 0xBF58476D1CE4E5B9ULL ;
1584- z = (z ^ (z >> 27U )) * 0x94D049BB133111EBULL ;
1585- return z ^ (z >> 31U );
1586- }
1587-
1588- // Obtains a decent-quality pseudo-random number in [0, cardinality).
1589- static uint64_t random (canard_t * const self , const uint64_t cardinality )
1590- {
1591- CANARD_ASSERT (cardinality > 0 );
1592- return splitmix64 (& self -> prng_state ) % cardinality ;
1593- }
1594-
15951602static void node_id_occupancy_reset (canard_t * const self )
15961603{
15971604 self -> node_id_occupancy_bitmap [0 ] = 1 ; // Reserve 0 for compatibility with v0
15981605 self -> node_id_occupancy_bitmap [1 ] = 0 ;
15991606}
16001607
16011608// Records the seen node-ID and reallocates the local node if a collision is found.
1602- static void node_id_occupancy_update (canard_t * const self , const byte_t src , const byte_t dst )
1603- {
1604- (void )self ;
1605- (void )src ;
1606- (void )dst ;
1607- // TODO not implemented.
1608- // TODO purge pending TX transfers that have already emitted the first frame.
1609- rx_filter_configure (self );
1609+ static void node_id_occupancy_update (canard_t * const self , const byte_t src )
1610+ {
1611+ // Update the node-ID occupancy bitmap. We cannot detect departures of an individual node, so in the presence of
1612+ // churn the slots will be eventually exhausted. We mitigate this by applying probabilistic purge once the
1613+ // population count exceeds some threshold, such that the purge probability becomes 1 when the bitmap is almost
1614+ // full. Such non-deterministic purge is preferable because it avoids spurious synchronization effects.
1615+ if (!bitmap_test (self -> node_id_occupancy_bitmap , src ) || (self -> node_id == src )) {
1616+ bitmap_set (self -> node_id_occupancy_bitmap , src );
1617+ const byte_t cap = CANARD_NODE_ID_CAPACITY ;
1618+ const byte_t pc = popcount (self -> node_id_occupancy_bitmap [0 ]) + popcount (self -> node_id_occupancy_bitmap [1 ]);
1619+ const byte_t zc = cap - pc ; // zero count, i.e., the number of free slots
1620+ const bool purge = (pc > (cap / 2 )) && chance (self , zc ); // deny purge unless getting dense
1621+ CANARD_ASSERT (zc > 0 ); // the algorithm guarantees some free slots always
1622+
1623+ // Check for collision, reroll if needed. This must be done before we purge the occupancy map.
1624+ if (self -> node_id == src ) {
1625+ // Uniformly pick a cleared bit from the bitmap. It will become our new node-ID.
1626+ size_t cleared_bit_index = (size_t )random (self , zc );
1627+ CANARD_ASSERT (cleared_bit_index < zc );
1628+ size_t bit_index = 0 ;
1629+ while (cleared_bit_index > 0 ) {
1630+ if (!bitmap_test (self -> node_id_occupancy_bitmap , bit_index ++ )) {
1631+ cleared_bit_index -- ;
1632+ }
1633+ CANARD_ASSERT (bit_index < CANARD_NODE_ID_CAPACITY );
1634+ }
1635+
1636+ // Assign the new ID.
1637+ CANARD_ASSERT ((0 < bit_index ) && (bit_index < CANARD_NODE_ID_CAPACITY ));
1638+ self -> node_id = (byte_t )bit_index ;
1639+ CANARD_ASSERT (!bitmap_test (self -> node_id_occupancy_bitmap , self -> node_id ));
1640+
1641+ // Update dependent states.
1642+ tx_purge_continuations (self );
1643+ self -> rx .filters_dirty = true;
1644+ self -> err .collision ++ ;
1645+ }
1646+
1647+ // The purge is delayed because we need the original occupancy map for reallocation.
1648+ if (purge ) {
1649+ node_id_occupancy_reset (self );
1650+ bitmap_set (self -> node_id_occupancy_bitmap , src );
1651+ }
1652+ }
16101653}
16111654
16121655bool canard_new (canard_t * const self ,
@@ -1701,11 +1744,9 @@ static void ingest_frame(canard_t* const self,
17011744 const frame_t frame )
17021745{
17031746 // Update the node-ID occupancy/collision monitoring states before routing the message.
1704- // We do this only on start frames because non-start frames have the version detection ambiguity,
1705- // which is not a problem for the routing logic (new states can only be created on start frames),
1706- // but it may cause phantom occupancy detection. Also, doing it only on start frames reduces the load.
1747+ // We do this only on start frames only mostly to manage load.
17071748 if (frame .start ) {
1708- node_id_occupancy_update (self , frame .src , frame . dst );
1749+ node_id_occupancy_update (self , frame .src );
17091750 }
17101751 // Route the frame to the appropriate destination internally.
17111752 canard_subscription_t * const sub = rx_route (self , & frame );
0 commit comments