Skip to content

Commit ddfac97

Browse files
authored
Feature: API for automatic configuration detection (#278)
* factored out some common import functionality * cleaned up interface.py * make class Bus in interface.py extend BusABC (IDEs can now see the attributes) * added static BusABC._detect_available_configs() * added public detect_available_channels() method * added _detect_available_configs() to virtual bus * added locks around the global channels variable in virtual bus * added unit tests for detect_available_configs() * added channel detection for socketcan * added tests for socketcan channel detection * catch ImportErrors in detect_available_configs() * skip socketcan test on windwos * various cleanups
1 parent b97a502 commit ddfac97

File tree

12 files changed

+347
-68
lines changed

12 files changed

+347
-68
lines changed

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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ class BusABC(object):
2525
2626
As well as setting the `channel_info` attribute to a string describing the
2727
interface.
28+
29+
They may implement :meth:`~can.BusABC._detect_available_configs` to allow
30+
the interface to report which configurations are currently available for
31+
new connections.
32+
2833
"""
2934

3035
#: a string describing the underlying bus channel
@@ -146,4 +151,19 @@ def shutdown(self):
146151
"""
147152
self.flush_tx_buffer()
148153

154+
@staticmethod
155+
def _detect_available_configs():
156+
"""Detect all configurations/channels that this interface could
157+
currently connect with.
158+
159+
This might be quite time consuming.
160+
161+
May not to be implemented by every interface on every platform.
162+
163+
:rtype: Iterator[dict]
164+
:return: an iterable of dicts, each being a configuration suitable
165+
for usage in the interface's bus constructor.
166+
"""
167+
raise NotImplementedError()
168+
149169
__metaclass__ = ABCMeta

can/interface.py

Lines changed: 132 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,91 @@
99

1010
from __future__ import absolute_import
1111

12-
import can
12+
import sys
1313
import importlib
14-
15-
from can.broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC
1614
from pkg_resources import iter_entry_points
17-
from can.util import load_config
15+
import logging
16+
17+
import can
18+
from .bus import BusABC
19+
from .broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC
20+
from .util import load_config
21+
22+
if sys.version_info.major > 2:
23+
basestring = str
24+
25+
26+
log = logging.getLogger('can.interface')
27+
log_autodetect = log.getChild('detect_available_configs')
1828

1929
# interface_name => (module, classname)
2030
BACKENDS = {
21-
'kvaser': ('can.interfaces.kvaser', 'KvaserBus'),
22-
'socketcan_ctypes': ('can.interfaces.socketcan', 'SocketcanCtypes_Bus'),
23-
'socketcan_native': ('can.interfaces.socketcan', 'SocketcanNative_Bus'),
24-
'serial': ('can.interfaces.serial.serial_can', 'SerialBus'),
25-
'pcan': ('can.interfaces.pcan', 'PcanBus'),
26-
'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'),
27-
'ixxat': ('can.interfaces.ixxat', 'IXXATBus'),
28-
'nican': ('can.interfaces.nican', 'NicanBus'),
29-
'iscan': ('can.interfaces.iscan', 'IscanBus'),
30-
'virtual': ('can.interfaces.virtual', 'VirtualBus'),
31-
'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'),
32-
'vector': ('can.interfaces.vector', 'VectorBus'),
33-
'slcan': ('can.interfaces.slcan', 'slcanBus')
31+
'kvaser': ('can.interfaces.kvaser', 'KvaserBus'),
32+
'socketcan_ctypes': ('can.interfaces.socketcan', 'SocketcanCtypes_Bus'),
33+
'socketcan_native': ('can.interfaces.socketcan', 'SocketcanNative_Bus'),
34+
'serial': ('can.interfaces.serial.serial_can','SerialBus'),
35+
'pcan': ('can.interfaces.pcan', 'PcanBus'),
36+
'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'),
37+
'ixxat': ('can.interfaces.ixxat', 'IXXATBus'),
38+
'nican': ('can.interfaces.nican', 'NicanBus'),
39+
'iscan': ('can.interfaces.iscan', 'IscanBus'),
40+
'virtual': ('can.interfaces.virtual', 'VirtualBus'),
41+
'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'),
42+
'vector': ('can.interfaces.vector', 'VectorBus'),
43+
'slcan': ('can.interfaces.slcan', 'slcanBus')
3444
}
3545

36-
3746
BACKENDS.update({
3847
interface.name: (interface.module_name, interface.attrs[0])
3948
for interface in iter_entry_points('python_can.interface')
4049
})
4150

4251

