Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: qxc2016/python-can
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: develop
Choose a base ref
...
head repository: qxc2016/python-can
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: feature-channel-detection
Choose a head ref
  • 10 commits
  • 6 files changed
  • 1 contributor

Commits on Mar 2, 2018

  1. Copy the full SHA
    088423d View commit details

Commits on Mar 6, 2018

  1. cleaning up interface.py

    felixdivo committed Mar 6, 2018
    Copy the full SHA
    828b10f View commit details
  2. Copy the full SHA
    405d569 View commit details

Commits on Mar 9, 2018

  1. Copy the full SHA
    c60b065 View commit details
  2. Copy the full SHA
    f093fca View commit details
  3. added _detect_available_configs() to virtual bus and added locks arou…

    …nd the global channels variable
    felixdivo committed Mar 9, 2018
    Copy the full SHA
    005a26d View commit details
  4. Copy the full SHA
    91a50a0 View commit details

Commits on Mar 10, 2018

  1. Copy the full SHA
    3ebdfec View commit details
  2. Copy the full SHA
    c9c4b30 View commit details
  3. Copy the full SHA
    17dd758 View commit details
Showing with 241 additions and 53 deletions.
  1. +1 −1 can/__init__.py
  2. +20 −0 can/bus.py
  3. +122 −43 can/interface.py
  4. +1 −0 can/interfaces/__init__.py
  5. +56 −9 can/interfaces/virtual.py
  6. +41 −0 test/test_detect_available_configs.py
2 changes: 1 addition & 1 deletion can/__init__.py
Original file line number Diff line number Diff line change
@@ -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, \
20 changes: 20 additions & 0 deletions can/bus.py
Original file line number Diff line number Diff line change
@@ -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
@@ -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.
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
165 changes: 122 additions & 43 deletions can/interface.py
Original file line number Diff line number Diff line change
@@ -9,38 +9,84 @@

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')


# 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
"""

# 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.
@@ -61,39 +107,72 @@ 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(search_only_in=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.
:param search_only_in: 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: Iterator[dict]
:return: an iterable of dicts, each suitable for usage in
:class:`~can.interface.Bus`'s constructor.
"""
logger = log.getChild('detect_available_configs')

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

result = []
for interface in search_only_in:

bus_class = _get_class_for_interface(interface)

# get available channels
try:
module = importlib.import_module(module_name)
except Exception as e:
raise ImportError(
"Cannot import module {} for CAN interface '{}': {}".format(module_name, interface, e)
)
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
)
)

return cls(channel, **kwargs)
available = bus_class._detect_available_configs()
except NotImplementedError:
logger.debug('interface "%s" does not support detection of available configurations', interface)
else:
logger.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 result


class CyclicSendTask(CyclicSendTaskABC):
1 change: 1 addition & 0 deletions can/interfaces/__init__.py
Original file line number Diff line number Diff line change
@@ -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',
65 changes: 56 additions & 9 deletions can/interfaces/virtual.py
Original file line number Diff line number Diff line change
@@ -15,6 +15,8 @@
import queue
except ImportError:
import Queue as queue
from threading import RLock
import random

from can.bus import BusABC

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

# Channels are lists of queues, one for each connection
channels = {}
channels_lock = RLock()


class VirtualBus(BusABC):
"""Virtual CAN bus using an internal message queue for testing."""
"""
A virtual CAN bus using an internal message queue. It can be
used for example for testing.
In this interface, a channel is an arbitarty object used as
an identifier for connected buses.
Implements :meth:`can.BusABC._detect_available_configs`; see
:meth:`can.VirtualBus._detect_available_configs` for how it
behaves here.
"""

def __init__(self, channel=None, receive_own_messages=False, **config):
self.channel_info = 'Virtual bus channel %s' % channel
# the channel identifier may be an arbitrary object
self.channel_id = channel
self.channel_info = 'Virtual bus channel %s' % self.channel_id
self.receive_own_messages = receive_own_messages

# Create a new channel if one does not exist
if channel not in channels:
channels[channel] = []
with channels_lock:

# Create a new channel if one does not exist
if self.channel_id not in channels:
channels[self.channel_id] = []
self.channel = channels[self.channel_id]

self.queue = queue.Queue()
self.channel = channels[channel]
self.channel.append(self.queue)
self.queue = queue.Queue()
self.channel.append(self.queue)

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

def shutdown(self):
self.channel.remove(self.queue)
with channels_lock:
self.channel.remove(self.queue)

# remove if emtpy
if not self.channel:
del channels[self.channel_id]

@staticmethod
def _detect_available_configs():
"""
Returns all currently used channels as well as
one other currently unused channel.
This method will have problems if thousands of
autodetected busses are used at once.
"""
with channels_lock:
available_channels = list(channels.keys())

# find a currently unused channel
get_extra = lambda: "channel-{}".format(random.randint(0, 9999))
extra = get_extra()
while extra in available_channels:
extra = get_extra()

available_channels += [extra]

return [
{'interface': 'virtual', 'channel': channel}
for channel in available_channels
]
41 changes: 41 additions & 0 deletions test/test_detect_available_configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env python
# coding: utf-8

"""
This module tests :meth:`can.BusABC._detect_available_configs` /
:meth:`can.BusABC.detect_available_configs`.
"""

import sys
import unittest

from can import detect_available_configs

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


class TestDetectAvailableConfigs(unittest.TestCase):

def test_count_returned(self):
# At least virtual has to always return at least one interface
self.assertGreaterEqual (len(detect_available_configs() ), 1)
self.assertEquals (len(detect_available_configs(search_only_in=[]) ), 0)
self.assertGreaterEqual (len(detect_available_configs(search_only_in='virtual') ), 1)
self.assertGreaterEqual (len(detect_available_configs(search_only_in=['virtual']) ), 1)
self.assertGreaterEqual (len(detect_available_configs(search_only_in=None) ), 1)

def test_general_values(self):
returned = detect_available_configs()
for config in returned:
self.assertIn('interface', config)
self.assertIn('channel', config)
self.assertIsInstance(config['interface'], basestring)

def test_content_virtual(self):
returned = detect_available_configs(search_only_in='virtual')
for config in returned:
self.assertEqual(config['interface'], 'virtual')

if __name__ == '__main__':
unittest.main()