2323from scapy .supersocket import SuperSocket
2424from scapy .layers .can import CAN
2525from scapy .packet import Packet
26- from scapy .error import warning
26+ from scapy .error import warning , log_runtime
2727from typing import (
2828 List ,
2929 Type ,
4141
4242__all__ = ["CANSocket" , "PythonCANSocket" ]
4343
44+ # Interfaces with hardware or kernel-level CAN filtering.
45+ # These keep bus-level filters for efficiency since the device/driver
46+ # handles filtering before frames reach userspace.
47+ # All other interfaces (USB adapters like candle, gs_usb, cantact;
48+ # serial like slcan) only do software filtering in BusABC.recv() and
49+ # need filters cleared at the bus level to avoid starving matching
50+ # frames behind non-matching ones (echo frames, other bus traffic).
51+ _HW_FILTERED_INTERFACES = frozenset ([
52+ 'socketcan' , 'kvaser' , 'vector' , 'pcan' , 'ixxat' ,
53+ 'nican' , 'neovi' , 'etas' , 'systec' , 'nixnet' ,
54+ ])
55+
56+
57+ def _is_sw_filtered (interface_key ):
58+ # type: (str) -> bool
59+ """Return True if the bus identified by interface_key only does
60+ software filtering (inside BusABC.recv).
61+
62+ For such interfaces, bus-level filters must be cleared so that
63+ bus.recv(timeout=0) returns ALL frames. Per-socket filtering
64+ is then handled by distribute() via _matches_filters().
65+
66+ Without this, BusABC.recv(timeout=0) on software-filtered buses
67+ (candle, gs_usb, cantact, slcan, etc.) can silently consume
68+ one non-matching frame per call and return None, starving matching
69+ frames that sit behind it in the USB/serial buffer.
70+ """
71+ iface = interface_key .split ('_' )[0 ].lower ()
72+ return iface not in _HW_FILTERED_INTERFACES
73+
4474
4575class SocketMapper (object ):
4676 """Internal Helper class to map a python-can bus object to
@@ -57,6 +87,14 @@ def __init__(self, bus, sockets):
5787 self .bus = bus
5888 self .sockets = sockets
5989 self .closing = False
90+ # Per-bus lock serializing read_bus() and send_bus().
91+ # On serial interfaces (slcan) the python-can Bus uses a single
92+ # serial port for both recv() and send(). Without serialization,
93+ # concurrent calls from different threads (TimeoutScheduler for
94+ # recv, main thread for send) corrupt the serial byte stream,
95+ # causing slcan parsing errors. The lock is per-mapper (per-bus)
96+ # so different CAN buses are not blocked by each other.
97+ self .bus_lock = threading .Lock ()
6098
6199 # Maximum time (seconds) to spend reading frames in one read_bus()
62100 # call. On serial interfaces (slcan) the final bus.recv(timeout=0)
@@ -76,28 +114,43 @@ def read_bus(self):
76114 slcan serial timeout). This method limits total time spent
77115 reading so the TimeoutScheduler thread stays responsive.
78116
79- This method intentionally does NOT hold pool_mutex so that
80- concurrent send() calls are not blocked during the serial I/O.
117+ This method does NOT hold pool_mutex but DOES hold bus_lock to
118+ serialize with send_bus(). This prevents concurrent serial I/O
119+ on slcan interfaces while still allowing sends to other buses.
81120 """
82121 if self .closing :
83122 return []
84123 msgs = []
85124 deadline = time .monotonic () + self .READ_BUS_TIME_LIMIT
86- while True :
87- try :
88- msg = self .bus .recv (timeout = 0 )
89- if msg is None :
125+ with self .bus_lock :
126+ while True :
127+ try :
128+ msg = self .bus .recv (timeout = 0 )
129+ if msg is None :
130+ break
131+ else :
132+ msgs .append (msg )
133+ if time .monotonic () >= deadline :
134+ break
135+ except Exception as e :
136+ if not self .closing :
137+ warning ("[MUX] python-can exception caught: %s" % e )
90138 break
91- else :
92- msgs .append (msg )
93- if time .monotonic () >= deadline :
94- break
95- except Exception as e :
96- if not self .closing :
97- warning ("[MUX] python-can exception caught: %s" % e )
98- break
99139 return msgs
100140
141+ def send_bus (self , msg ):
142+ # type: (can_Message) -> None
143+ """Send a CAN message on the bus.
144+
145+ Serialized with read_bus() via bus_lock to prevent concurrent
146+ serial I/O on slcan interfaces.
147+
148+ :param msg: python-can Message to send.
149+ :raises can_CanError: If the underlying python-can Bus raises.
150+ """
151+ with self .bus_lock :
152+ self .bus .send (msg )
153+
101154 def distribute (self , msgs ):
102155 # type: (List[can_Message]) -> None
103156 """Distribute received messages to all subscribed sockets."""
@@ -122,10 +175,10 @@ def internal_send(self, sender, msg):
122175
123176 A given SocketWrapper wants to send a CAN message. The python-can
124177 Bus object is obtained from an internal pool of SocketMapper objects.
125- The given message is sent on the python-can Bus object and also
126- inserted into the message queues of all other SocketWrapper objects
127- which are connected to the same python-can bus object
128- by the SocketMapper.
178+ The message is sent on the python-can Bus object via send_bus()
179+ (serialized with read_bus() by bus_lock) and also inserted into
180+ the message queues of all other SocketWrapper objects connected to
181+ the same python-can bus object by the SocketMapper.
129182
130183 :param sender: SocketWrapper which initiated a send of a CAN message
131184 :param msg: CAN message to be sent
@@ -136,30 +189,43 @@ def internal_send(self, sender, msg):
136189 with self .pool_mutex :
137190 try :
138191 mapper = self .pool [sender .name ]
139- mapper .bus .send (msg )
140- for sock in mapper .sockets :
141- if sock == sender :
142- continue
143- if not sock ._matches_filters (msg ):
144- continue
145-
146- with sock .lock :
147- sock .rx_queue .append (msg )
148192 except KeyError :
149193 warning ("[SND] Socket %s not found in pool" % sender .name )
150- except can_CanError as e :
151- warning ("[SND] python-can exception caught: %s" % e )
194+ return
195+ # Snapshot the peer sockets while under pool_mutex
196+ peers = [s for s in mapper .sockets if s != sender ]
197+
198+ # Send on the bus outside pool_mutex but inside bus_lock.
199+ # This serializes with read_bus() to prevent concurrent serial
200+ # I/O on slcan interfaces, while still allowing multiplexing
201+ # and sends on other buses to proceed concurrently.
202+ try :
203+ mapper .send_bus (msg )
204+ except can_CanError as e :
205+ warning ("[SND] python-can exception caught: %s" % e )
206+ return
207+
208+ # Distribute to peer sockets (no need for pool_mutex here,
209+ # we already have a snapshot of the peers list).
210+ for sock in peers :
211+ if not sock ._matches_filters (msg ):
212+ continue
213+ with sock .lock :
214+ sock .rx_queue .append (msg )
152215
153216 def multiplex_rx_packets (self ):
154217 # type: () -> None
155218 """This calls the mux() function of all SocketMapper
156219 objects in this SocketPool
157220 """
158- if time .monotonic () - self .last_call < 0.001 :
221+ now = time .monotonic ()
222+ if now - self .last_call < 0.001 :
159223 # Avoid starvation if multiple threads are doing selects, since
160224 # this object is singleton and all python-CAN sockets are using
161225 # the same instance and locking the same locks.
162226 return
227+ self .last_call = now
228+
163229 # Snapshot pool entries under the lock, then read from each bus
164230 # WITHOUT holding pool_mutex. On slow serial interfaces (slcan)
165231 # bus.recv(timeout=0) can take ~2-3ms per frame; holding the
@@ -171,7 +237,6 @@ def multiplex_rx_packets(self):
171237 msgs = mapper .read_bus ()
172238 if msgs :
173239 mapper .distribute (msgs )
174- self .last_call = time .monotonic ()
175240
176241 def register (self , socket , * args , ** kwargs ):
177242 # type: (SocketWrapper, Tuple[Any, ...], Dict[str, Any]) -> None
@@ -198,34 +263,37 @@ def register(self, socket, *args, **kwargs):
198263 t = self .pool [k ]
199264 t .sockets .append (socket )
200265 # Update bus-level filters to the union of all sockets'
201- # filters. For non-slcan interfaces (socketcan, kvaser,
202- # vector), this enables efficient hardware/kernel
203- # filtering. For slcan, the bus filters were already
204- # cleared on creation, so this is a no-op (all sockets
205- # on slcan share the unfiltered bus).
206- if not k .lower ().startswith ('slcan' ):
266+ # filters. For hardware-filtered interfaces (socketcan,
267+ # kvaser, vector, pcan, ixxat), this enables efficient
268+ # kernel/device filtering. For software-filtered
269+ # interfaces (slcan, candle, gs_usb, cantact), the bus
270+ # filters were already cleared on creation, so this is
271+ # a no-op (all sockets share the unfiltered bus).
272+ if not _is_sw_filtered (k ):
207273 filters = [s .filters for s in t .sockets
208274 if s .filters is not None ]
209275 if filters :
210276 t .bus .set_filters (reduce (add , filters ))
211277 socket .name = k
212278 else :
213279 bus = can_Bus (* args , ** kwargs )
214- # Serial interfaces like slcan only do software
215- # filtering inside BusABC.recv(): the recv loop reads
216- # one frame, finds it doesn't match, and returns
217- # None -- silently consuming serial bandwidth without
218- # returning the frame to the mux. This starves the
219- # mux on busy buses.
280+ # Software-filtered interfaces (slcan, candle, gs_usb,
281+ # cantact, etc.) only filter inside BusABC.recv(): the
282+ # recv loop reads one frame, finds it doesn't match,
283+ # and returns None -- silently consuming serial/USB
284+ # bandwidth without returning the frame to the mux.
285+ # On USB adapters with timeout=0 mapped to ~1ms reads,
286+ # this means only one non-matching frame is consumed
287+ # per poll cycle, starving matching ECU responses that
288+ # sit behind echo frames in the hardware buffer.
220289 #
221- # For slcan, clear the filters from the bus so that
222- # bus.recv() returns ALL frames. Per-socket filtering
223- # in distribute() via _matches_filters() handles
224- # delivery. Other interfaces (socketcan, kvaser,
225- # vector, candle) perform efficient hardware/kernel
226- # filtering and should keep their bus-level filters.
227- if kwargs .get ('can_filters' ) and \
228- k .lower ().startswith ('slcan' ):
290+ # For all software-filtered interfaces, clear the bus
291+ # filters so bus.recv() returns ALL frames. Per-socket
292+ # filtering in distribute() via _matches_filters()
293+ # handles delivery. Hardware-filtered interfaces
294+ # (socketcan, kvaser, vector, pcan, ixxat) keep their
295+ # bus-level filters for efficiency.
296+ if kwargs .get ('can_filters' ) and _is_sw_filtered (k ):
229297 bus .set_filters (None )
230298 socket .name = k
231299 self .pool [k ] = SocketMapper (bus , [socket ])
@@ -242,17 +310,25 @@ def unregister(self, socket):
242310 if socket .name is None :
243311 raise TypeError ("SocketWrapper.name should never be None" )
244312
313+ mapper_to_shutdown = None
245314 with self .pool_mutex :
246315 try :
247316 t = self .pool [socket .name ]
248317 t .sockets .remove (socket )
249318 if not t .sockets :
250319 t .closing = True
251- t .bus .shutdown ()
252320 del self .pool [socket .name ]
321+ mapper_to_shutdown = t
253322 except KeyError :
254323 warning ("Socket %s already removed from pool" % socket .name )
255324
325+ # Shutdown the bus outside pool_mutex. Acquire bus_lock to
326+ # wait for any in-progress read_bus() or send_bus() to finish
327+ # before shutting down the underlying transport.
328+ if mapper_to_shutdown is not None :
329+ with mapper_to_shutdown .bus_lock :
330+ mapper_to_shutdown .bus .shutdown ()
331+
256332
257333SocketsPool = _SocketsPool ()
258334
@@ -341,6 +417,9 @@ def recv_raw(self, x=0xffff):
341417 """Returns a tuple containing (cls, pkt_data, time)"""
342418 msg = self .can_iface .recv ()
343419
420+ if msg is None :
421+ return self .basecls , None , None
422+
344423 hdr = msg .is_extended_id << 31 | msg .is_remote_frame << 30 | \
345424 msg .is_error_frame << 29 | msg .arbitration_id
346425
@@ -382,7 +461,13 @@ def select(sockets, remain=conf.recv_poll_rate):
382461 :returns: an array of sockets that were selected and
383462 the function to be called next to get the packets (i.g. recv)
384463 """
464+ # Move kernel-buffered CAN frames into the per-socket rx_queues
465+ # BEFORE checking which sockets are ready. The previous order
466+ # (check, then multiplex) returned a stale ready-list that did
467+ # not include sockets whose data had just been multiplexed,
468+ # causing a one-iteration delay.
385469 SocketsPool .multiplex_rx_packets ()
470+
386471 ready_sockets = \
387472 [s for s in sockets if isinstance (s , PythonCANSocket ) and
388473 len (s .can_iface .rx_queue )]
@@ -401,6 +486,13 @@ def close(self):
401486 """Closes this socket"""
402487 if self .closed :
403488 return
489+ # Final poll to ensure all kernel-buffered frames are distributed
490+ # to any shared socket instances before we unregister.
491+ try :
492+ SocketsPool .multiplex_rx_packets ()
493+ except Exception :
494+ log_runtime .debug ("Exception during SocketsPool multiplex in close" ,
495+ exc_info = True )
404496 super (PythonCANSocket , self ).close ()
405497 self .can_iface .shutdown ()
406498
0 commit comments