Skip to content

Feature: API for automatic configuration detection #278

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

Merged
merged 28 commits into from
May 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
088423d
factored out some common import functionality
felixdivo Mar 2, 2018
828b10f
cleaning up interface.py
felixdivo Mar 6, 2018
405d569
make class Bus in interface.py extend BusABC (IDEs can now see the at…
felixdivo Mar 6, 2018
c60b065
added static BusABC._detect_available_configs()
felixdivo Mar 9, 2018
f093fca
added public detect_available_channels() method
felixdivo Mar 9, 2018
005a26d
added _detect_available_configs() to virtual bus and added locks arou…
felixdivo Mar 9, 2018
91a50a0
fixes for channel detection
felixdivo Mar 9, 2018
3ebdfec
various fixes for channel detection
felixdivo Mar 10, 2018
c9c4b30
added unit tests for detect_available_configs()
felixdivo Mar 10, 2018
17dd758
small fixes for Python 2/3 compatibility
felixdivo Mar 10, 2018
b66a8d4
added channel detection for socketcan
felixdivo Apr 27, 2018
861656b
Merge branch 'develop' into feature-channel-detection
felixdivo Apr 27, 2018
34e3c52
added tests for socketcan channel detection
felixdivo Apr 27, 2018
f0c927f
catch ImportErrors in detect_available_configs()
felixdivo Apr 27, 2018
f3b7cfc
fix loading of socketcan's special case
felixdivo Apr 27, 2018
c31770e
various smaller fixes
felixdivo Apr 27, 2018
f0f8ae8
added link to helper method in socketcan bus classes
felixdivo Apr 27, 2018
3f51084
added some tests and fixed calls to helper
felixdivo Apr 27, 2018
de38513
windows fix
felixdivo Apr 28, 2018
bdf6a30
skip socketcan test on windwos
felixdivo Apr 28, 2018
81c8722
Win + Python 2 fix
felixdivo Apr 28, 2018
69404ba
use different command call in socketcan's find_available_interfaces()
felixdivo Apr 28, 2018
886bc9f
debugging commit
felixdivo Apr 28, 2018
e8c4407
debugging commit
felixdivo Apr 28, 2018
6732fbb
corrected index in command parsing
felixdivo Apr 29, 2018
00f73ae
cleanups
felixdivo Apr 29, 2018
5bf6f03
renamed "search_only_in" to "interfaces"
felixdivo May 1, 2018
e0212ea
fix method signature in unit test
felixdivo May 1, 2018
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
2 changes: 1 addition & 1 deletion can/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class CanError(IOError):
from can.notifier import Notifier
from can.interfaces import VALID_INTERFACES
from . import interface
from .interface import Bus
from .interface import Bus, detect_available_configs

from can.broadcastmanager import send_periodic, \
CyclicSendTaskABC, \
Expand Down
20 changes: 20 additions & 0 deletions can/bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ class BusABC(object):

As well as setting the `channel_info` attribute to a string describing the
interface.

They may implement :meth:`~can.BusABC._detect_available_configs` to allow
the interface to report which configurations are currently available for
new connections.

"""

#: a string describing the underlying bus channel
Expand Down Expand Up @@ -146,4 +151,19 @@ def shutdown(self):
"""
self.flush_tx_buffer()

@staticmethod
def _detect_available_configs():
"""Detect all configurations/channels that this interface could
currently connect with.

This might be quite time consuming.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you consider passing a timeout value or abort event?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not really, since that would be comparably difficult to implement and is not necessary for the two interfaces I implemented it for. (socketcan & virtual) Do you really think we need it?


May not to be implemented by every interface on every platform.

:rtype: Iterator[dict]
:return: an iterable of dicts, each being a configuration suitable
for usage in the interface's bus constructor.
"""
raise NotImplementedError()

__metaclass__ = ABCMeta
173 changes: 132 additions & 41 deletions can/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,91 @@

from __future__ import absolute_import

import can
import sys
import importlib

from can.broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC
from pkg_resources import iter_entry_points
from can.util import load_config
import logging

import can
from .bus import BusABC
from .broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC
from .util import load_config

if sys.version_info.major > 2:
basestring = str


log = logging.getLogger('can.interface')
log_autodetect = log.getChild('detect_available_configs')

# interface_name => (module, classname)
BACKENDS = {
'kvaser': ('can.interfaces.kvaser', 'KvaserBus'),
'socketcan_ctypes': ('can.interfaces.socketcan', 'SocketcanCtypes_Bus'),
'socketcan_native': ('can.interfaces.socketcan', 'SocketcanNative_Bus'),
'serial': ('can.interfaces.serial.serial_can', 'SerialBus'),
'pcan': ('can.interfaces.pcan', 'PcanBus'),
'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'),
'ixxat': ('can.interfaces.ixxat', 'IXXATBus'),
'nican': ('can.interfaces.nican', 'NicanBus'),
'iscan': ('can.interfaces.iscan', 'IscanBus'),
'virtual': ('can.interfaces.virtual', 'VirtualBus'),
'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'),
'vector': ('can.interfaces.vector', 'VectorBus'),
'slcan': ('can.interfaces.slcan', 'slcanBus')
'kvaser': ('can.interfaces.kvaser', 'KvaserBus'),
'socketcan_ctypes': ('can.interfaces.socketcan', 'SocketcanCtypes_Bus'),
'socketcan_native': ('can.interfaces.socketcan', 'SocketcanNative_Bus'),
'serial': ('can.interfaces.serial.serial_can','SerialBus'),
'pcan': ('can.interfaces.pcan', 'PcanBus'),
'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'),
'ixxat': ('can.interfaces.ixxat', 'IXXATBus'),
'nican': ('can.interfaces.nican', 'NicanBus'),
'iscan': ('can.interfaces.iscan', 'IscanBus'),
'virtual': ('can.interfaces.virtual', 'VirtualBus'),
'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'),
'vector': ('can.interfaces.vector', 'VectorBus'),
'slcan': ('can.interfaces.slcan', 'slcanBus')
}


