Skip to content

Commit 5c523ec

Browse files
Add usage of select instead of polling for Linux based platform (PCAN Interface) (#1410)
* - integrate handling of Linux based event using select for pcan interface * - adapt pcan unit tests regarding PCAN_RECEIVE_EVENT GetValue call * - add the case HAS_EVENTS = FALSE whne no special event basic mechanism is importes * - force select mock for test_recv_no_message * - just for testing : see what is going wrong with CI testing and my setup * - correct patch on pcan test test_recv_no_message - run black * - remove debug log * -just reformat test_pcan * - move global variable IS_WINDOW and IS_LINUX from pcan to basic module * - remove redundant comment in pcan _recv_internal * refactor _recv_internal Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com>
1 parent 35de98e commit 5c523ec

File tree

3 files changed

+113
-81
lines changed

3 files changed

+113
-81
lines changed

can/interfaces/pcan/basic.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121

2222
import logging
2323

24-
if platform.system() == "Windows":
25-
import winreg
24+
PLATFORM = platform.system()
25+
IS_WINDOWS = PLATFORM == "Windows"
26+
IS_LINUX = PLATFORM == "Linux"
2627

28+
if IS_WINDOWS:
29+
import winreg
2730

2831
logger = logging.getLogger("can.pcan")
2932

can/interfaces/pcan/pcan.py

Lines changed: 102 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
"""
22
Enable basic CAN over a PCAN USB device.
33
"""
4-
54
import logging
65
import time
76
from datetime import datetime
87
import platform
9-
from typing import Optional, List
8+
from typing import Optional, List, Tuple
109

1110
from packaging import version
1211

@@ -50,6 +49,8 @@
5049
PCAN_LISTEN_ONLY,
5150
PCAN_PARAMETER_OFF,
5251
TPCANHandle,
52+
IS_LINUX,
53+
IS_WINDOWS,
5354
PCAN_PCIBUS1,
5455
PCAN_USBBUS1,
5556
PCAN_PCCBUS1,
@@ -70,7 +71,6 @@
7071

7172
MIN_PCAN_API_VERSION = version.parse("4.2.0")
7273

73-
7474
try:
7575
# use the "uptime" library if available
7676
import uptime
@@ -86,22 +86,27 @@
8686
)
8787
boottimeEpoch = 0
8888

89-
try:
90-
# Try builtin Python 3 Windows API
91-
from _overlapped import CreateEvent
92-
from _winapi import WaitForSingleObject, WAIT_OBJECT_0, INFINITE
89+
HAS_EVENTS = False
9390

94-
HAS_EVENTS = True
95-
except ImportError:
91+
if IS_WINDOWS:
9692
try:
97-
# Try pywin32 package
98-
from win32event import CreateEvent
99-
from win32event import WaitForSingleObject, WAIT_OBJECT_0, INFINITE
93+
# Try builtin Python 3 Windows API
94+
from _overlapped import CreateEvent
95+
from _winapi import WaitForSingleObject, WAIT_OBJECT_0, INFINITE
10096

10197
HAS_EVENTS = True
10298
except ImportError:
103-
# Use polling instead
104-
HAS_EVENTS = False
99+
pass
100+
101+
elif IS_LINUX:
102+
try:
103+
import errno
104+
import os
105+
import select
106+
107+
HAS_EVENTS = True
108+
except Exception:
109+
pass
105110

106111

