Skip to content

Commit 92037c4

Browse files
authored
Merge branch 'develop' into software-filtering
2 parents a973d73 + ddfac97 commit 92037c4

11 files changed

Lines changed: 292 additions & 35 deletions

can/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class CanError(IOError):
3535
from can.notifier import Notifier
3636
from can.interfaces import VALID_INTERFACES
3737
from . import interface
38-
from .interface import Bus
38+
from .interface import Bus, detect_available_configs
3939

4040
from can.broadcastmanager import send_periodic, \
4141
CyclicSendTaskABC, \

can/bus.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ class BusABC(object):
2121
"""The CAN Bus Abstract Base Class.
2222
2323
Concrete implementations *must* implement the following:
24-
* :meth:`~can.BusABC.send`
25-
* :meth:`~can.BusABC._recv_internal`
24+
* :meth:`~can.BusABC.send` to send individual messages
25+
* :meth:`~can.BusABC._recv_internal` to receive individual messages
2626
* set the :attr:`~can.BusABC.channel_info` attribute to a string describing
27-
the interface and/or channel
27+
the underlying bus and/or channel
2828
2929
The *may* implement the following:
3030
* :meth:`~can.BusABC.flush_tx_buffer` to allow discrading any
@@ -35,10 +35,13 @@ class BusABC(object):
3535
periodic sending and push it down to the kernel or hardware
3636
* :meth:`~can.BusABC._apply_filters` to apply efficient filters
3737
to lower level systems like the OS kernel or hardware
38+
* :meth:`~can.BusABC._detect_available_configs` to allow the interface
39+
to report which configurations are currently available for new
40+
connections
3841
3942
"""
4043

41-
#: a string describing the underlying bus channel
44+
#: a string describing the underlying bus and/or channel
4245
channel_info = 'unknown'
4346

4447
@abstractmethod
@@ -260,4 +263,19 @@ def shutdown(self):
260263
"""
261264
pass
262265

266+
@staticmethod
267+
def _detect_available_configs():
268+
"""Detect all configurations/channels that this interface could
269+
currently connect with.
270+
271+
This might be quite time consuming.
272+
273+
May not to be implemented by every interface on every platform.
274+
275+
:rtype: Iterator[dict]
276+
:return: an iterable of dicts, each being a configuration suitable
277+
for usage in the interface's bus constructor.
278+
"""
279+
raise NotImplementedError()
280+
263281
__metaclass__ = ABCMeta

can/interface.py

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,21 @@
99

1010
from __future__ import absolute_import
1111

12+
import sys
1213
import importlib
1314
from pkg_resources import iter_entry_points
15+
import logging
1416

1517
import can
1618
from .bus import BusABC
1719
from .broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC
1820
from .util import load_config
1921

22+
if sys.version_info.major > 2:
23+
basestring = str
24+
25+
log = logging.getLogger('can.interface')
26+
log_autodetect = log.getChild('detect_available_configs')
2027

2128
# interface_name => (module, classname)
2229
BACKENDS = {
@@ -48,10 +55,17 @@ def _get_class_for_interface(interface):
4855
:raises:
4956
NotImplementedError if the interface is not known
5057
:raises:
51-
ImportError if there was a problem while importing the
52-
interface or the bus class within that
58+
ImportError if there was a problem while importing the
59+
interface or the bus class within that
5360
"""
5461

62+
# filter out the socketcan special case
63+
if interface == 'socketcan':
64+
try:
65+
interface = can.util.choose_socketcan_implementation()
66+
except Exception as e:
67+
raise ImportError("Cannot choose socketcan implementation: {}".format(e))
68+
5569
# Find the correct backend
5670
try:
5771
module_name, class_name = BACKENDS[interface]
@@ -102,18 +116,76 @@ def __new__(cls, other, channel=None, *args, **kwargs):
102116

103117
# Figure out the configuration
104118
config = load_config(config={
105-
'interface': kwargs.get('bustype'),
119+
'interface': kwargs.get('bustype', kwargs.get('interface')),
106120
'channel': channel
107121
})
108122

109-
# remove the bustype so it doesn't get passed to the backend
123+
# remove the bustype & interface so it doesn't get passed to the backend
110124
if 'bustype' in kwargs:
111125
del kwargs['bustype']
126+
if 'interface' in kwargs:
127+
del kwargs['interface']
112128

113129
cls = _get_class_for_interface(config['interface'])
114130
return cls(channel=config['channel'], *args, **kwargs)
115131

116132