BACKENDS.update({
interface.name: (interface.module_name, interface.attrs[0])
for interface in iter_entry_points('python_can.interface')
})


class Bus(object):
def _get_class_for_interface(interface):
"""
Returns the main bus class for the given interface.

:raises:
NotImplementedError if the interface is not known
:raises:
ImportError if there was a problem while importing the
interface or the bus class within that
"""

# filter out the socketcan special case
if interface == 'socketcan':
try:
interface = can.util.choose_socketcan_implementation()
except Exception as e:
raise ImportError("Cannot choose socketcan implementation: {}".format(e))

# Find the correct backend
try:
module_name, class_name = BACKENDS[interface]
except KeyError:
raise NotImplementedError("CAN interface '{}' not supported".format(interface))

# Import the correct interface module
try:
module = importlib.import_module(module_name)
except Exception as e:
raise ImportError(
"Cannot import module {} for CAN interface '{}': {}".format(module_name, interface, e)
)

# Get the correct class
try:
bus_class = getattr(module, class_name)
except Exception as e:
raise ImportError(
"Cannot import class {} from module {} for CAN interface '{}': {}"
.format(class_name, module_name, interface, e)
)

return bus_class


class Bus(BusABC):
"""
Instantiates a CAN Bus of the given `bustype`, falls back to reading a
configuration file from default locations.
Expand All @@ -61,39 +114,77 @@ def __new__(cls, other, channel=None, *args, **kwargs):
or set in the can.rc config.

"""

# Figure out the configuration
config = load_config(config={
'interface': kwargs.get('bustype'),
'interface': kwargs.get('bustype', kwargs.get('interface')),
'channel': channel
})

# remove the bustype & interface so it doesn't get passed to the backend
if 'bustype' in kwargs:
# remove the bustype so it doesn't get passed to the backend
del kwargs['bustype']
interface = config['interface']
channel = config['channel']
if 'interface' in kwargs:
del kwargs['interface']

# Import the correct Bus backend
try:
(module_name, class_name) = BACKENDS[interface]
except KeyError:
raise NotImplementedError("CAN interface '{}' not supported".format(interface))
cls = _get_class_for_interface(config['interface'])
return cls(channel=config['channel'], *args, **kwargs)


def detect_available_configs(interfaces=None):
"""Detect all configurations/channels that the interfaces could
currently connect with.

This might be quite time consuming.

Automated configuration detection may not be implemented by
every interface on every platform. This method will not raise
an error in that case, but with rather return an empty list
for that interface.

:param interfaces: either
- the name of an interface to be searched in as a string,
- an iterable of interface names to search in, or
- `None` to search in all known interfaces.
:rtype: list of `dict`s
:return: an iterable of dicts, each suitable for usage in
:class:`can.interface.Bus`'s constructor.
"""

# Figure out where to search
if interfaces is None:
# use an iterator over the keys so we do not have to copy it
interfaces = BACKENDS.keys()
elif isinstance(interfaces, basestring):
interfaces = [interfaces, ]
# else it is supposed to be an iterable of strings

result = []
for interface in interfaces:

try:
module = importlib.import_module(module_name)
except Exception as e:
raise ImportError(
"Cannot import module {} for CAN interface '{}': {}".format(module_name, interface, e)
)
bus_class = _get_class_for_interface(interface)
except ImportError:
log_autodetect.debug('interface "%s" can not be loaded for detection of available configurations', interface)
continue

