-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathcaninterface_bcm.py
More file actions
336 lines (282 loc) · 15.3 KB
/
caninterface_bcm.py
File metadata and controls
336 lines (282 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# -*- coding: utf-8 -*-
#
# Author: Jonas Berg
# Copyright (c) 2015, Semcon Sweden AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted
# provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
# following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
# the following disclaimer in the documentation and/or other materials provided with the distribution.
# 3. Neither the name of the Semcon Sweden AB nor the names of its contributors may be used to endorse or
# promote products derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
import errno
import socket
import struct
import sys
from . import canframe
from . import constants
from . import exceptions
from . import utilities
class SocketCanBcmInterface():
"""
A Linux SocketCAN interface, using the Broadcast Manager (BCM) in the Linux kernel.
Args:
interfacename (str): For example 'vcan0' or 'can1'
timeout (numerical or None): Timeout value in seconds receiving BCM messages from the kernel. Defaults
to None (blocking).
Raises:
CanException: For interface problems. See :exc:`.CanException`.
CanTimeoutException: At timeout. See :exc:`.CanTimeoutException`.
"""
def __init__(self, interfacename, timeout=None):
assert sys.version_info >= (3, 4, 0), "Python version 3.4 or later required for using SocketCAN BCM!"
self._interfacename = str(interfacename)
self._socket = socket.socket(socket.PF_CAN, socket.SOCK_DGRAM, socket.CAN_BCM)
try:
self._socket.settimeout(timeout)
self._socket.connect((self._interfacename,))
except (ValueError, TypeError):
self.close()
raise exceptions.CanException("Wrong timeout value for CAN interface {}: {!r}".format(
self._interfacename, timeout))
except OSError:
self.close()
raise exceptions.CanException("Could not open CAN interface {}".format(self._interfacename))
def __repr__(self):
return "SocketCan BCM interface: {}".format(self._interfacename)
@property
def interfacename(self):
"""Get the interface name (read-only). The interface name is set in the constructor."""
return self._interfacename
def close(self):
"""Close the socket"""
self._socket.close()
def recv_next_frame(self):
"""Receive one CAN frame.
Returns a :class:`.CanFrame` object.
"""
# Receive BCM message
try:
raw_message, address = self._socket.recvfrom(constants.MAX_NUMBER_OF_BYTES_FROM_BCM)
except socket.timeout:
raise exceptions.CanTimeoutException("Timeout when reading BCM message from CAN interface {}".format(
self._interfacename))
except OSError as e:
if e.errno == errno.EBADF: # 9 on Linux
raise exceptions.CanException("The BCM socket seems to be closed. CAN interface: {}".format(
self._interfacename))
elif e.errno == errno.ENETDOWN: # 100 on Linux
raise exceptions.CanException("The CAN interface {} seems to be down.".format(
self._interfacename))
raise
# Parse BCM header
try:
opcode, flags, ival1_count, ival1, ival2, frame_id, frame_format, number_of_bcm_frames = \
_parse_bcm_header(raw_message[0:constants.SIZE_BCM_HEADER])
except IndexError:
raise exceptions.CanException("Too short BCM header received: {!r}".format(raw_message))
except Exception as e:
raise exceptions.CanException("Malformed BCM header received: {!r}. Exception: {r}".format(
raw_message, e))
if opcode != socket.CAN_BCM_RX_CHANGED:
raise exceptions.CanException("Wrong BCM message opcode: {!r}".format(raw_message))
# Extract CAN frame
try:
raw_frame = raw_message[constants.SIZE_BCM_HEADER:
constants.SIZE_BCM_HEADER + constants.SIZE_CAN_RAWFRAME]
except IndexError:
raise exceptions.CanException("Too short BCM frame received: {!r}".format(raw_message))
return canframe.CanFrame.from_rawframe(raw_frame)
def send_frame(self, input_frame):
"""Send a single CAN frame (a :class:`.CanFrame` object)"""
try:
header = _build_bcm_header(socket.CAN_BCM_TX_SEND,
flags=0,
interval=0,
frame_id=input_frame.frame_id,
frame_format=input_frame.frame_format,
number_of_bcm_frames=1)
except AttributeError:
raise exceptions.CanException("The input_frame is wrong: {!r}".format(input_frame))
self._send_via_socket(header + input_frame.get_rawframe())
def setup_periodic_send(self, input_frame, interval=None, restart_timer=True):
"""Setup periodic transmission for a frame ID.
Args:
input_frame (:class:`.CanFrame` object): The frame (including data and frame ID) to send periodically.
interval (float or None): Interval between consecutive transmissions (in milliseconds). Defaults
to None (do not update the timing information).
restart_timer (bool): Start or restart the transmission timer. Defaults to True. Set
this to false if you just would like to update the data to be sent, but not force
reset of the transmission timer.
"""
# Possibly use the TX_ANNOUNCE flag to emit data changes immediately (does not affect timer cycle)
flags = 0
if interval is None:
interval = 0
else:
try:
float(interval)
except ValueError:
raise exceptions.CanException("Wrong interval type: {!r}".format(interval))
if interval < 0:
raise exceptions.CanException("Wrong interval: {!r}".format(interval))
flags |= constants.BCM_FLAG_SETTIMER
restart_timer = bool(restart_timer)
if restart_timer:
flags |= constants.BCM_FLAG_STARTTIMER
try:
header = _build_bcm_header(socket.CAN_BCM_TX_SETUP,
flags=flags,
interval=interval,
frame_id=input_frame.frame_id,
frame_format=input_frame.frame_format,
number_of_bcm_frames=1)
except AttributeError:
raise exceptions.CanException("The input_frame is wrong: {!r}".format(input_frame))
self._send_via_socket(header + input_frame.get_rawframe())
def stop_periodic_send(self, frame_id, frame_format=constants.CAN_FRAMEFORMAT_STANDARD):
"""Stop the periodic transmission for this frame_id.
Args:
frame_id (int): Frame ID
frame_format (str): Frame format. Should be ``'standard'`` or ``'extended'``. Defaults to standard frame format.
"""
utilities.check_frame_id_and_format(frame_id, frame_format)
header = _build_bcm_header(socket.CAN_BCM_TX_DELETE,
flags=0,
interval=0,
frame_id=frame_id,
frame_format=frame_format,
number_of_bcm_frames=0)
self._send_via_socket(header)
def setup_reception(self, frame_id, frame_format=constants.CAN_FRAMEFORMAT_STANDARD,
min_interval=None, data_mask=None):
"""Setup reception for this frame_id (pretty much subscribe).
Args:
frame_id (int): Frame ID
frame_format (str): Frame format. Should be ``'standard'`` or ``'extended'``. Defaults to
standard frame format.
min_interval (float or None): Minimum interval between received frames (in milliseconds). Useful for
throttling rapid data streams. Defaults to None (no throttling). A min_interval of 0 corresponds to
no throttling.
data_mask (bytes or None): Enable filtering on data changes. The mask is bytes object (length 8 bytes). Set
the corresponding bits to 1 to detect data change in that location. Defaults to None (data is not studied
for changes, all incoming frames are given to the user).
"""
utilities.check_frame_id_and_format(frame_id, frame_format)
flags = 0
if min_interval is None:
min_interval = 0
else:
if min_interval < 0:
raise exceptions.CanException("Wrong min_interval: {!r}".format(min_interval))
elif min_interval > 0:
flags |= constants.BCM_FLAG_SETTIMER
if data_mask is None:
flags |= constants.BCM_FLAG_RX_FILTER_ID # All frames with this particular ID will be received.
masking_frame = canframe.CanFrame(frame_id,
constants.NULL_BYTE * constants.MAX_NUMBER_OF_CAN_DATA_BYTES,
frame_format)
else:
if len(data_mask) != constants.MAX_NUMBER_OF_CAN_DATA_BYTES:
raise exceptions.CanException("Wrong data_mask: {!r}".format(data_mask))
flags |= constants.BCM_FLAG_RX_CHECK_DLC # Detect also changes in DLC
masking_frame = canframe.CanFrame(frame_id, data_mask, frame_format)
header = _build_bcm_header(socket.CAN_BCM_RX_SETUP,
flags=flags,
interval=min_interval,
frame_id=frame_id,
frame_format=frame_format,
number_of_bcm_frames=1)
self._send_via_socket(header + masking_frame.get_rawframe())
def stop_reception(self, frame_id, frame_format=constants.CAN_FRAMEFORMAT_STANDARD):
"""Disable the reception for this frame_id.
Args:
frame_id (int): Frame ID
frame_format (str): Frame format. Should be ``'standard'`` or ``'extended'``. Defaults to standard frame format.
"""
utilities.check_frame_id_and_format(frame_id, frame_format)
header = _build_bcm_header(socket.CAN_BCM_RX_DELETE,
flags=0,
interval=0,
frame_id=frame_id,
frame_format=frame_format,
number_of_bcm_frames=0)
self._send_via_socket(header)
def _send_via_socket(self, input_bytes):
"""Send data on the object's socket. Handles OSError.
Args:
input_bytes (byte): Data to send
"""
try:
self._socket.send(input_bytes)
except OSError as e:
if e.errno == errno.EBADF: # 9 on Linux
template = "Could not send CAN BCM message on interface {}. The BCM socket seems to be closed."
raise exceptions.CanException(template.format(self._interfacename))
elif e.errno == errno.ENETDOWN: # 100 on Linux
template = "Could not send CAN BCM message on interface {}. The CAN interface seems to be down."
raise exceptions.CanException(template.format(self._interfacename))
elif e.errno == errno.EINVAL: # 22 on Linux
template = "Could not send CAN BCM message on interface {}. Linux kernel SocketCAN is protesting. " + \
"You are probably referring to a non-existing frame."
raise exceptions.CanNotFoundByKernelException(template.format(self._interfacename))
raise
def _build_bcm_header(opcode, flags, interval, frame_id, frame_format, number_of_bcm_frames):
"""
Build a BCM message header.
Args:
opcode (int): Command to the BCM
flags (int): Flags to the BCM
interval (float): Timing interval in milliseconds
frame_id (int): Frame ID.
frame_format (str): Frame format. Should be ``'standard'`` or ``'extended'``
number_of_bcm_frames (int): Number of attached raw frames to the header.
Returns the header as bytes (length 56 bytes)
Note that 'interval' is the ival2 in Linux kernel documentation.
"""
frame_id_std_ext = frame_id
if frame_format == constants.CAN_FRAMEFORMAT_EXTENDED:
frame_id_std_ext |= constants.CAN_MASK_EXTENDED_FRAME_BIT
interval_s = interval / constants.MILLISECONDS_PER_SECOND
ival1_count = 0
ival1 = 0
ival1_seconds, ival1_useconds = utilities.split_seconds_to_full_and_part(ival1)
ival2_seconds, ival2_useconds = utilities.split_seconds_to_full_and_part(interval_s)
return struct.pack(constants.FORMAT_BCM_HEADER,
opcode,
flags,
ival1_count,
ival1_seconds, ival1_useconds,
ival2_seconds, ival2_useconds,
frame_id_std_ext,
number_of_bcm_frames)
def _parse_bcm_header(header):
"""Parse a BCM message header.
Args:
header (bytes): BCM header. Should have a length of 56 bytes.
Returns the tuple
(opcode, flags, ival1_count, ival1, ival2, frame_id, frame_format, number_of_bcm_frames)
"""
opcode, flags, ival1_count, ival1_seconds, ival1_useconds, ival2_seconds, ival2_useconds, \
frame_id_std_ext, number_of_bcm_frames = struct.unpack(constants.FORMAT_BCM_HEADER, header)
ival1 = (ival1_seconds + ival1_useconds / constants.MICROSECONDS_PER_SECOND) * constants.MILLISECONDS_PER_SECOND
ival2 = (ival2_seconds + ival2_useconds / constants.MICROSECONDS_PER_SECOND) * constants.MILLISECONDS_PER_SECOND
frame_id = frame_id_std_ext & constants.CAN_MASK_ID_ONLY
frame_format = constants.CAN_FRAMEFORMAT_EXTENDED if frame_id_std_ext & constants.CAN_MASK_EXTENDED_FRAME_BIT \
else constants.CAN_FRAMEFORMAT_STANDARD
return opcode, flags, ival1_count, ival1, ival2, frame_id, frame_format, number_of_bcm_frames