Skip to content

Commit 6dca2e1

Browse files
committed
Merge master into feature-asyncio
2 parents fd3be01 + 6bc90a8 commit 6dca2e1

35 files changed

+1515
-764
lines changed

.github/workflows/pythonpackage.yml

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@ name: Python package
55

66
on:
77
push:
8-
branches: [ "master" ]
8+
branches:
9+
- 'master'
10+
paths-ignore:
11+
- 'README.rst'
12+
- 'LICENSE.txt'
913
pull_request:
10-
branches: [ "master" ]
14+
branches:
15+
- 'master'
16+
paths-ignore:
17+
- 'README.rst'
18+
- 'LICENSE.txt'
1119

1220
jobs:
1321
build:
@@ -17,22 +25,39 @@ jobs:
1725
fail-fast: false
1826
matrix:
1927
python-version: ['3.x']
28+
features: ['', '[db_export]']
2029

2130
steps:
22-
- uses: actions/checkout@v3
31+
- uses: actions/checkout@v4
2332
- name: Set up Python ${{ matrix.python-version }}
24-
uses: actions/setup-python@v3
33+
uses: actions/setup-python@v5
2534
with:
2635
python-version: ${{ matrix.python-version }}
36+
cache: 'pip'
37+
cache-dependency-path: |
38+
'pyproject.toml'
39+
'requirements-dev.txt'
2740
- name: Install dependencies
28-
run: |
29-
python -m pip install --upgrade pip
30-
pip install pytest pytest-cov
31-
pip install -e .
41+
run: python3 -m pip install -e '.${{ matrix.features }}' -r requirements-dev.txt
3242
- name: Test with pytest
33-
run: |
34-
pytest -v --cov=canopen --cov-report=xml --cov-branch
43+
run: pytest -v --cov=canopen --cov-report=xml --cov-branch
3544
- name: Upload coverage reports to Codecov
3645
uses: codecov/codecov-action@v4
3746
with:
3847
token: ${{ secrets.CODECOV_TOKEN }}
48+
49+
docs:
50+
runs-on: ubuntu-latest
51+
steps:
52+
- uses: actions/checkout@v4
53+
- uses: actions/setup-python@v5
54+
with:
55+
python-version: 3.12
56+
cache: 'pip'
57+
cache-dependency-path: |
58+
'pyproject.toml'
59+
'doc/requirements.txt'
60+
- name: Install dependencies
61+
run: python3 -m pip install -r doc/requirements.txt -e .
62+
- name: Build docs
63+
run: make -C doc html

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ coverage.xml
5858
*.log
5959

6060
# Sphinx documentation
61-
docs/_build/
61+
doc/_build/
6262

6363
# PyBuilder
6464
target/

README.rst

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ The aim of the project is to support the most common parts of the CiA 301
66
standard in a simple Pythonic interface. It is mainly targeted for testing and
77
automation tasks rather than a standard compliant master implementation.
88

9-
The library supports Python 3.8+.
9+
The library supports Python 3.8 or newer.
1010

1111
This library is the asyncio port of CANopen. See below for code example.
1212

@@ -60,11 +60,11 @@ Incomplete support for creating slave nodes also exists.
6060
Installation
6161
------------
6262

63-
Install from PyPI_ using pip::
63+
Install from PyPI_ using :program:`pip`::
6464

6565
$ pip install canopen
6666

67-
Install from latest master on GitHub::
67+
Install from latest ``master`` on GitHub::
6868

6969
$ pip install https://github.com/christiansandberg/canopen/archive/master.zip
7070

@@ -77,9 +77,13 @@ it in `develop mode`_::
7777

7878
Unit tests can be run using the pytest_ framework::
7979

80-
$ pip install pytest
80+
$ pip install -r requirements-dev.txt
8181
$ pytest -v
8282

83+
You can also use :mod:`unittest` standard library module::
84+
85+
$ python3 -m unittest discover test -v
86+
8387
Documentation
8488
-------------
8589

@@ -89,7 +93,8 @@ http://canopen.readthedocs.io/en/latest/
8993

9094
It can also be generated from a local clone using Sphinx_::
9195

92-
$ python setup.py build_sphinx
96+
$ pip install -r doc/requirements.txt
97+
$ make -C doc html
9398

9499

