1
+ import errno
1
2
import logging
2
3
import select
3
4
import socket
4
5
import struct
5
6
7
+ try :
8
+ from fcntl import ioctl
9
+ except ModuleNotFoundError : # Missing on Windows
10
+ pass
11
+
6
12
from typing import List , Optional , Tuple , Union
7
13
8
14
log = logging .getLogger (__name__ )
21
27
22
28
# Additional constants for the interaction with Unix kernels
23
29
SO_TIMESTAMPNS = 35
30
+ SIOCGSTAMP = 0x8906
24
31
25
32
26
33
class UdpMulticastBus (BusABC ):
@@ -174,6 +181,9 @@ def __init__(
174
181
self .hop_limit = hop_limit
175
182
self .max_buffer = max_buffer
176
183
184
+ # `False` will always work, no matter the setup. This might be changed by _create_socket().
185
+ self .timestamp_nanosecond = False
186
+
177
187
# Look up multicast group address in name server and find out IP version of the first suitable target
178
188
# and then get the address family of it (socket.AF_INET or socket.AF_INET6)
179
189
connection_candidates = socket .getaddrinfo ( # type: ignore
@@ -200,8 +210,15 @@ def __init__(
200
210
201
211
# used in recv()
202
212
self .received_timestamp_struct = "@ll"
203
- ancillary_data_size = struct .calcsize (self .received_timestamp_struct )
204
- self .received_ancillary_buffer_size = socket .CMSG_SPACE (ancillary_data_size )
213
+ self .received_timestamp_struct_size = struct .calcsize (
214
+ self .received_timestamp_struct
215
+ )
216
+ if self .timestamp_nanosecond :
217
+ self .received_ancillary_buffer_size = socket .CMSG_SPACE (
218
+ self .received_timestamp_struct_size
219
+ )
220
+ else :
221
+ self .received_ancillary_buffer_size = 0
205
222
206
223
# used by send()
207
224
self ._send_destination = (self .group , self .port )
@@ -238,7 +255,15 @@ def _create_socket(self, address_family: socket.AddressFamily) -> socket.socket:
238
255
sock .setsockopt (socket .SOL_SOCKET , socket .SO_REUSEADDR , 1 )
239
256
240
257
# set how to receive timestamps
241
- sock .setsockopt (socket .SOL_SOCKET , SO_TIMESTAMPNS , 1 )
258
+ try :
259
+ sock .setsockopt (socket .SOL_SOCKET , SO_TIMESTAMPNS , 1 )
260
+ except OSError as error :
261
+ if error .errno == errno .ENOPROTOOPT : # It is unavailable on macOS
262
+ self .timestamp_nanosecond = False
263
+ else :
264
+ raise error
265
+ else :
266
+ self .timestamp_nanosecond = True
242
267
243
268
# Bind it to the port (on any interface)
244
269
sock .bind (("" , self .port ))
@@ -272,18 +297,22 @@ def send(self, data: bytes, timeout: Optional[float] = None) -> None:
272
297
273
298
:param timeout: the timeout in seconds after which an Exception is raised is sending has failed
274
299
:param data: the data to be sent
275
- :raises OSError: if an error occurred while writing to the underlying socket
276
- :raises socket.timeout: if the timeout ran out before sending was completed (this is a subclass of
277
- *OSError*)
300
+ :raises can.CanOperationError: if an error occurred while writing to the underlying socket
301
+ :raises can.CanTimeoutError: if the timeout ran out before sending was completed
278
302
"""
279
303
if timeout != self ._last_send_timeout :
280
304
self ._last_send_timeout = timeout
281
305
# this applies to all blocking calls on the socket, but sending is the only one that is blocking
282
306
self ._socket .settimeout (timeout )
283
307
284
- bytes_sent = self ._socket .sendto (data , self ._send_destination )
285
- if bytes_sent < len (data ):
286
- raise socket .timeout ()
308
+ try :
309
+ bytes_sent = self ._socket .sendto (data , self ._send_destination )
310
+ if bytes_sent < len (data ):
311
+ raise TimeoutError ()
312
+ except TimeoutError :
313
+ raise can .CanTimeoutError () from None
314
+ except OSError as error :
315
+ raise can .CanOperationError ("failed to send via socket" ) from error
287
316
288
317
def recv (
289
318
self , timeout : Optional [float ] = None
@@ -320,21 +349,41 @@ def recv(
320
349
self .max_buffer , self .received_ancillary_buffer_size
321
350
)
322
351
323
- # fetch timestamp; this is configured in in _create_socket()
324
- assert len (ancillary_data ) == 1 , "only requested a single extra field"
325
- cmsg_level , cmsg_type , cmsg_data = ancillary_data [0 ]
326
- assert (
327
- cmsg_level == socket .SOL_SOCKET and cmsg_type == SO_TIMESTAMPNS
328
- ), "received control message type that was not requested"
329
- # see https://man7.org/linux/man-pages/man3/timespec.3.html -> struct timespec for details
330
- seconds , nanoseconds = struct .unpack (
331
- self .received_timestamp_struct , cmsg_data
332
- )
333
- if nanoseconds >= 1e9 :
334
- raise can .CanError (
335
- f"Timestamp nanoseconds field was out of range: { nanoseconds } not less than 1e9"
352
+ # fetch timestamp; this is configured in _create_socket()
353
+ if self .timestamp_nanosecond :
354
+ # Very similar to timestamp handling in can/interfaces/socketcan/socketcan.py -> capture_message()
355
+ if len (ancillary_data ) != 1 :
356
+ raise can .CanOperationError (
357
+ "Only requested a single extra field but got a different amount"
358
+ )
359
+ cmsg_level , cmsg_type , cmsg_data = ancillary_data [0 ]
360
+ if cmsg_level != socket .SOL_SOCKET or cmsg_type != SO_TIMESTAMPNS :
361
+ raise can .CanOperationError (
362
+ "received control message type that was not requested"
363
+ )
364
+ # see https://man7.org/linux/man-pages/man3/timespec.3.html -> struct timespec for details
365
+ seconds , nanoseconds = struct .unpack (
366
+ self .received_timestamp_struct , cmsg_data
367
+ )
368
+ if nanoseconds >= 1e9 :
369
+ raise can .CanOperationError (
370
+ f"Timestamp nanoseconds field was out of range: { nanoseconds } not less than 1e9"
371
+ )
372
+ timestamp = seconds + nanoseconds * 1.0e-9
373
+ else :
374
+ result_buffer = ioctl (
375
+ self ._socket .fileno (),
376
+ SIOCGSTAMP ,
377
+ bytes (self .received_timestamp_struct_size ),
378
+ )
379
+ seconds , microseconds = struct .unpack (
380
+ self .received_timestamp_struct , result_buffer
336
381
)
337
- timestamp = seconds + nanoseconds * 1.0e-9
382
+ if microseconds >= 1e6 :
383
+ raise can .CanOperationError (
384
+ f"Timestamp microseconds field was out of range: { microseconds } not less than 1e6"
385
+ )
386
+ timestamp = seconds + microseconds * 1e-6
338
387
339
388
return raw_message_data , sender_address , timestamp
340
389
0 commit comments