107112
class PcanBus(BusABC):
@@ -294,10 +299,16 @@ def __init__(
294299
raise PcanCanInitializationError(self._get_formatted_error(result))
295300

296301
if HAS_EVENTS:
297-
self._recv_event = CreateEvent(None, 0, 0, None)
298-
result = self.m_objPCANBasic.SetValue(
299-
self.m_PcanHandle, PCAN_RECEIVE_EVENT, self._recv_event
300-
)
302+
if IS_WINDOWS:
303+
self._recv_event = CreateEvent(None, 0, 0, None)
304+
result = self.m_objPCANBasic.SetValue(
305+
self.m_PcanHandle, PCAN_RECEIVE_EVENT, self._recv_event
306+
)
307+
elif IS_LINUX:
308+
result, self._recv_event = self.m_objPCANBasic.GetValue(
309+
self.m_PcanHandle, PCAN_RECEIVE_EVENT
310+
)
311+
301312
if result != PCAN_ERROR_OK:
302313
raise PcanCanInitializationError(self._get_formatted_error(result))
303314

@@ -441,84 +452,96 @@ def set_device_number(self, device_number):
441452
return False
442453
return True
443454

444-
def _recv_internal(self, timeout):
455+
def _recv_internal(
456+
self, timeout: Optional[float]
457+
) -> Tuple[Optional[Message], bool]:
458+
end_time = time.time() + timeout if timeout is not None else None
445459

446-
if HAS_EVENTS:
447-
# We will utilize events for the timeout handling
448-
timeout_ms = int(timeout * 1000) if timeout is not None else INFINITE
449-
elif timeout is not None:
450-
# Calculate max time
451-
end_time = time.perf_counter() + timeout
452-
453-
# log.debug("Trying to read a msg")
454-
455-
result = None
456-
while result is None:
460+
while True:
457461
if self.fd:
458-
result = self.m_objPCANBasic.ReadFD(self.m_PcanHandle)
462+
result, pcan_msg, pcan_timestamp = self.m_objPCANBasic.ReadFD(
463+
self.m_PcanHandle
464+
)
459465
else:
460-
result = self.m_objPCANBasic.Read(self.m_PcanHandle)
461-
if result[0] == PCAN_ERROR_QRCVEMPTY:
462-
if HAS_EVENTS:
463-
result = None
464-
val = WaitForSingleObject(self._recv_event, timeout_ms)
465-
if val != WAIT_OBJECT_0:
466-
return None, False
467-
elif timeout is not None and time.perf_counter() >= end_time:
468-
return None, False
466+
result, pcan_msg, pcan_timestamp = self.m_objPCANBasic.Read(
467+
self.m_PcanHandle
468+
)
469+
470+
if result == PCAN_ERROR_OK:
471+
# message received
472+
break
473+
474+
if result == PCAN_ERROR_QRCVEMPTY:
475+
# receive queue is empty, wait or return on timeout
476+
477+
if end_time is None:
478+
time_left: Optional[float] = None
479+
timed_out = False
469480
else:
470-
result = None
481+
time_left = max(0.0, end_time - time.time())
482+
timed_out = time_left == 0.0
483+
484+
if timed_out:
485+
return None, False
486+
487+
if not HAS_EVENTS:
488+
# polling mode
471489
time.sleep(0.001)
472-
elif result[0] & (PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY):
473-
log.warning(self._get_formatted_error(result[0]))
474-
return None, False
475-
elif result[0] != PCAN_ERROR_OK:
476-
raise PcanCanOperationError(self._get_formatted_error(result[0]))
477-
478-
theMsg = result[1]
479-
itsTimeStamp = result[2]
480-
481-
# log.debug("Received a message")
482-
483-
is_extended_id = (
484-
theMsg.MSGTYPE & PCAN_MESSAGE_EXTENDED.value
485-
) == PCAN_MESSAGE_EXTENDED.value
486-
is_remote_frame = (
487-
theMsg.MSGTYPE & PCAN_MESSAGE_RTR.value
488-
) == PCAN_MESSAGE_RTR.value
489-
is_fd = (theMsg.MSGTYPE & PCAN_MESSAGE_FD.value) == PCAN_MESSAGE_FD.value
490-
bitrate_switch = (
491-
theMsg.MSGTYPE & PCAN_MESSAGE_BRS.value
492-
) == PCAN_MESSAGE_BRS.value
493-
error_state_indicator = (
494-
theMsg.MSGTYPE & PCAN_MESSAGE_ESI.value
495-
) == PCAN_MESSAGE_ESI.value
496-
is_error_frame = (
497-
theMsg.MSGTYPE & PCAN_MESSAGE_ERRFRAME.value
498-
) == PCAN_MESSAGE_ERRFRAME.value
490+
continue
491+
492+
if IS_WINDOWS:
493+
# Windows with event
494+
if time_left is None:
495+
time_left_ms = INFINITE
496+
else:
497+
time_left_ms = int(time_left * 1000)
498+
_ret = WaitForSingleObject(self._recv_event, time_left_ms)
499+
if _ret == WAIT_OBJECT_0:
500+
continue
501+
502+
elif IS_LINUX:
503+
# Linux with event
504+
recv, _, _ = select.select([self._recv_event], [], [], time_left)
505+
if self._recv_event in recv:
506+
continue
507+
508+
elif result & (PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY):
509+
log.warning(self._get_formatted_error(result))
510+
511+
else:
512+
raise PcanCanOperationError(self._get_formatted_error(result))
513+
514+
return None, False
515+
516+
is_extended_id = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_EXTENDED.value)
517+
is_remote_frame = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_RTR.value)
518+
is_fd = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_FD.value)
519+
bitrate_switch = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_BRS.value)
520+
error_state_indicator = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_ESI.value)
521+
is_error_frame = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_ERRFRAME.value)
499522