95100
Hardware support
@@ -131,12 +136,12 @@ The :code:`n` is the PDO index (normally 1 to 4). The second form of access is f
131136
# Arguments are passed to python-can's can.Bus() constructor
132137
# (see https://python-can.readthedocs.io/en/latest/bus.html).
133138
network.connect()
134-
# network.connect(bustype='socketcan', channel='can0')
135-
# network.connect(bustype='kvaser', channel=0, bitrate=250000)
136-
# network.connect(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000)
137-
# network.connect(bustype='ixxat', channel=0, bitrate=250000)
138-
# network.connect(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000)
139-
# network.connect(bustype='nican', channel='CAN0', bitrate=250000)
139+
# network.connect(interface='socketcan', channel='can0')
140+
# network.connect(interface='kvaser', channel=0, bitrate=250000)
141+
# network.connect(interface='pcan', channel='PCAN_USBBUS1', bitrate=250000)
142+
# network.connect(interface='ixxat', channel=0, bitrate=250000)
143+
# network.connect(interface='vector', app_name='CANalyzer', channel=0, bitrate=250000)
144+
# network.connect(interface='nican', channel='CAN0', bitrate=250000)
140145
141146
# Read a variable using SDO
142147
device_name = node.sdo['Manufacturer device name'].raw

canopen/__init__.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@
99
# package is not installed
1010
__version__ = "unknown"
1111

12-
Node = RemoteNode
13-
12+
__all__ = [
13+
"Network",
14+
"NodeScanner",
15+
"RemoteNode",
16+
"LocalNode",
17+
"SdoCommunicationError",
18+
"SdoAbortedError",
19+
"import_od",
20+
"export_od",
21+
"ObjectDictionary",
22+
"ObjectDictionaryError",
23+
"BaseNode402",
24+
]
1425
__pypi_url__ = "https://pypi.org/project/canopen/"
26+
27+
Node = RemoteNode

canopen/network.py

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,9 @@
99
from can import BusABC, Notifier
1010
from asyncio import AbstractEventLoop
1111

12-
try:
13-
import can
14-
from can import Listener
15-
from can import CanError
16-
except ImportError:
17-
# Do not fail if python-can is not installed
18-
can = None
19-
CanError = Exception
20-
class Listener:
21-
""" Dummy listener """
12+
import can
13+
from can import Listener
14+
from can import CanError
2215

2316
from canopen.node import RemoteNode, LocalNode
2417
from canopen.sync import SyncProducer
@@ -37,11 +30,10 @@ class Listener:
3730
class Network(MutableMapping):
3831
"""Representation of one CAN bus containing one or more nodes."""
3932

40-
def __init__(
41-
self,
42-
bus: Optional[BusABC] = None,
43-
loop: Optional[AbstractEventLoop] = None
44-
):
33+
NOTIFIER_CYCLE: float = 1.0 #: Maximum waiting time for one notifier iteration.
34+
NOTIFIER_SHUTDOWN_TIMEOUT: float = 5.0 #: Maximum waiting time to stop notifiers.
35+
36+
def __init__(self, bus: Optional[can.BusABC] = None, loop: Optional[AbstractEventLoop] = None):
4537
"""
4638
:param can.BusABC bus:
4739
A python-can bus instance to re-use.
@@ -55,7 +47,7 @@ def __init__(
5547
#: List of :class:`can.Listener` objects.
5648
#: Includes at least MessageListener.
5749
self.listeners = [MessageListener(self)]
58-
self.notifier: Optional[Notifier] = None
50+
self.notifier: Optional[can.Notifier] = None
5951
self.nodes: Dict[int, Union[RemoteNode, LocalNode]] = {}
6052
self.subscribers: Dict[int, List[Callback]] = {}
6153
self.send_lock = threading.Lock()
@@ -106,7 +98,7 @@ def connect(self, *args, **kwargs) -> Network:
10698
10799
:param channel:
108100
Backend specific channel for the CAN interface.
109-
:param str bustype:
101+
:param str interface:
110102
Name of the interface. See
111103
`python-can manual <https://python-can.readthedocs.io/en/stable/configuration.html#interface-names>`__
112104
for full list of supported interfaces.
@@ -138,7 +130,7 @@ def connect(self, *args, **kwargs) -> Network:
138130
if self.bus is None:
139131
self.bus = can.Bus(*args, **kwargs)
140132
logger.info("Connected to '%s'", self.bus.channel_info)
141-
self.notifier = can.Notifier(self.bus, self.listeners, 1, **kwargs_notifier)
133+
self.notifier = can.Notifier(self.bus, self.listeners, self.NOTIFIER_CYCLE, **kwargs_notifier)
142134
return self
143135

144136
def disconnect(self) -> None:
@@ -150,7 +142,7 @@ def disconnect(self) -> None:
150142
if hasattr(node, "pdo"):
151143
node.pdo.stop()
152144
if self.notifier is not None:
153-
self.notifier.stop()
145+
self.notifier.stop(self.NOTIFIER_SHUTDOWN_TIMEOUT)
154146
if self.bus is not None:
155147
self.bus.shutdown()
156148
self.bus = None
@@ -352,7 +344,6 @@ def __init__(
352344
self.msg = can.Message(is_extended_id=can_id > 0x7FF,
353345
arbitration_id=can_id,
354346
data=data, is_remote_frame=remote)
355-
self._task = None
356347
self._start()
357348

358349
def _start(self):
@@ -418,9 +409,6 @@ class NodeScanner:
418409
The network to use when doing active searching.
419410
"""
420411