# get available channels
try:
cls = getattr(module, class_name)
except Exception as e:
raise ImportError(
"Cannot import class {} from module {} for CAN interface '{}': {}".format(
class_name, module_name, interface, e
)
)
available = list(bus_class._detect_available_configs())
except NotImplementedError:
log_autodetect.debug('interface "%s" does not support detection of available configurations', interface)
else:
log_autodetect.debug('interface "%s" detected %i available configurations', interface, len(available))

# add the interface name to the configs if it is not already present
for config in available:
if 'interface' not in config:
config['interface'] = interface

# append to result
result += available

return cls(channel, **kwargs)
return result


class CyclicSendTask(CyclicSendTaskABC):
Expand Down
1 change: 1 addition & 0 deletions can/interfaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from pkg_resources import iter_entry_points

# TODO: isn't this a unnecessary information duplicate of `can/interface.py :: BACKENDS`?
VALID_INTERFACES = set(['kvaser', 'serial', 'pcan', 'socketcan_native',
'socketcan_ctypes', 'socketcan', 'usb2can', 'ixxat',
'nican', 'iscan', 'vector', 'virtual', 'neovi',
Expand Down
1 change: 1 addition & 0 deletions can/interfaces/socketcan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# coding: utf-8

"""
See: https://www.kernel.org/doc/Documentation/networking/can.txt
"""

from can.interfaces.socketcan import socketcan_constants as constants
Expand Down
35 changes: 35 additions & 0 deletions can/interfaces/socketcan/socketcan_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@
Defines common socketcan functions.
"""

import logging
import os
import errno
import struct
import sys
if sys.version_info[0] < 3 and os.name == 'posix':
import subprocess32 as subprocess
else:
import subprocess
import re

from can.interfaces.socketcan.socketcan_constants import CAN_EFF_FLAG

log = logging.getLogger('can.socketcan_common')

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


_PATTERN_CAN_INTERFACE = re.compile(r"v?can\d+")

def find_available_interfaces():
"""Returns the names of all open can/vcan interfaces using
the ``ip link list`` command. If the lookup fails, an error
is logged to the console and an empty list is returned.

:rtype: an iterable of :class:`str`
"""

try:
# it might be good to add "type vcan", but that might (?) exclude physical can devices
command = ["ip", "-o", "link", "list", "up"]
output = subprocess.check_output(command, universal_newlines=True)

except Exception as e: # subprocess.CalledProcessError was too specific
log.error("failed to fetch opened can devices: %s", e)
return []

else:
#log.debug("find_available_interfaces(): output=\n%s", output)
# output contains some lines like "1: vcan42: <NOARP,UP,LOWER_UP> ..."
# extract the "vcan42" of each line
interface_names = [line.split(": ", 3)[1] for line in output.splitlines()]
log.debug("find_available_interfaces(): detected: %s", interface_names)
return filter(_PATTERN_CAN_INTERFACE.match, interface_names)

def error_code_to_str(code):
"""
Converts a given error code (errno) to a useful and human readable string.
Expand Down
8 changes: 7 additions & 1 deletion can/interfaces/socketcan/socketcan_ctypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
from can.bus import BusABC
from can.message import Message
from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW
from can.interfaces.socketcan.socketcan_common import *
from can.interfaces.socketcan.socketcan_common import \
pack_filters, find_available_interfaces, error_code_to_str

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

return task

@staticmethod
def _detect_available_configs():
return [{'interface': 'socketcan_ctypes', 'channel': channel}
for channel in find_available_interfaces()]


class SOCKADDR(ctypes.Structure):
# See /usr/include/i386-linux-gnu/bits/socket.h for original struct
Expand Down
19 changes: 13 additions & 6 deletions can/interfaces/socketcan/socketcan_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

"""
This implementation is for versions of Python that have native
can socket and can bcm socket support: >=3.5
can socket and can bcm socket support.

See :meth:`can.util.choose_socketcan_implementation()`.
"""

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

import can

from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW, CAN_*_FLAG
from can.interfaces.socketcan.socketcan_common import *
from can import Message, BusABC

from can.broadcastmanager import ModifiableCyclicTaskABC, RestartableCyclicTaskABC, LimitedDurationCyclicSendTaskABC
from can.broadcastmanager import ModifiableCyclicTaskABC, \
RestartableCyclicTaskABC, LimitedDurationCyclicSendTaskABC
from can.interfaces.socketcan.socketcan_constants import * # CAN_RAW, CAN_*_FLAG
from can.interfaces.socketcan.socketcan_common import \
pack_filters, find_available_interfaces, error_code_to_str

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

@staticmethod
def _detect_available_configs():
return [{'interface': 'socketcan_native', 'channel': channel}
for channel in find_available_interfaces()]


if __name__ == "__main__":
# Create two sockets on vcan0 to test send and receive
Expand Down
Loading