133+
def detect_available_configs(interfaces=None):
134+
"""Detect all configurations/channels that the interfaces could
135+
currently connect with.
136+
137+
This might be quite time consuming.
138+
139+
Automated configuration detection may not be implemented by
140+
every interface on every platform. This method will not raise
141+
an error in that case, but with rather return an empty list
142+
for that interface.
143+
144+
:param interfaces: either
145+
- the name of an interface to be searched in as a string,
146+
- an iterable of interface names to search in, or
147+
- `None` to search in all known interfaces.
148+
:rtype: list of `dict`s
149+
:return: an iterable of dicts, each suitable for usage in
150+
:class:`can.interface.Bus`'s constructor.
151+
"""
152+
153+
# Figure out where to search
154+
if interfaces is None:
155+
# use an iterator over the keys so we do not have to copy it
156+
interfaces = BACKENDS.keys()
157+
elif isinstance(interfaces, basestring):
158+
interfaces = [interfaces, ]
159+
# else it is supposed to be an iterable of strings
160+
161+
result = []
162+
for interface in interfaces:
163+
164+
try:
165+
bus_class = _get_class_for_interface(interface)
166+
except ImportError:
167+
log_autodetect.debug('interface "%s" can not be loaded for detection of available configurations', interface)
168+
continue
169+
170+
# get available channels
171+
try:
172+
available = list(bus_class._detect_available_configs())
173+
except NotImplementedError:
174+
log_autodetect.debug('interface "%s" does not support detection of available configurations', interface)
175+
else:
176+
log_autodetect.debug('interface "%s" detected %i available configurations', interface, len(available))
177+
178+
# add the interface name to the configs if it is not already present
179+
for config in available:
180+
if 'interface' not in config:
181+
config['interface'] = interface
182+
183+
# append to result
184+
result += available
185+
186+
return result
187+
188+
117189
class CyclicSendTask(CyclicSendTaskABC):
118190

119191
@classmethod

can/interfaces/socketcan/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# coding: utf-8
33

44
"""
5+
See: https://www.kernel.org/doc/Documentation/networking/can.txt
56
"""
67

78
from can.interfaces.socketcan import socketcan_constants as constants

can/interfaces/socketcan/socketcan_common.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@
55
Defines common socketcan functions.
66
"""
77

8+
import logging
89
import os
910
import errno
1011
import struct
12+
import sys
13+
if sys.version_info[0] < 3 and os.name == 'posix':
14+
import subprocess32 as subprocess
15+
else:
16+
import subprocess
17+
import re
1118

1219
from can.interfaces.socketcan.socketcan_constants import CAN_EFF_FLAG
1320

21+
log = logging.getLogger('can.socketcan_common')
1422

1523
def pack_filters(can_filters=None):
1624
if can_filters is None:
@@ -36,6 +44,33 @@ def pack_filters(can_filters=None):
3644
return struct.pack(can_filter_fmt, *filter_data)
3745

3846

47+
_PATTERN_CAN_INTERFACE = re.compile(r"v?can\d+")
48+
49+
def find_available_interfaces():
50+
"""Returns the names of all open can/vcan interfaces using
51+
the ``ip link list`` command. If the lookup fails, an error
52+
is logged to the console and an empty list is returned.
53+
54+
:rtype: an iterable of :class:`str`
55+
"""
56+
57+
try:
58+
# it might be good to add "type vcan", but that might (?) exclude physical can devices
59+
command = ["ip", "-o", "link", "list", "up"]
60+
output = subprocess.check_output(command, universal_newlines=True)
61+
62+
except Exception as e: # subprocess.CalledProcessError was too specific
63+
log.error("failed to fetch opened can devices: %s", e)
64+
return []
65+
66+
else:
67+
#log.debug("find_available_interfaces(): output=\n%s", output)
68+
# output contains some lines like "1: vcan42: <NOARP,UP,LOWER_UP> ..."
69+
# extract the "vcan42" of each line
70+
interface_names = [line.split(": ", 3)[1] for line in output.splitlines()]
71+
log.debug("find_available_interfaces(): detected: %s", interface_names)
72+
return filter(_PATTERN_CAN_INTERFACE.match, interface_names)
73+
3974
def error_code_to_str(code):
4075
"""
4176
Converts a given error code (errno) to a useful and human readable string.

can/interfaces/socketcan/socketcan_ctypes.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
from can.bus import BusABC
1919
from can.message import Message
2020
from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW
21-
from can.interfaces.socketcan.socketcan_common import *
21+
from can.interfaces.socketcan.socketcan_common import \
22+
pack_filters, find_available_interfaces, error_code_to_str
2223

2324
# Set up logging
2425
log = logging.getLogger('can.socketcan.ctypes')
@@ -164,6 +165,11 @@ def send_periodic(self, msg, period, duration=None):
164165

165166
return task
166167

168+
@staticmethod
169+
def _detect_available_configs():
170+
return [{'interface': 'socketcan_ctypes', 'channel': channel}
171+
for channel in find_available_interfaces()]
172+
167173

