Skip to content

Commit 11c71b6

Browse files
committed
Merge branch 'master' into feature-add-network
2 parents 544615a + cf9f33e commit 11c71b6

File tree

8 files changed

+153
-4
lines changed

8 files changed

+153
-4
lines changed

.github/workflows/pr-linters.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Run PR linters
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
7+
permissions:
8+
contents: read
9+
pull-requests: read
10+
11+
jobs:
12+
13+
mypy:
14+
name: Run mypy static type checker (optional)
15+
runs-on: ubuntu-latest
16+
continue-on-error: true
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: actions/setup-python@v5
20+
with:
21+
python-version: 3.12
22+
cache: pip
23+
cache-dependency-path: |
24+
'pyproject.toml'
25+
'requirements-dev.txt'
26+
- run: pip install -r requirements-dev.txt -e .
27+
- name: Run mypy and report
28+
run: mypy --config-file pyproject.toml .

.github/workflows/pythonpackage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
- name: Test with pytest
4343
run: pytest -v --cov=canopen --cov-report=xml --cov-branch
4444
- name: Upload coverage reports to Codecov
45-
uses: codecov/codecov-action@v4
45+
uses: codecov/codecov-action@v5
4646
with:
4747
token: ${{ secrets.CODECOV_TOKEN }}
4848

canopen/network.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,10 @@ def unsubscribe(self, can_id, callback=None) -> None:
7575
If given, remove only this callback. Otherwise all callbacks for
7676
the CAN ID.
7777
"""
78-
if callback is None:
79-
del self.subscribers[can_id]
80-
else:
78+
if callback is not None:
8179
self.subscribers[can_id].remove(callback)
80+
if not self.subscribers[can_id] or callback is None:
81+
del self.subscribers[can_id]
8282

8383
def connect(self, *args, **kwargs) -> Network:
8484
"""Connect to CAN bus using python-can.

