Skip to content

Commit 36150d7

Browse files
committed
Merge branch 'test-sdo-variables' into sdorecord-skip-count
2 parents 27b3724 + 30c4071 commit 36150d7

8 files changed

Lines changed: 164 additions & 62 deletions

File tree

test/sample.eds

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,7 @@ DataType=0x0007
100100
AccessType=ro
101101
PDOMapping=0
102102

103-
[1018sub3]
104-
ParameterName=Revision number
105-
ObjectType=0x7
106-
DataType=0x0007
107-
AccessType=ro
108-
PDOMapping=0
103+
; [1018sub3] left out for testing
109104

110105
[1018sub4]
111106
ParameterName=Serial number
@@ -123,11 +118,62 @@ SupportedObjects=3
123118
[1003]
124119
ParameterName=Pre-defined error field
125120
ObjectType=0x8
126-
CompactSubObj=255
121+
SubNumber=9
122+
123+
[1003sub0]
124+
ParameterName=Number of errors
125+
ObjectType=0x7
126+
DataType=0x0005
127+
AccessType=rw
128+
DefaultValue=3
129+
PDOMapping=0
130+
131+
[1003sub1]
132+
ParameterName=Pre-defined error field_1
133+
ObjectType=0x7
134+
DataType=0x0007
135+
AccessType=ro
136+
DefaultValue=0
137+
PDOMapping=0
138+
139+
; [1003sub2] left out for testing
140+
141+
[1003sub3]
142+
ParameterName=Pre-defined error field_3
143+
ObjectType=0x7
144+
DataType=0x0007
145+
AccessType=ro
146+
DefaultValue=0
147+
PDOMapping=0
148+
149+
[1003sub4]
150+
ParameterName=Pre-defined error field_4
151+
ObjectType=0x7
152+
DataType=0x0007
153+
AccessType=ro
154+
DefaultValue=0
155+
PDOMapping=0
156+
157+
[1003sub5]
158+
ParameterName=Pre-defined error field_5
159+
ObjectType=0x7
160+
DataType=0x0007
161+
AccessType=ro
162+
DefaultValue=0
163+
PDOMapping=0
164+
165+
; [1003sub6] left out for testing
166+
167+
[1003sub7]
168+
ParameterName=Pre-defined error field_7
169+
ObjectType=0x7
127170
DataType=0x0007
128171
AccessType=ro
172+
DefaultValue=0
129173
PDOMapping=0
130174

175+
; [1003sub8] left out for testing
176+
131177
[1008]
132178
ParameterName=Manufacturer device name
133179
ObjectType=0x7

test/test_eds.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def test_relative_variable(self):
121121
def test_record(self):
122122
record = self.od['Identity object']
123123
self.assertIsInstance(record, canopen.objectdictionary.ODRecord)
124-
self.assertEqual(len(record), 5)
124+
self.assertEqual(len(record), 4)
125125
self.assertEqual(record.index, 0x1018)
126126
self.assertEqual(record.name, 'Identity object')
127127
var = record['Vendor-ID']
@@ -357,3 +357,7 @@ def verify_od(self, source, doctype):
357357
f" mismatch on {pretty_index(evar.index, evar.subindex)}")
358358

359359
self.assertEqual(self.od.comments, exported_od.comments)
360+
361+
362+
if __name__ == "__main__":
363+
unittest.main()

test/test_emcy.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,7 @@ def check(*args, res):
218218
check(res=b'\x00\x00\x00\x00\x00\x00\x00\x00')
219219
check(3, res=b'\x00\x00\x03\x00\x00\x00\x00\x00')
220220
check(3, b"\xaa\xbb", res=b'\x00\x00\x03\xaa\xbb\x00\x00\x00')
221+
222+
223+
if __name__ == "__main__":
224+
unittest.main()