43-
class Bus(object):
52+
def _get_class_for_interface(interface):
53+
"""
54+
Returns the main bus class for the given interface.
55+
56+
:raises:
57+
NotImplementedError if the interface is not known
58+
:raises:
59+
ImportError if there was a problem while importing the
60+
interface or the bus class within that
61+
"""
62+
63+
# filter out the socketcan special case
64+
if interface == 'socketcan':
65+
try:
66+
interface = can.util.choose_socketcan_implementation()
67+
except Exception as e:
68+
raise ImportError("Cannot choose socketcan implementation: {}".format(e))
69+
70+
# Find the correct backend
71+
try:
72+
module_name, class_name = BACKENDS[interface]
73+
except KeyError:
74+
raise NotImplementedError("CAN interface '{}' not supported".format(interface))
75+
76+
# Import the correct interface module
77+
try:
78+
module = importlib.import_module(module_name)
79+
except Exception as e:
80+
raise ImportError(
81+
"Cannot import module {} for CAN interface '{}': {}".format(module_name, interface, e)
82+
)
83+
84+
# Get the correct class
85+
try:
86+
bus_class = getattr(module, class_name)
87+
except Exception as e:
88+
raise ImportError(
89+
"Cannot import class {} from module {} for CAN interface '{}': {}"
90+
.format(class_name, module_name, interface, e)
91+
)
92+
93+
return bus_class
94+
95+
96+
class Bus(BusABC):
4497
"""
4598
Instantiates a CAN Bus of the given `bustype`, falls back to reading a
4699
configuration file from default locations.
@@ -61,39 +114,77 @@ def __new__(cls, other, channel=None, *args, **kwargs):
61114
or set in the can.rc config.
62115
63116
"""
117+
118+
# Figure out the configuration
64119
config = load_config(config={
65-
'interface': kwargs.get('bustype'),
120+
'interface': kwargs.get('bustype', kwargs.get('interface')),
66121
'channel': channel
67122
})
68123

124+
# remove the bustype & interface so it doesn't get passed to the backend
69125
if 'bustype' in kwargs:
70-
# remove the bustype so it doesn't get passed to the backend
71126
del kwargs['bustype']
72-
interface = config['interface']
73-
channel = config['channel']
127+
if 'interface' in kwargs:
128+
del kwargs['interface']
74129

75-
# Import the correct Bus backend
76-
try:
77-
(module_name, class_name) = BACKENDS[interface]
78-
except KeyError:
79-
raise NotImplementedError("CAN interface '{}' not supported".format(interface))
130+
cls = _get_class_for_interface(config['interface'])
131+
return cls(channel=config['channel'], *args, **kwargs)
132+
133+
134+
def detect_available_configs(interfaces=None):
135+
"""Detect all configurations/channels that the interfaces could
136+
currently connect with.
137+
138+
This might be quite time consuming.
139+
140+
Automated configuration detection may not be implemented by
141+
every interface on every platform. This method will not raise
142+
an error in that case, but with rather return an empty list
143+
for that interface.
144+
145+
:param interfaces: either
146+
- the name of an interface to be searched in as a string,
147+
- an iterable of interface names to search in, or
148+
- `None` to search in all known interfaces.
149+
:rtype: list of `dict`s
150+
:return: an iterable of dicts, each suitable for usage in
151+
:class:`can.interface.Bus`'s constructor.
152+
"""
153+
154+
# Figure out where to search
155+
if interfaces is None:
156+
# use an iterator over the keys so we do not have to copy it
157+
interfaces = BACKENDS.keys()
158+
elif isinstance(interfaces, basestring):
159+
interfaces = [interfaces, ]
160+
# else it is supposed to be an iterable of strings
161+
162+
result = []
163+
for interface in interfaces:
80164

81165
try:
82-
module = importlib.import_module(module_name)
83-
except Exception as e:
84-
raise ImportError(
85-
"Cannot import module {} for CAN interface '{}': {}".format(module_name, interface, e)
86-
)
166+
bus_class = _get_class_for_interface(interface)
167+
except ImportError:
168+
log_autodetect.debug('interface "%s" can not be loaded for detection of available configurations', interface)
169+
continue
170+
171+
# get available channels
87172
try:
88-
cls = getattr(module, class_name)
89-
except Exception as e:
90-
raise ImportError(
91-
"Cannot import class {} from module {} for CAN interface '{}': {}".format(
92-
class_name, module_name, interface, e
93-
)
94-
)
173+
available = list(bus_class._detect_available_configs())
174+
except NotImplementedError:
175+
log_autodetect.debug('interface "%s" does not support detection of available configurations', interface)
176+
else:
177+
log_autodetect.debug('interface "%s" detected %i available configurations', interface, len(available))
178+
179+
# add the interface name to the configs if it is not already present
180+
for config in available:
181+
if 'interface' not in config:
182+
config['interface'] = interface
183+
184+
# append to result
185+
result += available
95186

96-
return cls(channel, **kwargs)
187+
return result
97188

98189

99190
class CyclicSendTask(CyclicSendTaskABC):

can/interfaces/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from pkg_resources import iter_entry_points
99

10+
# TODO: isn't this a unnecessary information duplicate of `can/interface.py :: BACKENDS`?
1011
VALID_INTERFACES = set(['kvaser', 'serial', 'pcan', 'socketcan_native',
1112
'socketcan_ctypes', 'socketcan', 'usb2can', 'ixxat',
1213
'nican', 'iscan', 'vector', 'virtual', 'neovi',

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

0 commit comments

Comments
 (0)