canopen/node/local.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ def __init__(
3939
self.emcy = EmcyProducer(0x80 + self.id)
4040

4141
def associate_network(self, network: canopen.network.Network):
42+
if self.has_network():
43+
raise RuntimeError("Node is already associated with a network")
4244
self.network = network
4345
self.sdo.network = network
4446
self.tpdo.network = network
@@ -49,6 +51,8 @@ def associate_network(self, network: canopen.network.Network):
4951
network.subscribe(0, self.nmt.on_command)
5052

5153
def remove_network(self) -> None:
54+
if not self.has_network():
55+
return
5256
self.network.unsubscribe(self.sdo.rx_cobid, self.sdo.on_request)
5357
self.network.unsubscribe(0, self.nmt.on_command)
5458
self.network = canopen.network._UNINITIALIZED_NETWORK

canopen/node/remote.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ def __init__(
5151
self.load_configuration()
5252

5353
def associate_network(self, network: canopen.network.Network):
54+
if self.has_network():
55+
raise RuntimeError("Node is already associated with a network")
5456
self.network = network
5557
self.sdo.network = network
5658
self.pdo.network = network
@@ -64,6 +66,8 @@ def associate_network(self, network: canopen.network.Network):
6466
network.subscribe(0, self.nmt.on_command)
6567

6668
def remove_network(self) -> None:
69+
if not self.has_network():
70+
return
6771
for sdo in self.sdo_channels:
6872
self.network.unsubscribe(sdo.tx_cobid, sdo.on_response)
6973
self.network.unsubscribe(0x700 + self.id, self.nmt.on_heartbeat)

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,11 @@ testpaths = [
4848
filterwarnings = [
4949
"ignore::DeprecationWarning",
5050
]
51+
52+
[tool.mypy]
53+
python_version = "3.8"
54+
exclude = [
55+
"^examples*",
56+
"^test*",
57+
"^setup.py*",
58+
]

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
mypy~=1.10
12
pytest~=8.3
23
pytest-cov~=5.0

test/test_node.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import unittest
2+
3+
import canopen
4+
5+
6+
def count_subscribers(network: canopen.Network) -> int:
7+
"""Count the number of subscribers in the network."""
8+
return sum(len(n) for n in network.subscribers.values())
9+
10+
11+
class TestLocalNode(unittest.TestCase):
12+
13+
@classmethod
14+
def setUpClass(cls):
15+
cls.network = canopen.Network()
16+
cls.network.NOTIFIER_SHUTDOWN_TIMEOUT = 0.0
17+
cls.network.connect(interface="virtual")
18+
19+
cls.node = canopen.LocalNode(2, canopen.objectdictionary.ObjectDictionary())
20+
21+
@classmethod
22+
def tearDownClass(cls):
23+
cls.network.disconnect()
24+
25+
def test_associate_network(self):
26+
# Need to store the number of subscribers before associating because the
27+
# network implementation automatically adds subscribers to the list
28+
n_subscribers = count_subscribers(self.network)
29+
30+
# Associating the network with the local node
31+
self.node.associate_network(self.network)
32+
self.assertIs(self.node.network, self.network)
33+
self.assertIs(self.node.sdo.network, self.network)
34+
self.assertIs(self.node.tpdo.network, self.network)
35+
self.assertIs(self.node.rpdo.network, self.network)
36+
self.assertIs(self.node.nmt.network, self.network)
37+
self.assertIs(self.node.emcy.network, self.network)
38+
39+
# Test that its not possible to associate the network multiple times
40+
with self.assertRaises(RuntimeError) as cm:
41+
self.node.associate_network(self.network)
42+
self.assertIn("already associated with a network", str(cm.exception))
43+
44+
# Test removal of the network. The count of subscribers should
45+
# be the same as before the association
46+
self.node.remove_network()
47+
uninitalized = canopen.network._UNINITIALIZED_NETWORK
48+
self.assertIs(self.node.network, uninitalized)
49+
self.assertIs(self.node.sdo.network, uninitalized)
50+
self.assertIs(self.node.tpdo.network, uninitalized)
51+
self.assertIs(self.node.rpdo.network, uninitalized)
52+
self.assertIs(self.node.nmt.network, uninitalized)
53+
self.assertIs(self.node.emcy.network, uninitalized)
54+
self.assertEqual(count_subscribers(self.network), n_subscribers)
55+
56+
# Test that its possible to deassociate the network multiple times
57+
self.node.remove_network()
58+
59+
60+
class TestRemoteNode(unittest.TestCase):
61+
62+
@classmethod
63+
def setUpClass(cls):
64+
cls.network = canopen.Network()
65+
cls.network.NOTIFIER_SHUTDOWN_TIMEOUT = 0.0
66+
cls.network.connect(interface="virtual")
67+
68+
cls.node = canopen.RemoteNode(2, canopen.objectdictionary.ObjectDictionary())
69+
70+
@classmethod
71+
def tearDownClass(cls):
72+
cls.network.disconnect()
73+
74+
def test_associate_network(self):
75+
# Need to store the number of subscribers before associating because the
76+
# network implementation automatically adds subscribers to the list
77+
n_subscribers = count_subscribers(self.network)
78+
79+
# Associating the network with the local node
80+
self.node.associate_network(self.network)
81+
self.assertIs(self.node.network, self.network)
82+
self.assertIs(self.node.sdo.network, self.network)
83+
self.assertIs(self.node.tpdo.network, self.network)
84+
self.assertIs(self.node.rpdo.network, self.network)
85+
self.assertIs(self.node.nmt.network, self.network)
86+
87+
# Test that its not possible to associate the network multiple times
88+
with self.assertRaises(RuntimeError) as cm:
89+
self.node.associate_network(self.network)
90+
self.assertIn("already associated with a network", str(cm.exception))
91+
92+
# Test removal of the network. The count of subscribers should
93+
# be the same as before the association
94+
self.node.remove_network()
95+
uninitalized = canopen.network._UNINITIALIZED_NETWORK
96+
self.assertIs(self.node.network, uninitalized)
97+
self.assertIs(self.node.sdo.network, uninitalized)
98+
self.assertIs(self.node.tpdo.network, uninitalized)
99+
self.assertIs(self.node.rpdo.network, uninitalized)
100+
self.assertIs(self.node.nmt.network, uninitalized)
101+
self.assertEqual(count_subscribers(self.network), n_subscribers)
102+
103+
# Test that its possible to deassociate the network multiple times
104+
self.node.remove_network()

0 commit comments

Comments
 (0)