421-
#: Activate or deactivate scanning
422-
active = True
423-
424412
SERVICES = (0x700, 0x580, 0x180, 0x280, 0x380, 0x480, 0x80)
425413

426414
def __init__(self, network: Optional[Network] = None):

canopen/nmt.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from canopen.async_guard import ensure_not_async
99

1010
if TYPE_CHECKING:
11-
from canopen.network import Network
11+
from canopen.network import Network, PeriodicMessageTask
12+
1213

1314
logger = logging.getLogger(__name__)
1415

@@ -94,10 +95,10 @@ def state(self) -> str:
9495
- 'RESET'
9596
- 'RESET COMMUNICATION'
9697
"""
97-
if self._state in NMT_STATES:
98+
try:
9899
return NMT_STATES[self._state]
99-
else:
100-
return self._state
100+
except KeyError:
101+
return f"UNKNOWN STATE '{self._state}'"
101102

102103
@state.setter
103104
def state(self, new_state: str):
@@ -115,7 +116,7 @@ class NmtMaster(NmtBase):
115116
def __init__(self, node_id: int):
116117
super(NmtMaster, self).__init__(node_id)
117118
self._state_received = None
118-
self._node_guarding_producer = None
119+
self._node_guarding_producer: Optional[PeriodicMessageTask] = None
119120
#: Timestamp of last heartbeat message
120121
self.timestamp: Optional[float] = None
121122
self.state_update = threading.Condition()
@@ -259,7 +260,7 @@ class NmtSlave(NmtBase):
259260

260261
def __init__(self, node_id: int, local_node):
261262
super(NmtSlave, self).__init__(node_id)
262-
self._send_task = None
263+
self._send_task: Optional[PeriodicMessageTask] = None
263264
self._heartbeat_time_ms = 0
264265
self._local_node = local_node
265266

canopen/node/remote.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,28 @@ def __load_configuration_helper(self, index, subindex, name, value):
156156
index, subindex, e)
157157
raise
158158

159-
def load_configuration(self):
160-
''' Load the configuration of the node from the object dictionary.'''
159+
def load_configuration(self) -> None:
160+
"""Load the configuration of the node from the Object Dictionary.
161+
162+
Iterate through all objects in the Object Dictionary and download the
163+
values to the remote node via SDO.
164+
To avoid PDO mapping conflicts, PDO-related objects are handled through
165+
the methods :meth:`canopen.pdo.PdoBase.read` and
166+
:meth:`canopen.pdo.PdoBase.save`.
167+
168+
"""
169+
# First apply PDO configuration from object dictionary
170+
self.pdo.read(from_od=True)
171+
self.pdo.save()
172+
173+
# Now apply all other records in object dictionary
161174
for obj in self.object_dictionary.values():
175+
if 0x1400 <= obj.index < 0x1c00:
176+
# Ignore PDO related objects
177+
continue
162178
if isinstance(obj, ODRecord) or isinstance(obj, ODArray):
163179
for subobj in obj.values():
164180
if isinstance(subobj, ODVariable) and subobj.writable and (subobj.value is not None):
165181
self.__load_configuration_helper(subobj.index, subobj.subindex, subobj.name, subobj.value)
166182
elif isinstance(obj, ODVariable) and obj.writable and (obj.value is not None):
167183
self.__load_configuration_helper(obj.index, None, obj.name, obj.value)
168-
self.pdo.read() # reads the new configuration from the driver

0 commit comments

Comments
 (0)