Skip to content

Commit 84e1f36

Browse files
illuusiohardbytefelixdivo
authored
Add Neousys WDT_DIO CAN interface (#980)
* Add Neousys WDT_DIO CAN interface * Neousys: Rename can/interfaces/neousys_wdt.py to can/interfaces/neousys/__init__.py * Neousys: reorganise stuff from init to own file and rename NeousysWdtBus to NeousysBus * Neousys: Remove threading code and start using Queue as thread safe message bug * Neousys: Add minimal unittest case(s) for creating, receiving, sending and shutdown * Update test_neousys.py * Neousys: Update Pylint disables that they are in more readable form Co-authored-by: Felix Divo <[email protected]> * Neousys: Make super-method more Python 3.x Co-authored-by: Felix Divo <[email protected]> Co-authored-by: Brian Thorne <[email protected]> Co-authored-by: Felix Divo <[email protected]>
1 parent fcb337d commit 84e1f36

File tree

4 files changed

+388
-0
lines changed

4 files changed

+388
-0
lines changed

can/interfaces/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"cantact": ("can.interfaces.cantact", "CantactBus"),
2929
"gs_usb": ("can.interfaces.gs_usb", "GsUsbBus"),
3030
"nixnet": ("can.interfaces.nixnet", "NiXNETcanBus"),
31+
"neousys": ("can.interfaces.neousys", "NeousysBus"),
3132
}
3233