test/test_local.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def test_segmented_download(self):
8989
def test_slave_send_heartbeat(self):
9090
# Setting the heartbeat time should trigger heartbeating
9191
# to start
92-
self.remote_node.sdo["Producer heartbeat time"].raw = 1000
92+
self.remote_node.sdo["Producer heartbeat time"].raw = 100
9393
state = self.remote_node.nmt.wait_for_heartbeat()
9494
self.local_node.nmt.stop_heartbeat()
9595
# The NMT master will change the state INITIALISING (0)
@@ -98,7 +98,7 @@ def test_slave_send_heartbeat(self):
9898

9999
def test_nmt_state_initializing_to_preoper(self):
100100
# Initialize the heartbeat timer
101-
self.local_node.sdo["Producer heartbeat time"].raw = 1000
101+
self.local_node.sdo["Producer heartbeat time"].raw = 100
102102
self.local_node.nmt.stop_heartbeat()
103103
# This transition shall start the heartbeating
104104
self.local_node.nmt.state = 'INITIALISING'

test/test_network.py

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
2-
import unittest
32
import threading
3+
import time
4+
import unittest
45

56
import canopen
67
import can
@@ -231,59 +232,57 @@ def test_network_send_periodic(self):
231232
DATA1 = bytes([1, 2, 3])
232233
DATA2 = bytes([4, 5, 6])
233234
COB_ID = 0x123
234-
PERIOD = 0.1
235+
PERIOD = 0.01
235236
TIMEOUT = PERIOD * 10
236-
self.network.connect(interface="virtual", receive_own_messages=True)
237+
self.network.connect(interface="virtual")
237238
self.addCleanup(self.network.disconnect)
238239

239-
acc = []
240-
condition = threading.Condition()
241-
242-
def hook(_, data, ts):
243-
with condition:
244-
item = data, ts
245-
acc.append(item)
246-
condition.notify_all()
240+
bus = can.Bus(interface="virtual")
241+
self.addCleanup(bus.shutdown)
247242

248-
self.network.subscribe(COB_ID, hook)
249-
self.addCleanup(self.network.unsubscribe, COB_ID)
243+
acc = []
250244

251245
task = self.network.send_periodic(COB_ID, DATA1, PERIOD)
252246
self.addCleanup(task.stop)
253247

254-
def periodicity():
248+
def wait_for_periodicity():
255249
# Check if periodicity is established; flakiness has been observed
256250
# on macOS.
257-
if len(acc) >= 2:
258-
delta = acc[-1][1] - acc[-2][1]
259-
return round(delta, ndigits=1) == PERIOD
260-
return False
251+
end_time = time.time() + TIMEOUT
252+
while time.time() < end_time:
253+
if msg := bus.recv(PERIOD):
254+
acc.append(msg)
255+
if len(acc) >= 2:
256+
first, last = acc[-2:]
257+
delta = last.timestamp - first.timestamp
258+
if round(delta, ndigits=2) == PERIOD:
259+
return
260+
self.fail("Timed out")
261261

262262
# Wait for frames to arrive; then check the result.
263-
with condition:
264-
condition.wait_for(periodicity, TIMEOUT)
265-
self.assertTrue(all(v[0] == DATA1 for v in acc))
263+
wait_for_periodicity()
264+
self.assertTrue(all([v.data == DATA1 for v in acc]))
266265

267266
# Update task data, which may implicitly restart the timer.
268267
# Wait for frames to arrive; then check the result.
269268
task.update(DATA2)
270-
with condition:
271-
acc.clear()
272-
condition.wait_for(periodicity, TIMEOUT)
269+
acc.clear()
270+
wait_for_periodicity()
273271
# Find the first message with new data, and verify that all subsequent
274272
# messages also carry the new payload.
275-
data = [v[0] for v in acc]
273+
data = [v.data for v in acc]
274+
self.assertIn(DATA2, data)
276275
idx = data.index(DATA2)
277-
self.assertTrue(all(v[0] == DATA2 for v in acc[idx:]))
276+
self.assertTrue(all([v.data == DATA2 for v in acc[idx:]]))
278277

