Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add usage of select instead of pulling for Linux based platform (PCAN Interface) #1410

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions can/interfaces/pcan/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@

import logging

if platform.system() == "Windows":
import winreg
PLATFORM = platform.system()
IS_WINDOWS = PLATFORM == "Windows"
IS_LINUX = PLATFORM == "Linux"

if IS_WINDOWS:
import winreg

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

Expand Down
178 changes: 102 additions & 76 deletions can/interfaces/pcan/pcan.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
"""
Enable basic CAN over a PCAN USB device.
"""

import logging
import time
from datetime import datetime
import platform
from typing import Optional, List
from typing import Optional, List, Tuple

from packaging import version

Expand Down Expand Up @@ -50,6 +49,8 @@
PCAN_LISTEN_ONLY,
PCAN_PARAMETER_OFF,
TPCANHandle,
IS_LINUX,
IS_WINDOWS,
PCAN_PCIBUS1,
PCAN_USBBUS1,
PCAN_PCCBUS1,
Expand All @@ -70,7 +71,6 @@

MIN_PCAN_API_VERSION = version.parse("4.2.0")


try:
# use the "uptime" library if available
import uptime
Expand All @@ -86,22 +86,27 @@
)
boottimeEpoch = 0

try:
# Try builtin Python 3 Windows API
from _overlapped import CreateEvent
from _winapi import WaitForSingleObject, WAIT_OBJECT_0, INFINITE
HAS_EVENTS = False

HAS_EVENTS = True
except ImportError:
if IS_WINDOWS:
try:
# Try pywin32 package
from win32event import CreateEvent
from win32event import WaitForSingleObject, WAIT_OBJECT_0, INFINITE
# Try builtin Python 3 Windows API
from _overlapped import CreateEvent
from _winapi import WaitForSingleObject, WAIT_OBJECT_0, INFINITE

HAS_EVENTS = True
except ImportError:
# Use polling instead
HAS_EVENTS = False
pass

elif IS_LINUX:
try:
import errno
import os
import select

HAS_EVENTS = True
except Exception:
pass


class PcanBus(BusABC):
Expand Down Expand Up @@ -294,10 +299,16 @@ def __init__(
raise PcanCanInitializationError(self._get_formatted_error(result))

if HAS_EVENTS:
self._recv_event = CreateEvent(None, 0, 0, None)
result = self.m_objPCANBasic.SetValue(
self.m_PcanHandle, PCAN_RECEIVE_EVENT, self._recv_event
)
if IS_WINDOWS:
self._recv_event = CreateEvent(None, 0, 0, None)
result = self.m_objPCANBasic.SetValue(
self.m_PcanHandle, PCAN_RECEIVE_EVENT, self._recv_event
)
elif IS_LINUX:
result, self._recv_event = self.m_objPCANBasic.GetValue(
self.m_PcanHandle, PCAN_RECEIVE_EVENT
)

if result != PCAN_ERROR_OK:
raise PcanCanInitializationError(self._get_formatted_error(result))

Expand Down Expand Up @@ -441,84 +452,96 @@ def set_device_number(self, device_number):
return False
return True

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

if HAS_EVENTS:
# We will utilize events for the timeout handling
timeout_ms = int(timeout * 1000) if timeout is not None else INFINITE
elif timeout is not None:
# Calculate max time
end_time = time.perf_counter() + timeout

# log.debug("Trying to read a msg")

result = None
while result is None:
while True:
if self.fd:
result = self.m_objPCANBasic.ReadFD(self.m_PcanHandle)
result, pcan_msg, pcan_timestamp = self.m_objPCANBasic.ReadFD(
self.m_PcanHandle
)
else:
result = self.m_objPCANBasic.Read(self.m_PcanHandle)
if result[0] == PCAN_ERROR_QRCVEMPTY:
if HAS_EVENTS:
result = None
val = WaitForSingleObject(self._recv_event, timeout_ms)
if val != WAIT_OBJECT_0:
return None, False
elif timeout is not None and time.perf_counter() >= end_time:
return None, False
result, pcan_msg, pcan_timestamp = self.m_objPCANBasic.Read(
self.m_PcanHandle
)

if result == PCAN_ERROR_OK:
# message received
break

if result == PCAN_ERROR_QRCVEMPTY:
# receive queue is empty, wait or return on timeout

if end_time is None:
time_left: Optional[float] = None
timed_out = False
else:
result = None
time_left = max(0.0, end_time - time.time())
timed_out = time_left == 0.0

if timed_out:
return None, False

if not HAS_EVENTS:
# polling mode
time.sleep(0.001)
elif result[0] & (PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY):
log.warning(self._get_formatted_error(result[0]))
return None, False
elif result[0] != PCAN_ERROR_OK:
raise PcanCanOperationError(self._get_formatted_error(result[0]))

theMsg = result[1]
itsTimeStamp = result[2]

# log.debug("Received a message")

is_extended_id = (
theMsg.MSGTYPE & PCAN_MESSAGE_EXTENDED.value
) == PCAN_MESSAGE_EXTENDED.value
is_remote_frame = (
theMsg.MSGTYPE & PCAN_MESSAGE_RTR.value
) == PCAN_MESSAGE_RTR.value
is_fd = (theMsg.MSGTYPE & PCAN_MESSAGE_FD.value) == PCAN_MESSAGE_FD.value
bitrate_switch = (
theMsg.MSGTYPE & PCAN_MESSAGE_BRS.value
) == PCAN_MESSAGE_BRS.value
error_state_indicator = (
theMsg.MSGTYPE & PCAN_MESSAGE_ESI.value
) == PCAN_MESSAGE_ESI.value
is_error_frame = (
theMsg.MSGTYPE & PCAN_MESSAGE_ERRFRAME.value
) == PCAN_MESSAGE_ERRFRAME.value
continue

if IS_WINDOWS:
# Windows with event
if time_left is None:
time_left_ms = INFINITE
else:
time_left_ms = int(time_left * 1000)
_ret = WaitForSingleObject(self._recv_event, time_left_ms)
if _ret == WAIT_OBJECT_0:
continue

elif IS_LINUX:
# Linux with event
recv, _, _ = select.select([self._recv_event], [], [], time_left)
if self._recv_event in recv:
continue

elif result & (PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY):
log.warning(self._get_formatted_error(result))

else:
raise PcanCanOperationError(self._get_formatted_error(result))

return None, False

is_extended_id = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_EXTENDED.value)
is_remote_frame = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_RTR.value)
is_fd = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_FD.value)
bitrate_switch = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_BRS.value)
error_state_indicator = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_ESI.value)
is_error_frame = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_ERRFRAME.value)

