Skip to content

Commit 2bd4758

Browse files
authored
udp_multicast interface: support windows (#1914)
* support windows Neither the SO_TIMESTAMPNS / via recvmsg() method, nor the SIOCGSTAMP / ioctl() method for obtaining the packet timestamp is supported on Windows. This code adds a fallback to datetime, and switches to recvfrom() so that the udp_multicast bus becomes usable on windows. * clean-up example on windows, the example would otherwise cause an _enter_buffered_busy fatal error, notifier shutdown and bus shutdown are racing eachother... * fix double inversion * remove unused import * datetime to time.time() * add msgpack dependency for windows * enable udp_multicast back2back tests on windows * handle WSAEINVAL like EINVAL * avoid potential AttributeError on Linux * simplify unittest skip condition * fix formatting issue
1 parent 8e685ac commit 2bd4758

File tree

4 files changed

+54
-25
lines changed

4 files changed

+54
-25
lines changed

can/interfaces/udp_multicast/bus.py

+43-18
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import select
44
import socket
55
import struct
6+
import time
67
import warnings
78
from typing import List, Optional, Tuple, Union
89

@@ -12,9 +13,12 @@
1213

1314
from .utils import check_msgpack_installed, pack_message, unpack_message
1415

16+
ioctl_supported = True
17+
1518
try:
1619
from fcntl import ioctl
1720
except ModuleNotFoundError: # Missing on Windows
21+
ioctl_supported = False
1822
pass
1923

2024

@@ -30,6 +34,9 @@
3034
SO_TIMESTAMPNS = 35
3135
SIOCGSTAMP = 0x8906
3236

37+
# Additional constants for the interaction with the Winsock API
38+
WSAEINVAL = 10022
39+
3340

3441
class UdpMulticastBus(BusABC):
3542
"""A virtual interface for CAN communications between multiple processes using UDP over Multicast IP.
@@ -272,7 +279,11 @@ def _create_socket(self, address_family: socket.AddressFamily) -> socket.socket:
272279
try:
273280
sock.setsockopt(socket.SOL_SOCKET, SO_TIMESTAMPNS, 1)
274281
except OSError as error:
275-
if error.errno == errno.ENOPROTOOPT: # It is unavailable on macOS
282+
if (
283+
error.errno == errno.ENOPROTOOPT
284+
or error.errno == errno.EINVAL
285+
or error.errno == WSAEINVAL
286+
): # It is unavailable on macOS (ENOPROTOOPT) or windows(EINVAL/WSAEINVAL)
276287
self.timestamp_nanosecond = False
277288
else:
278289
raise error
@@ -353,18 +364,18 @@ def recv(
353364
) from exc
354365

355366
if ready_receive_sockets: # not empty
356-
# fetch data & source address
357-
(
358-
raw_message_data,
359-
ancillary_data,
360-
_, # flags
361-
sender_address,
362-
) = self._socket.recvmsg(
363-
self.max_buffer, self.received_ancillary_buffer_size
364-
)
365-
366367
# fetch timestamp; this is configured in _create_socket()
367368
if self.timestamp_nanosecond:
369+
# fetch data, timestamp & source address
370+
(
371+
raw_message_data,
372+
ancillary_data,
373+
_, # flags
374+
sender_address,
375+
) = self._socket.recvmsg(
376+
self.max_buffer, self.received_ancillary_buffer_size
377+
)
378+
368379
# Very similar to timestamp handling in can/interfaces/socketcan/socketcan.py -> capture_message()
369380
if len(ancillary_data) != 1:
370381
raise can.CanOperationError(
@@ -385,14 +396,28 @@ def recv(
385396
)
386397
timestamp = seconds + nanoseconds * 1.0e-9
387398
else:
388-
result_buffer = ioctl(
389-
self._socket.fileno(),
390-
SIOCGSTAMP,
391-
bytes(self.received_timestamp_struct_size),
392-
)
393-
seconds, microseconds = struct.unpack(
394-
self.received_timestamp_struct, result_buffer
399+
# fetch data & source address
400+
(raw_message_data, sender_address) = self._socket.recvfrom(
401+
self.max_buffer
395402
)
403+
404+
if ioctl_supported:
405+
result_buffer = ioctl(
406+
self._socket.fileno(),
407+
SIOCGSTAMP,
408+
bytes(self.received_timestamp_struct_size),
409+
)
410+
seconds, microseconds = struct.unpack(
411+
self.received_timestamp_struct, result_buffer
412+
)
413+
else:
414+
# fallback to time.time_ns
415+
now = time.time()
416+
417+
# Extract seconds and microseconds
418+
seconds = int(now)
419+
microseconds = int((now - seconds) * 1000000)
420+
396421
if microseconds >= 1e6:
397422
raise can.CanOperationError(
398423
f"Timestamp microseconds field was out of range: {microseconds} not less than 1e6"

doc/interfaces/udp_multicast.rst

+3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ from ``bus_1`` to ``bus_2``:
5353
# give the notifier enough time to get triggered by the second bus
5454
time.sleep(2.0)
5555
56+
# clean-up
57+
notifier.stop()
58+
5659
5760
Bus Class Documentation
5861
-----------------------

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ dependencies = [
1111
"wrapt~=1.10",
1212
"packaging >= 23.1",
1313
"typing_extensions>=3.10.0.0",
14-
"msgpack~=1.1.0; platform_system != 'Windows'",
14+
"msgpack~=1.1.0",
1515
]
1616
requires-python = ">=3.8"
1717
license = { text = "LGPL v3" }

test/back2back_test.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
IS_PYPY,
2222
IS_TRAVIS,
2323
IS_UNIX,
24+
IS_WINDOWS,
2425
TEST_CAN_FD,
2526
TEST_INTERFACE_SOCKETCAN,
2627
)
@@ -302,9 +303,9 @@ class BasicTestSocketCan(Back2BackTestCase):
302303

303304
# this doesn't even work on Travis CI for macOS; for example, see
304305
# https://travis-ci.org/github/hardbyte/python-can/jobs/745389871
305-
@unittest.skipUnless(
306-
IS_UNIX and not (IS_CI and IS_OSX),
307-
"only supported on Unix systems (but not on macOS at Travis CI and GitHub Actions)",
306+
@unittest.skipIf(
307+
IS_CI and IS_OSX,
308+
"not supported for macOS CI",
308309
)
309310
class BasicTestUdpMulticastBusIPv4(Back2BackTestCase):
310311
INTERFACE_1 = "udp_multicast"
@@ -319,9 +320,9 @@ def test_unique_message_instances(self):
319320

320321
# this doesn't even work for loopback multicast addresses on Travis CI; for example, see
321322
# https://travis-ci.org/github/hardbyte/python-can/builds/745065503
322-
@unittest.skipUnless(
323-
IS_UNIX and not (IS_TRAVIS or (IS_CI and IS_OSX)),
324-
"only supported on Unix systems (but not on Travis CI; and not on macOS at GitHub Actions)",
323+
@unittest.skipIf(
324+
IS_CI and IS_OSX,
325+
"not supported for macOS CI",
325326
)
326327
class BasicTestUdpMulticastBusIPv6(Back2BackTestCase):
327328
HOST_LOCAL_MCAST_GROUP_IPv6 = "ff11:7079:7468:6f6e:6465:6d6f:6d63:6173"

0 commit comments

Comments
 (0)