279278
# Stop the task.
280279
task.stop()
281280
# A message may have been in flight when we stopped the timer,
282281
# so allow a single failure.
283282
bus = self.network.bus
284-
msg = bus.recv(TIMEOUT)
283+
msg = bus.recv(PERIOD)
285284
if msg is not None:
286-
self.assertIsNone(bus.recv(TIMEOUT))
285+
self.assertIsNone(bus.recv(PERIOD))
287286

288287

289288
class TestScanner(unittest.TestCase):

test/test_nmt.py

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import threading
12
import time
23
import unittest
34

@@ -41,28 +42,28 @@ def test_state_set_invalid(self):
4142

4243
class TestNmtMaster(unittest.TestCase):
4344
NODE_ID = 2
44-
COB_ID = 0x700 + NODE_ID
4545
PERIOD = 0.01
46-
TIMEOUT = PERIOD * 2
46+
TIMEOUT = PERIOD * 10
4747

4848
def setUp(self):
49-
bus = can.ThreadSafeBus(
50-
interface="virtual",
51-
channel="test",
52-
receive_own_messages=True,
53-
)
54-
net = canopen.Network(bus)
49+
net = canopen.Network()
5550
net.NOTIFIER_SHUTDOWN_TIMEOUT = 0.0
56-
net.connect()
51+
net.connect(interface="virtual")
5752
with self.assertLogs():
5853
node = net.add_node(self.NODE_ID, SAMPLE_EDS)
5954

60-
self.bus = bus
55+
self.bus = can.Bus(interface="virtual")
6156
self.net = net
6257
self.node = node
6358

6459
def tearDown(self):
6560
self.net.disconnect()
61+
self.bus.shutdown()
62+
63+
def dispatch_heartbeat(self, code):
64+
cob_id = 0x700 + self.NODE_ID
65+
hb = can.Message(arbitration_id=cob_id, data=[code])
66+
self.bus.send(hb)
6667

6768
def test_nmt_master_no_heartbeat(self):
6869
with self.assertRaisesRegex(NmtError, "heartbeat"):
@@ -74,47 +75,54 @@ def test_nmt_master_on_heartbeat(self):
7475
# Skip the special INITIALISING case.
7576
for code in [st for st in NMT_STATES if st != 0]:
7677
with self.subTest(code=code):
77-
task = self.net.send_periodic(self.COB_ID, [code], self.PERIOD)
78-
try:
79-
actual = self.node.nmt.wait_for_heartbeat(self.TIMEOUT)
80-
finally:
81-
task.stop()
78+
t = threading.Timer(0.01, self.dispatch_heartbeat, args=(code,))
79+
t.start()
80+
self.addCleanup(t.join)
81+
actual = self.node.nmt.wait_for_heartbeat(0.1)
8282
expected = NMT_STATES[code]
8383
self.assertEqual(actual, expected)
8484

85-
def test_nmt_master_on_heartbeat_initialising(self):
86-
task = self.net.send_periodic(self.COB_ID, [0], self.PERIOD)
87-
self.addCleanup(task.stop)
85+
def test_nmt_master_wait_for_bootup(self):
86+
t = threading.Timer(0.01, self.dispatch_heartbeat, args=(0x00,))
87+
t.start()
88+
self.addCleanup(t.join)
8889
self.node.nmt.wait_for_bootup(self.TIMEOUT)
90+
self.assertEqual(self.node.nmt.state, "PRE-OPERATIONAL")
91+
92+
def test_nmt_master_on_heartbeat_initialising(self):
93+
t = threading.Timer(0.01, self.dispatch_heartbeat, args=(0x00,))
94+
t.start()
95+
self.addCleanup(t.join)
8996
state = self.node.nmt.wait_for_heartbeat(self.TIMEOUT)
9097
self.assertEqual(state, "PRE-OPERATIONAL")
9198

