33# Author: Pavel Kirienko <pavel@opencyphal.org>
44# pylint: disable=duplicate-code
55
6+ from dataclasses import dataclass
67import enum
78import time
89import errno
2930_logger = logging .getLogger (__name__ )
3031
3132
33+ @dataclass
34+ class _TimestampedErrorList :
35+ """Collection of media errors with a single timestamp. Used as a helper for typing."""
36+
37+ timestamp : Timestamp
38+ errors : typing .List [Media .Error ]
39+
40+
3241class SocketCANMedia (Media ):
3342 """
3443 This media implementation provides a simple interface for the standard Linux SocketCAN media layer.
@@ -125,17 +134,29 @@ def number_of_acceptance_filters(self) -> int:
125134 """
126135 return 512
127136
128- def start (self , handler : Media .ReceivedFramesHandler , no_automatic_retransmission : bool ) -> None :
137+ def start (
138+ self ,
139+ handler : Media .ReceivedFramesHandler ,
140+ error_handler : typing .Optional [Media .ErrorHandler ],
141+ no_automatic_retransmission : bool ,
142+ ) -> None :
129143 if self ._maybe_thread is None :
130144 self ._maybe_thread = threading .Thread (
131- target = self ._thread_function , name = str (self ), args = (handler , asyncio .get_event_loop ()), daemon = True
145+ target = self ._thread_function ,
146+ name = str (self ),
147+ args = (handler , error_handler , asyncio .get_event_loop ()),
148+ daemon = True ,
132149 )
133150 self ._maybe_thread .start ()
134151 if no_automatic_retransmission :
135152 _logger .info ("%s non-automatic retransmission is not supported" , self )
136153 else :
137154 raise RuntimeError ("The RX frame handler is already set up" )
138155
156+ if error_handler is not None :
157+ err_mask = _CAN_ERR_TX_TIMEOUT | _CAN_ERR_CRTL | _CAN_ERR_BUSOFF
158+ self ._sock .setsockopt (socket .SOL_CAN_RAW , _CAN_RAW_ERR_FILTER , err_mask )
159+
139160 def configure_acceptance_filters (self , configuration : typing .Sequence [FilterConfiguration ]) -> None :
140161 if self ._closed :
141162 raise pycyphal .transport .ResourceClosedError (repr (self ))
@@ -194,14 +215,30 @@ def close(self) -> None:
194215 self ._ctl_worker .close ()
195216 self ._ctl_main .close ()
196217
197- def _thread_function (self , handler : Media .ReceivedFramesHandler , loop : asyncio .AbstractEventLoop ) -> None :
218+ def _thread_function (
219+ self ,
220+ handler : Media .ReceivedFramesHandler ,
221+ error_handler : typing .Optional [Media .ErrorHandler ],
222+ loop : asyncio .AbstractEventLoop ,
223+ ) -> None :
198224 def handler_wrapper (frs : typing .Sequence [typing .Tuple [Timestamp , Envelope ]]) -> None :
199225 try :
200226 if not self ._closed : # Don't call after closure to prevent race conditions and use-after-close.
201227 handler (frs )
202228 except Exception as exc :
203229 _logger .exception ("%s: Unhandled exception in the receive handler: %s; lost frames: %s" , self , exc , frs )
204230
231+ def error_handler_wrapper (errors : _TimestampedErrorList ) -> None :
232+ try :
233+ # Check if we are not closed and the handler exists
234+ if not self ._closed and error_handler is not None :
235+ for error in errors .errors :
236+ error_handler (errors .timestamp , error )
237+ except Exception as exc :
238+ _logger .exception (
239+ "%s: Unhandled exception in the receive error handler: %s; lost error: %s" , self , exc , errors
240+ )
241+
205242 while not self ._closed and not loop .is_closed ():
206243 try :
207244 (
@@ -213,14 +250,22 @@ def handler_wrapper(frs: typing.Sequence[typing.Tuple[Timestamp, Envelope]]) ->
213250
214251 if self ._sock in read_ready :
215252 frames : typing .List [typing .Tuple [Timestamp , Envelope ]] = []
253+ errors : typing .Optional [_TimestampedErrorList ] = None
216254 try :
217255 while True :
218- frames .append (self ._read_frame (ts_mono_ns ))
256+ out = self ._read_frame (ts_mono_ns )
257+ if isinstance (out , _TimestampedErrorList ):
258+ errors = out
259+ break # Report previously received frames first
260+ else :
261+ frames .append (out )
219262 except OSError as ex :
220263 if ex .errno != errno .EAGAIN :
221264 raise
222265 try :
223266 loop .call_soon_threadsafe (handler_wrapper , frames )
267+ if errors :
268+ loop .call_soon_threadsafe (error_handler_wrapper , errors )
224269 except RuntimeError as ex :
225270 _logger .debug ("%s: Event loop is closed, exiting: %r" , self , ex )
226271 break
@@ -241,7 +286,7 @@ def handler_wrapper(frs: typing.Sequence[typing.Tuple[Timestamp, Envelope]]) ->
241286 self ._closed = True
242287 _logger .debug ("%s thread is about to exit" , self )
243288
244- def _read_frame (self , ts_mono_ns : int ) -> typing .Tuple [Timestamp , Envelope ]:
289+ def _read_frame (self , ts_mono_ns : int ) -> typing .Tuple [Timestamp , Envelope ] | _TimestampedErrorList :
245290 while True :
246291 data , ancdata , msg_flags , _addr = self ._sock .recvmsg ( # type: ignore
247292 self ._native_frame_size , self ._ancillary_data_buffer_size
@@ -266,9 +311,13 @@ def _read_frame(self, ts_mono_ns: int) -> typing.Tuple[Timestamp, Envelope]:
266311
267312 assert ts_system_ns > 0 , "Missing the timestamp; does the driver support timestamping?"
268313 timestamp = Timestamp (system_ns = ts_system_ns , monotonic_ns = ts_mono_ns )
269- out = SocketCANMedia ._parse_native_frame (data )
270- if out is not None :
314+ out = self ._parse_native_frame (data )
315+ if isinstance ( out , DataFrame ) :
271316 return timestamp , Envelope (out , loopback = loopback )
317+ elif isinstance (out , list ):
318+ return _TimestampedErrorList (timestamp , out )
319+ else :
320+ assert False , "Unreachable"
272321
273322 def _compile_native_frame (self , source : DataFrame ) -> bytes :
274323 flags = _CANFD_BRS if (self ._is_fd and not self ._disable_brs ) else 0
@@ -278,13 +327,51 @@ def _compile_native_frame(self, source: DataFrame) -> bytes:
278327 assert len (out ) == self ._native_frame_size
279328 return out
280329
281- @staticmethod
282- def _parse_native_frame (source : bytes ) -> typing .Optional [DataFrame ]:
330+ def _parse_native_frame (self , source : bytes ) -> None | DataFrame | typing .List [Media .Error ]:
283331 header_size = _FRAME_HEADER_STRUCT .size
284332 ident_raw , data_length , _flags = _FRAME_HEADER_STRUCT .unpack (source [:header_size ])
285- if ( ident_raw & _CAN_RTR_FLAG ) or ( ident_raw & _CAN_ERR_FLAG ) : # Unsupported format, ignore silently
333+ if ident_raw & _CAN_RTR_FLAG : # Unsupported format, ignore silently
286334 _logger .debug ("Unsupported CAN frame dropped; raw SocketCAN ID is %08x" , ident_raw )
287335 return None
336+
337+ if ident_raw & _CAN_ERR_FLAG :
338+ out_error = []
339+ if ident_raw & _CAN_ERR_TX_TIMEOUT :
340+ _logger .error ("Error Tx Timeout on %s" , self ._iface_name )
341+ out_error .append (Media .Error .CAN_TX_TIMEOUT )
342+ if ident_raw & _CAN_ERR_CRTL : # Controller problem, details are in data[1]
343+ error_byte = source [header_size + 1 ]
344+ if error_byte & _CAN_ERR_CRTL_RX_OVERFLOW :
345+ _logger .error ("Error Rx Overflow State on %s" , self ._iface_name )
346+ out_error .append (Media .Error .CAN_RX_OVERFLOW )
347+ if error_byte & _CAN_ERR_CRTL_TX_OVERFLOW :
348+ _logger .error ("Error Tx Overflow State on %s" , self ._iface_name )
349+ out_error .append (Media .Error .CAN_TX_OVERFLOW )
350+ if error_byte & _CAN_ERR_CRTL_RX_WARNING :
351+ _logger .warning ("Error Rx Warning State on %s" , self ._iface_name )
352+ out_error .append (Media .Error .CAN_RX_WARNING )
353+ if error_byte & _CAN_ERR_CRTL_TX_WARNING :
354+ _logger .warning ("Error Tx Warning State on %s" , self ._iface_name )
355+ out_error .append (Media .Error .CAN_TX_WARNING )
356+ if error_byte & _CAN_ERR_CRTL_TX_PASSIVE :
357+ _logger .error ("Error Tx Passive State on %s" , self ._iface_name )
358+ out_error .append (Media .Error .CAN_TX_PASSIVE )
359+ if error_byte & _CAN_ERR_CRTL_RX_PASSIVE :
360+ _logger .error ("Error Rx Passive State on %s" , self ._iface_name )
361+ out_error .append (Media .Error .CAN_RX_PASSIVE )
362+ if ident_raw & _CAN_ERR_BUSOFF :
363+ _logger .error ("CAN Bus Off on %s" , self ._iface_name )
364+ out_error .append (Media .Error .CAN_BUS_OFF )
365+
366+ if len (out_error ) > 0 :
367+ return out_error
368+ else :
369+ _logger .debug (
370+ "Unsupported CAN error frame dropped; raw SocketCAN ID is %08x" ,
371+ ident_raw ,
372+ )
373+ return None
374+
288375 frame_format = FrameFormat .EXTENDED if ident_raw & _CAN_EFF_FLAG else FrameFormat .BASE
289376 data = source [header_size : header_size + data_length ]
290377 assert len (data ) == data_length
@@ -351,6 +438,48 @@ class _NativeFrameDataCapacity(enum.IntEnum):
351438_CAN_RTR_FLAG = 0x40000000
352439_CAN_ERR_FLAG = 0x20000000
353440
441+ # From the Linux kernel (linux/include/uapi/linux/can/error.h); not exposed via the Python's socket module
442+ _CAN_ERR_TX_TIMEOUT = 0x00000001
443+ """TX timeout (by netdevice driver)"""
444+ _CAN_ERR_LOSTARB = 0x00000002
445+ """lost arbitration / data[0]"""
446+ _CAN_ERR_CRTL = 0x00000004
447+ """controller problems / data[1]"""
448+ _CAN_ERR_PROT = 0x00000008
449+ """protocol violations / data[2..3]"""
450+ _CAN_ERR_TRX = 0x00000010
451+ """transceiver status / data[4]"""
452+ _CAN_ERR_ACK = 0x00000020
453+ """received no ACK on transmission"""
454+ _CAN_ERR_BUSOFF = 0x00000040
455+ """bus off"""
456+ _CAN_ERR_BUSERROR = 0x00000080
457+ """bus error (may flood!)"""
458+ _CAN_ERR_RESTARTED = 0x00000100
459+ """controller restarted"""
460+ _CAN_ERR_CNT = 0x00000200
461+ """TX error counter / data[6], RX error counter / data[7]"""
462+
463+ _CAN_ERR_CRTL_UNSPEC = 0x00
464+ """ unspecified"""
465+ _CAN_ERR_CRTL_RX_OVERFLOW = 0x01
466+ """ RX buffer overflow"""
467+ _CAN_ERR_CRTL_TX_OVERFLOW = 0x02
468+ """ TX buffer overflow"""
469+ _CAN_ERR_CRTL_RX_WARNING = 0x04
470+ """ reached warning level for RX errors"""
471+ _CAN_ERR_CRTL_TX_WARNING = 0x08
472+ """ reached warning level for TX errors"""
473+ _CAN_ERR_CRTL_RX_PASSIVE = 0x10
474+ """ reached error passive status RX"""
475+ _CAN_ERR_CRTL_TX_PASSIVE = 0x20
476+ """ reached error passive status TX (at least one error counter exceeds the protocol-defined level of 127)"""
477+ _CAN_ERR_CRTL_ACTIVE = 0x40
478+ """ recovered to error active state"""
479+
480+ # From the Linux kernel (linux/include/uapi/linux/can/raw.h); not exposed via the Python's socket module
481+ _CAN_RAW_ERR_FILTER = 2
482+
354483_CAN_EFF_MASK = 0x1FFFFFFF
355484
356485# approximate sk_buffer kernel struct overhead.
0 commit comments