168174
class SOCKADDR(ctypes.Structure):
169175
# See /usr/include/i386-linux-gnu/bits/socket.h for original struct

can/interfaces/socketcan/socketcan_native.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
"""
55
This implementation is for versions of Python that have native
6-
can socket and can bcm socket support: >=3.5
6+
can socket and can bcm socket support.
7+
8+
See :meth:`can.util.choose_socketcan_implementation()`.
79
"""
810

911
import logging
@@ -32,12 +34,12 @@
3234
log.error("CAN_* properties not found in socket module. These are required to use native socketcan")
3335

3436
import can
35-
36-
from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW, CAN_*_FLAG
37-
from can.interfaces.socketcan.socketcan_common import *
3837
from can import Message, BusABC
39-
40-
from can.broadcastmanager import ModifiableCyclicTaskABC, RestartableCyclicTaskABC, LimitedDurationCyclicSendTaskABC
38+
from can.broadcastmanager import ModifiableCyclicTaskABC, \
39+
RestartableCyclicTaskABC, LimitedDurationCyclicSendTaskABC
40+
from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW, CAN_*_FLAG
41+
from can.interfaces.socketcan.socketcan_common import \
42+
pack_filters, find_available_interfaces, error_code_to_str
4143

4244
# struct module defines a binary packing format:
4345
# https://docs.python.org/3/library/struct.html#struct-format-strings
@@ -492,6 +494,11 @@ def set_filters(self, can_filters=None):
492494
socket.CAN_RAW_FILTER,
493495
filter_struct)
494496

497+
@staticmethod
498+
def _detect_available_configs():
499+
return [{'interface': 'socketcan_native', 'channel': channel}
500+
for channel in find_available_interfaces()]
501+
495502

496503
if __name__ == "__main__":
497504
# Create two sockets on vcan0 to test send and receive

can/interfaces/virtual.py

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import queue
1616
except ImportError:
1717
import Queue as queue
18+
from threading import RLock
19+
import random
1820

1921
from can.bus import BusABC
2022

@@ -23,22 +25,37 @@
2325

2426
# Channels are lists of queues, one for each connection
2527
channels = {}
28+
channels_lock = RLock()
2629

2730

2831
class VirtualBus(BusABC):
29-
"""Virtual CAN bus using an internal message queue for testing."""
32+
"""
33+
A virtual CAN bus using an internal message queue. It can be
34+
used for example for testing.
35+
36+
In this interface, a channel is an arbitarty object used as
37+
an identifier for connected buses.
38+
39+
Implements :meth:`can.BusABC._detect_available_configs`; see
40+
:meth:`can.VirtualBus._detect_available_configs` for how it
41+
behaves here.
42+
"""
3043

3144
def __init__(self, channel=None, receive_own_messages=False, **config):
32-
self.channel_info = 'Virtual bus channel %s' % channel
45+
# the channel identifier may be an arbitrary object
46+
self.channel_id = channel
47+
self.channel_info = 'Virtual bus channel %s' % self.channel_id
3348
self.receive_own_messages = receive_own_messages
3449

35-
# Create a new channel if one does not exist
36-
if channel not in channels:
37-
channels[channel] = []
50+
with channels_lock:
51+
52+
# Create a new channel if one does not exist
53+
if self.channel_id not in channels:
54+
channels[self.channel_id] = []
55+
self.channel = channels[self.channel_id]
3856

39-
self.queue = queue.Queue()
40-
self.channel = channels[channel]
41-
self.channel.append(self.queue)
57+
self.queue = queue.Queue()
58+
self.channel.append(self.queue)
4259

4360
def recv(self, timeout=None):
4461
try:
@@ -58,4 +75,34 @@ def send(self, msg, timeout=None):
5875
#logger.log(9, 'Transmitted message:\n%s', msg)
5976

6077
def shutdown(self):
61-
self.channel.remove(self.queue)
78+
with channels_lock:
79+
self.channel.remove(self.queue)
80+
81+
# remove if emtpy
82+
if not self.channel:
83+
del channels[self.channel_id]
84+
85+
@staticmethod
86+
def _detect_available_configs():
87+
"""
88+
Returns all currently used channels as well as
89+
one other currently unused channel.
90+
91+
This method will have problems if thousands of
92+
autodetected busses are used at once.
93+
"""
94+
with channels_lock:
95+
available_channels = list(channels.keys())
96+
97+
# find a currently unused channel
98+
get_extra = lambda: "channel-{}".format(random.randint(0, 9999))
99+
extra = get_extra()
100+
while extra in available_channels:
101+
extra = get_extra()
102+
103+
available_channels += [extra]
104+
105+
return [
106+
{'interface': 'virtual', 'channel': channel}
107+
for channel in available_channels
108+
]

0 commit comments

Comments
 (0)