3334
BACKENDS.update(

can/interfaces/neousys/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
""" Neousys CAN bus driver """
2+
3+
from can.interfaces.neousys.neousys import NeousysBus

can/interfaces/neousys/neousys.py

+271
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
""" Neousys CAN bus driver """
2+
3+
#
4+
# This kind of interface can be found for example on Neousys POC-551VTC
5+
# One needs to have correct drivers and DLL (Share object for Linux) from Neousys
6+
#
7+
# https://www.neousys-tech.com/en/support-service/resources/category/299-poc-551vtc-driver
8+
#
9+
# Beware this is only tested on Linux kernel higher than v5.3. This should be drop in
10+
# with Windows but you have to replace with correct named DLL
11+
#
12+
13+
# pylint: disable=too-few-public-methods
14+
# pylint: disable=too-many-instance-attributes
15+
# pylint: disable=wrong-import-position
16+
# pylint: disable=method-hidden
17+
# pylint: disable=unused-import
18+
19+
import warnings
20+
import queue
21+
import logging
22+
import platform
23+
import time
24+
25+
from ctypes import (
26+
byref,
27+
CFUNCTYPE,
28+
c_ubyte,
29+
c_uint,
30+
c_ushort,
31+
POINTER,
32+
sizeof,
33+
Structure,
34+
)
35+
36+
if platform.system() == "Windows":
37+
from ctypes import WinDLL
38+
else:
39+
from ctypes import CDLL
40+
from can import BusABC, Message
41+
42+
43+
logger = logging.getLogger(__name__)
44+
45+
46+
class NeousysCanSetup(Structure):
47+
""" C CAN Setup struct """
48+
49+
_fields_ = [
50+
("bitRate", c_uint),
51+
("recvConfig", c_uint),
52+
("recvId", c_uint),
53+
("recvMask", c_uint),
54+
]
55+
56+
57+
class NeousysCanMsg(Structure):
58+
""" C CAN Message struct """
59+
60+
_fields_ = [
61+
("id", c_uint),
62+
("flags", c_ushort),
63+
("extra", c_ubyte),
64+
("len", c_ubyte),
65+
("data", c_ubyte * 8),
66+
]
67+
68+
69+
# valid:2~16, sum of the Synchronization, Propagation, and
70+
# Phase Buffer 1 segments, measured in time quanta.
71+
# valid:1~8, the Phase Buffer 2 segment in time quanta.
72+
# valid:1~4, Resynchronization Jump Width in time quanta
73+
# valid:1~1023, CAN_CLK divider used to determine time quanta
74+
class NeousysCanBitClk(Structure):
75+
""" C CAN BIT Clock struct """
76+
77+
_fields_ = [
78+
("syncPropPhase1Seg", c_ushort),
79+
("phase2Seg", c_ushort),
80+
("jumpWidth", c_ushort),
81+
("quantumPrescaler", c_ushort),
82+
]
83+
84+
85+
NEOUSYS_CAN_MSG_CALLBACK = CFUNCTYPE(None, POINTER(NeousysCanMsg), c_uint)
86+
NEOUSYS_CAN_STATUS_CALLBACK = CFUNCTYPE(None, c_uint)
87+
88+
NEOUSYS_CAN_MSG_EXTENDED_ID = 0x0004
89+
NEOUSYS_CAN_MSG_REMOTE_FRAME = 0x0040
90+
NEOUSYS_CAN_MSG_DATA_NEW = 0x0080
91+
NEOUSYS_CAN_MSG_DATA_LOST = 0x0100
92+
93+
NEOUSYS_CAN_MSG_USE_ID_FILTER = 0x00000008
94+
NEOUSYS_CAN_MSG_USE_DIR_FILTER = (
95+
0x00000010 | NEOUSYS_CAN_MSG_USE_ID_FILTER
96+
) # only accept the direction specified in the message type
97+
NEOUSYS_CAN_MSG_USE_EXT_FILTER = (
98+
0x00000020 | NEOUSYS_CAN_MSG_USE_ID_FILTER
99+
) # filters on only extended identifiers
100+
101+
NEOUSYS_CAN_STATUS_BUS_OFF = 0x00000080
102+
NEOUSYS_CAN_STATUS_EWARN = (
103+
0x00000040 # can controller error level has reached warning level.
104+
)
105+
NEOUSYS_CAN_STATUS_EPASS = (
106+
0x00000020 # can controller error level has reached error passive level.
107+
)
108+
NEOUSYS_CAN_STATUS_LEC_STUFF = 0x00000001 # a bit stuffing error has occurred.
109+
NEOUSYS_CAN_STATUS_LEC_FORM = 0x00000002 # a formatting error has occurred.
110+
NEOUSYS_CAN_STATUS_LEC_ACK = 0x00000003 # an acknowledge error has occurred.
111+
NEOUSYS_CAN_STATUS_LEC_BIT1 = (
112+
0x00000004 # the bus remained a bit level of 1 for longer than is allowed.
113+
)
114+
NEOUSYS_CAN_STATUS_LEC_BIT0 = (
115+
0x00000005 # the bus remained a bit level of 0 for longer than is allowed.
116+
)
117+
NEOUSYS_CAN_STATUS_LEC_CRC = 0x00000006 # a crc error has occurred.
118+
NEOUSYS_CAN_STATUS_LEC_MASK = (
119+
0x00000007 # this is the mask for the can last error code (lec).
120+
)
121+
122+
NEOUSYS_CANLIB = None
123+
124+
try:
125+
if platform.system() == "Windows":
126+
NEOUSYS_CANLIB = WinDLL("./WDT_DIO.dll")
127+
else:
128+
NEOUSYS_CANLIB = CDLL("libwdt_dio.so")
129+
logger.info("Loaded Neousys WDT_DIO Can driver")
130+
except OSError as error:
131+
logger.info("Cannot Neousys CAN bus dll or share object: %d", format(error))
132+
# NEOUSYS_CANLIB = None
133+
134+
135+
class NeousysBus(BusABC):
136+
""" Neousys CAN bus Class"""
137+
138+
def __init__(self, channel, device=0, bitrate=500000, **kwargs):
139+
"""
140+
:param channel: channel number
141+
:param device: device number
142+
:param bitrate: bit rate. Renamed to bitrate in next release.
143+
"""
144+
super().__init__(channel, **kwargs)
145+
146+
if NEOUSYS_CANLIB is not None:
147+
self.channel = channel
148+
149+
self.device = device
150+
151+
self.channel_info = "Neousys Can: device {}, channel {}".format(
152+
self.device, self.channel
153+
)
154+
155+
self.queue = queue.Queue()
156+
157+
# Init with accept all and wanted bitrate
158+
self.init_config = NeousysCanSetup(
159+
bitrate, NEOUSYS_CAN_MSG_USE_ID_FILTER, 0, 0
160+
)
161+
162+
self._neousys_recv_cb = NEOUSYS_CAN_MSG_CALLBACK(self._neousys_recv_cb)
163+
self._neousys_status_cb = NEOUSYS_CAN_STATUS_CALLBACK(
164+
self._neousys_status_cb
165+
)
166+
167+
if NEOUSYS_CANLIB.CAN_RegisterReceived(0, self._neousys_recv_cb) == 0:
168+
logger.error("Neousys CAN bus Setup receive callback")
169+
170+
if NEOUSYS_CANLIB.CAN_RegisterStatus(0, self._neousys_status_cb) == 0:
171+
logger.error("Neousys CAN bus Setup status callback")
172+
173+
if (
174+
NEOUSYS_CANLIB.CAN_Setup(
175+
channel, byref(self.init_config), sizeof(self.init_config)
176+
)
177+
== 0
178+
):
179+
logger.error("Neousys CAN bus Setup Error")
180+
181+
if NEOUSYS_CANLIB.CAN_Start(channel) == 0:
182+
logger.error("Neousys CAN bus Start Error")
183+
184+
def send(self, msg, timeout=None):
185+
"""
186+
:param msg: message to send
187+
:param timeout: timeout is not used here
188+
:return:
189+
"""
190+
191+
if NEOUSYS_CANLIB is None:
192+
logger.error("Can't send msg as Neousys DLL/SO is not loaded")
193+
else:
194+
tx_msg = NeousysCanMsg(
195+
msg.arbitration_id, 0, 0, msg.dlc, (c_ubyte * 8)(*msg.data)
196+
)
197+
198+
if (
199+
NEOUSYS_CANLIB.CAN_Send(self.channel, byref(tx_msg), sizeof(tx_msg))
200+
== 0
201+
):
202+
logger.error("Neousys Can can't send message")
203+
204+
def _recv_internal(self, timeout):
205+
msg = None
206+
207+
if not self.queue.empty():
208+
msg = self.queue.get()
209+
210+
return msg, False
211+
212+
def _neousys_recv_cb(self, msg, sizeof_msg):
213+
"""
214+
:param msg struct CAN_MSG
215+
:param sizeof_msg message number
216+
:return:
217+
"""
218+
remote_frame = False
219+
extended_frame = False
220+
221+
msg_bytes = bytearray(msg.contents.data)
222+
223+
if msg.contents.flags & NEOUSYS_CAN_MSG_REMOTE_FRAME:
224+
remote_frame = True
225+
226+
if msg.contents.flags & NEOUSYS_CAN_MSG_EXTENDED_ID:
227+
extended_frame = True
228+
229+
if msg.contents.flags & NEOUSYS_CAN_MSG_DATA_LOST:
230+
logger.error("_neousys_recv_cb flag CAN_MSG_DATA_LOST")
231+
232+
msg = Message(
233+
timestamp=time.time(),
234+
arbitration_id=msg.contents.id,
235+
is_remote_frame=remote_frame,
236+
is_extended_id=extended_frame,
237+
channel=self.channel,
238+
dlc=msg.contents.len,
239+
data=msg_bytes[: msg.contents.len],
240+
)
241+
242+
# Reading happens in Callback function and
243+
# with Python-CAN it happens polling
244+
# so cache stuff in array to for poll
245+
if not self.queue.full():
246+
self.queue.put(msg)
247+
else:
248+
logger.error("Neousys message Queue is full")
249+
250+
def _neousys_status_cb(self, status):
251+
"""
252+
:param status BUS Status
253+
:return:
254+
"""
255+
logger.info("%s _neousys_status_cb: %d", self.init_config, status)
256+
257+
def shutdown(self):
258+
if NEOUSYS_CANLIB is not None:
259+
NEOUSYS_CANLIB.CAN_Stop(self.channel)
260+
261+
def fileno(self):
262+
# Return an invalid file descriptor as not used
263+
return -1
264+
265+
@staticmethod
266+
def _detect_available_configs():
267+
channels = []
268+
269+
# There is only one channel
270+
channels.append({"interface": "neousys", "channel": 0})
271+
return channels

test/test_neousys.py

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env python
2+
# coding: utf-8
3+
4+
import ctypes
5+
import os
6+
import pickle
7+
import unittest
8+
from unittest.mock import Mock
9+
10+
from ctypes import (
11+
byref,
12+
cast,
13+
POINTER,
14+
sizeof,
15+
c_ubyte,
16+
)
17+
18+
import pytest
19+
20+
import can
21+
from can.interfaces.neousys import neousys
22+
23+
24+
class TestNeousysBus(unittest.TestCase):
25+
def setUp(self) -> None:
26+
can.interfaces.neousys.neousys.NEOUSYS_CANLIB = Mock()
27+
can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_RegisterReceived = Mock(
28+
return_value=1
29+
)
30+
can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_RegisterStatus = Mock(
31+
return_value=1
32+
)
33+
can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Setup = Mock(return_value=1)
34+
can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Start = Mock(return_value=1)
35+
can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Send = Mock(return_value=1)
36+
can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Stop = Mock(return_value=1)
37+
self.bus = can.Bus(channel=0, bustype="neousys", _testing=True)
38+
39+
def tearDown(self) -> None:
40+
if self.bus:
41+
self.bus.shutdown()
42+
self.bus = None
43+
44+
def test_bus_creation(self) -> None:
45+
self.assertIsInstance(self.bus, neousys.NeousysBus)
46+
self.assertTrue(neousys.NEOUSYS_CANLIB.CAN_Setup.called)
47+
self.assertTrue(neousys.NEOUSYS_CANLIB.CAN_Start.called)
48+
self.assertTrue(neousys.NEOUSYS_CANLIB.CAN_RegisterReceived.called)
49+
self.assertTrue(neousys.NEOUSYS_CANLIB.CAN_RegisterStatus.called)
50+
self.assertTrue(neousys.NEOUSYS_CANLIB.CAN_Send.not_called)
51+
self.assertTrue(neousys.NEOUSYS_CANLIB.CAN_Stop.not_called)
52+
53+
CAN_Start_args = (
54+
can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Setup.call_args[0]
55+
)
56+
57+
# sizeof struct should be 16
58+
self.assertEqual(CAN_Start_args[0], 0)
59+
self.assertEqual(CAN_Start_args[2], 16)
60+
NeousysCanSetup_struct = cast(
61+
CAN_Start_args[1], POINTER(neousys.NeousysCanSetup)
62+
)
63+
self.assertEqual(NeousysCanSetup_struct.contents.bitRate, 500000)
64+
self.assertEqual(
65+
NeousysCanSetup_struct.contents.recvConfig,
66+
neousys.NEOUSYS_CAN_MSG_USE_ID_FILTER,
67+
)
68+
69+
def test_bus_creation_bitrate(self) -> None:
70+
self.bus = can.Bus(channel=0, bustype="neousys", bitrate=200000, _testing=True)
71+
self.assertIsInstance(self.bus, neousys.NeousysBus)
72+
CAN_Start_args = (
73+
can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Setup.call_args[0]
74+
)
75+
76+
# sizeof struct should be 16
77+
self.assertEqual(CAN_Start_args[0], 0)
78+
self.assertEqual(CAN_Start_args[2], 16)
79+
NeousysCanSetup_struct = cast(
80+
CAN_Start_args[1], POINTER(neousys.NeousysCanSetup)
81+
)
82+
self.assertEqual(NeousysCanSetup_struct.contents.bitRate, 200000)
83+
self.assertEqual(
84+
NeousysCanSetup_struct.contents.recvConfig,
85+
neousys.NEOUSYS_CAN_MSG_USE_ID_FILTER,
86+
)
87+
88+
def test_receive(self) -> None:
89+
recv_msg = self.bus.recv(timeout=0.05)
90+
self.assertEqual(recv_msg, None)
91+
msg_data = [0x01, 0x02, 0x03, 0x04, 0x05]
92+
NeousysCanMsg_msg = neousys.NeousysCanMsg(
93+
0x01, 0x00, 0x00, 0x05, (c_ubyte * 8)(*msg_data)
94+
)
95+
self.bus._neousys_recv_cb(byref(NeousysCanMsg_msg), sizeof(NeousysCanMsg_msg))
96+
recv_msg = self.bus.recv(timeout=0.05)
97+
self.assertEqual(recv_msg.dlc, 5)
98+
self.assertSequenceEqual(recv_msg.data, msg_data)
99+
100+
def test_send(self) -> None:
101+
msg = can.Message(
102+
arbitration_id=0x01, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=False
103+
)
104+
self.bus.send(msg)
105+
self.assertTrue(neousys.NEOUSYS_CANLIB.CAN_Send.called)
106+
107+
def test_shutdown(self) -> None:
108+
self.bus.shutdown()
109+
self.assertTrue(neousys.NEOUSYS_CANLIB.CAN_Stop.called)
110+
111+
112+
if __name__ == "__main__":
113+
unittest.main()

0 commit comments

Comments
 (0)