if self.fd:
dlc = dlc2len(theMsg.DLC)
timestamp = boottimeEpoch + (itsTimeStamp.value / (1000.0 * 1000.0))
dlc = dlc2len(pcan_msg.DLC)
timestamp = boottimeEpoch + (pcan_timestamp.value / (1000.0 * 1000.0))
else:
dlc = theMsg.LEN
dlc = pcan_msg.LEN
timestamp = boottimeEpoch + (
(
itsTimeStamp.micros
+ 1000 * itsTimeStamp.millis
+ 0x100000000 * 1000 * itsTimeStamp.millis_overflow
pcan_timestamp.micros
+ 1000 * pcan_timestamp.millis
+ 0x100000000 * 1000 * pcan_timestamp.millis_overflow
)
/ (1000.0 * 1000.0)
)

rx_msg = Message(
timestamp=timestamp,
arbitration_id=theMsg.ID,
arbitration_id=pcan_msg.ID,
is_extended_id=is_extended_id,
is_remote_frame=is_remote_frame,
is_error_frame=is_error_frame,
dlc=dlc,
data=theMsg.DATA[:dlc],
data=pcan_msg.DATA[:dlc],
is_fd=is_fd,
bitrate_switch=bitrate_switch,
error_state_indicator=error_state_indicator,
Expand Down Expand Up @@ -597,6 +620,9 @@ def flash(self, flash):

def shutdown(self):
super().shutdown()
if HAS_EVENTS and IS_LINUX:
self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_RECEIVE_EVENT, 0)

self.m_objPCANBasic.Uninitialize(self.m_PcanHandle)

@property
Expand Down
9 changes: 6 additions & 3 deletions test/test_pcan.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import platform
import unittest
from unittest import mock
from unittest.mock import Mock
from unittest.mock import Mock, patch


import pytest
from parameterized import parameterized
Expand All @@ -30,7 +31,6 @@ def setUp(self) -> None:
self.mock_pcan.SetValue = Mock(return_value=PCAN_ERROR_OK)
self.mock_pcan.GetValue = self._mockGetValue
self.PCAN_API_VERSION_SIM = "4.2"

self.bus = None

def tearDown(self) -> None:
Expand All @@ -45,6 +45,8 @@ def _mockGetValue(self, channel, parameter):
"""
if parameter == PCAN_API_VERSION:
return PCAN_ERROR_OK, self.PCAN_API_VERSION_SIM.encode("ascii")
elif parameter == PCAN_RECEIVE_EVENT:
return PCAN_ERROR_OK, int.from_bytes(PCAN_RECEIVE_EVENT, "big")
raise NotImplementedError(
f"No mock return value specified for parameter {parameter}"
)
Expand Down Expand Up @@ -205,7 +207,8 @@ def test_recv_fd(self):
self.assertEqual(recv_msg.timestamp, 0)

@pytest.mark.timeout(3.0)
def test_recv_no_message(self):
@patch("select.select", return_value=([], [], []))
def test_recv_no_message(self, mock_select):
self.mock_pcan.Read = Mock(return_value=(PCAN_ERROR_QRCVEMPTY, None, None))
self.bus = can.Bus(interface="pcan")
self.assertEqual(self.bus.recv(timeout=0.5), None)
Expand Down