500523
if self.fd:
501-
dlc = dlc2len(theMsg.DLC)
502-
timestamp = boottimeEpoch + (itsTimeStamp.value / (1000.0 * 1000.0))
524+
dlc = dlc2len(pcan_msg.DLC)
525+
timestamp = boottimeEpoch + (pcan_timestamp.value / (1000.0 * 1000.0))
503526
else:
504-
dlc = theMsg.LEN
527+
dlc = pcan_msg.LEN
505528
timestamp = boottimeEpoch + (
506529
(
507-
itsTimeStamp.micros
508-
+ 1000 * itsTimeStamp.millis
509-
+ 0x100000000 * 1000 * itsTimeStamp.millis_overflow
530+
pcan_timestamp.micros
531+
+ 1000 * pcan_timestamp.millis
532+
+ 0x100000000 * 1000 * pcan_timestamp.millis_overflow
510533
)
511534
/ (1000.0 * 1000.0)
512535
)
513536

514537
rx_msg = Message(
515538
timestamp=timestamp,
516-
arbitration_id=theMsg.ID,
539+
arbitration_id=pcan_msg.ID,
517540
is_extended_id=is_extended_id,
518541
is_remote_frame=is_remote_frame,
519542
is_error_frame=is_error_frame,
520543
dlc=dlc,
521-
data=theMsg.DATA[:dlc],
544+
data=pcan_msg.DATA[:dlc],
522545
is_fd=is_fd,
523546
bitrate_switch=bitrate_switch,
524547
error_state_indicator=error_state_indicator,
@@ -597,6 +620,9 @@ def flash(self, flash):
597620

598621
def shutdown(self):
599622
super().shutdown()
623+
if HAS_EVENTS and IS_LINUX:
624+
self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_RECEIVE_EVENT, 0)
625+
600626
self.m_objPCANBasic.Uninitialize(self.m_PcanHandle)
601627

602628
@property

test/test_pcan.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import platform
77
import unittest
88
from unittest import mock
9-
from unittest.mock import Mock
9+
from unittest.mock import Mock, patch
10+
1011

1112
import pytest
1213
from parameterized import parameterized
@@ -30,7 +31,6 @@ def setUp(self) -> None:
3031
self.mock_pcan.SetValue = Mock(return_value=PCAN_ERROR_OK)
3132
self.mock_pcan.GetValue = self._mockGetValue
3233
self.PCAN_API_VERSION_SIM = "4.2"
33-
3434
self.bus = None
3535

3636
def tearDown(self) -> None:
@@ -45,6 +45,8 @@ def _mockGetValue(self, channel, parameter):
4545
"""
4646
if parameter == PCAN_API_VERSION:
4747
return PCAN_ERROR_OK, self.PCAN_API_VERSION_SIM.encode("ascii")
48+
elif parameter == PCAN_RECEIVE_EVENT:
49+
return PCAN_ERROR_OK, int.from_bytes(PCAN_RECEIVE_EVENT, "big")
4850
raise NotImplementedError(
4951
f"No mock return value specified for parameter {parameter}"
5052
)
@@ -205,7 +207,8 @@ def test_recv_fd(self):
205207
self.assertEqual(recv_msg.timestamp, 0)
206208

207209
@pytest.mark.timeout(3.0)
208-
def test_recv_no_message(self):
210+
@patch("select.select", return_value=([], [], []))
211+
def test_recv_no_message(self, mock_select):
209212
self.mock_pcan.Read = Mock(return_value=(PCAN_ERROR_QRCVEMPTY, None, None))
210213
self.bus = can.Bus(interface="pcan")
211214
self.assertEqual(self.bus.recv(timeout=0.5), None)

0 commit comments

Comments
 (0)