9299
def test_nmt_master_on_heartbeat_unknown_state(self):
93-
task = self.net.send_periodic(self.COB_ID, [0xcb], self.PERIOD)
94-
self.addCleanup(task.stop)
100+
t = threading.Timer(0.01, self.dispatch_heartbeat, args=(0xcb,))
101+
t.start()
102+
self.addCleanup(t.join)
95103
state = self.node.nmt.wait_for_heartbeat(self.TIMEOUT)
96104
# Expect the high bit to be masked out, and a formatted string to
97105
# be returned.
98106
self.assertEqual(state, "UNKNOWN STATE '75'")
99107

100108
def test_nmt_master_add_heartbeat_callback(self):
101-
from threading import Event
102-
event = Event()
109+
event = threading.Event()
103110
state = None
104111
def hook(st):
105112
nonlocal state
106113
state = st
107114
event.set()
108115
self.node.nmt.add_heartbeat_callback(hook)
109-
self.net.send_message(self.COB_ID, bytes([127]))
116+
117+
self.dispatch_heartbeat(0x7f)
110118
self.assertTrue(event.wait(self.TIMEOUT))
111119
self.assertEqual(state, 127)
112120

113121
def test_nmt_master_node_guarding(self):
114122
self.node.nmt.start_node_guarding(self.PERIOD)
115123
msg = self.bus.recv(self.TIMEOUT)
116124
self.assertIsNotNone(msg)
117-
self.assertEqual(msg.arbitration_id, self.COB_ID)
125+
self.assertEqual(msg.arbitration_id, 0x700 + self.NODE_ID)
118126
self.assertEqual(msg.dlc, 0)
119127

120128
self.node.nmt.stop_node_guarding()

test/test_od.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,7 @@ def test_subindexes(self):
261261
self.assertEqual(array[1].name, "Test Variable")
262262
self.assertEqual(array[2].name, "Test Variable 2")
263263
self.assertEqual(array[3].name, "Test Variable_3")
264+
265+
266+
if __name__ == "__main__":
267+
unittest.main()

test/test_sdo.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,43 @@
1010
RX = 2
1111

1212

13+
class TestSDOVariables(unittest.TestCase):
14+
"""Some basic assumptions on the behavior of SDO variable objects.
15+
16+
Mostly what is stated in the API docs.
17+
"""
18+
19+
def setUp(self):
20+
node = canopen.LocalNode(1, SAMPLE_EDS)
21+
self.sdo_node = node.sdo
22+
23+
def test_record_iter_length(self):
24+
"""Assume the "highest subindex supported" entry is not counted.
25+
26+
Sub-objects without an OD entry should be skipped as well."""
27+
record = self.sdo_node[0x1018]
28+
subs = sum(1 for _ in iter(record))
29+
self.assertEqual(len(record), 3)
30+
self.assertEqual(subs, 3)
31+
32+
def test_array_iter_length(self):
33+
"""Assume the "highest subindex supported" entry is not counted."""
34+
array = self.sdo_node[0x1003]
35+
subs = sum(1 for _ in iter(array))
36+
self.assertEqual(len(array), 3)
37+
self.assertEqual(subs, 3)
38+
# Simulate more entries getting added dynamically
39+
array[0].set_data(b'\x08')
40+
subs = sum(1 for _ in iter(array))
41+
self.assertEqual(subs, 8)
42+
43+
def test_array_members_dynamic(self):
44+
"""Check if sub-objects missing from OD entry are generated dynamically."""
45+
array = self.sdo_node[0x1003]
46+
for var in array.values():
47+
self.assertIsInstance(var, canopen.sdo.SdoVariable)
48+
49+
1350
class TestSDO(unittest.TestCase):
1451
"""
1552
Test SDO traffic by example. Most are taken from

0 commit comments

Comments
 (0)