Skip to content

Commit 657062a

Browse files
authored
DS402 code cleanup (#245)
* ds402: Define additional operation mode constants. Add the missing CYCLIC_SYNCHRONOUS_... modes to the mapping dicts, including the supported mode bit flags. * ds402: Use monotonic clock for timeouts. The value returned from time.time() can go backwards between multiple calls, so time.monotonic() is the recommended way for checking timeouts. * ds402: Make timeout values configurable. Move all timing-related magic numbers to class-level constants and use those for timeout calculations and sleep() calls. That allows the library user to override the defaults on a per-node basis if needed, by just setting the same attribute on the object. And it's cleaner code without magic numbers. * ds402: Code cleanup: whitespace and indentation. * ds402: Clean up list formatting and style. Consistently format constant definitions, especially constant mappings: * Write Controlword commands with for hex digits (16 bits). * Same for Supported Drive Modes flags (only 16 of 32 bits). * Move colon directly after key in dicts and align values consistently. Fixes lots of flake8 errors. * Always include trailing comma in multi-line sequences. * Use tuples instead of lists for constant definitions. They don't need to be mutable. * ds402: Use tuple unpacking for simplified bitmask code. Apply the same pattern whenever the Statusword is checked for a specific bit pattern.
1 parent 4e3654a commit 657062a

File tree

1 file changed

+118
-101
lines changed

1 file changed

+118
-101
lines changed

canopen/profiles/p402.py

Lines changed: 118 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -6,53 +6,54 @@
66

77
logger = logging.getLogger(__name__)
88

9+
910
class State402(object):
1011
# Controlword (0x6040) commands
11-
CW_OPERATION_ENABLED = 0x0F
12-
CW_SHUTDOWN = 0x06
13-
CW_SWITCH_ON = 0x07
14-
CW_QUICK_STOP = 0x02
15-
CW_DISABLE_VOLTAGE = 0x00
16-
CW_SWITCH_ON_DISABLED = 0x80
12+
CW_OPERATION_ENABLED = 0x000F
13+
CW_SHUTDOWN = 0x0006
14+
CW_SWITCH_ON = 0x0007
15+
CW_QUICK_STOP = 0x0002
16+
CW_DISABLE_VOLTAGE = 0x0000
17+
CW_SWITCH_ON_DISABLED = 0x0080
1718

1819
CW_CODE_COMMANDS = {
19-
CW_SWITCH_ON_DISABLED : 'SWITCH ON DISABLED',
20-
CW_DISABLE_VOLTAGE : 'DISABLE VOLTAGE',
21-
CW_SHUTDOWN : 'READY TO SWITCH ON',
22-
CW_SWITCH_ON : 'SWITCHED ON',
23-
CW_OPERATION_ENABLED : 'OPERATION ENABLED',
24-
CW_QUICK_STOP : 'QUICK STOP ACTIVE'
20+
CW_SWITCH_ON_DISABLED: 'SWITCH ON DISABLED',
21+
CW_DISABLE_VOLTAGE: 'DISABLE VOLTAGE',
22+
CW_SHUTDOWN: 'READY TO SWITCH ON',
23+
CW_SWITCH_ON: 'SWITCHED ON',
24+
CW_OPERATION_ENABLED: 'OPERATION ENABLED',
25+
CW_QUICK_STOP: 'QUICK STOP ACTIVE',
2526
}
2627

2728
CW_COMMANDS_CODE = {
28-
'SWITCH ON DISABLED' : CW_SWITCH_ON_DISABLED,
29-
'DISABLE VOLTAGE' : CW_DISABLE_VOLTAGE,
30-
'READY TO SWITCH ON' : CW_SHUTDOWN,
31-
'SWITCHED ON' : CW_SWITCH_ON,
32-
'OPERATION ENABLED' : CW_OPERATION_ENABLED,
33-
'QUICK STOP ACTIVE' : CW_QUICK_STOP
29+
'SWITCH ON DISABLED': CW_SWITCH_ON_DISABLED,
30+
'DISABLE VOLTAGE': CW_DISABLE_VOLTAGE,
31+
'READY TO SWITCH ON': CW_SHUTDOWN,
32+
'SWITCHED ON': CW_SWITCH_ON,
33+
'OPERATION ENABLED': CW_OPERATION_ENABLED,
34+
'QUICK STOP ACTIVE': CW_QUICK_STOP,
3435
}
3536

3637
# Statusword 0x6041 bitmask and values in the list in the dictionary value
3738
SW_MASK = {
38-
'NOT READY TO SWITCH ON': [0x4F, 0x00],
39-
'SWITCH ON DISABLED' : [0x4F, 0x40],
40-
'READY TO SWITCH ON' : [0x6F, 0x21],
41-
'SWITCHED ON' : [0x6F, 0x23],
42-
'OPERATION ENABLED' : [0x6F, 0x27],
43-
'FAULT' : [0x4F, 0x08],
44-
'FAULT REACTION ACTIVE' : [0x4F, 0x0F],
45-
'QUICK STOP ACTIVE' : [0x6F, 0x07]
39+
'NOT READY TO SWITCH ON': (0x4F, 0x00),
40+
'SWITCH ON DISABLED': (0x4F, 0x40),
41+
'READY TO SWITCH ON': (0x6F, 0x21),
42+
'SWITCHED ON': (0x6F, 0x23),
43+
'OPERATION ENABLED': (0x6F, 0x27),
44+
'FAULT': (0x4F, 0x08),
45+
'FAULT REACTION ACTIVE': (0x4F, 0x0F),
46+
'QUICK STOP ACTIVE': (0x6F, 0x07),
4647
}
4748

4849
# Transition path to get to the 'OPERATION ENABLED' state
4950
NEXTSTATE2ENABLE = {
50-
('START') : 'NOT READY TO SWITCH ON',
51-
('FAULT', 'NOT READY TO SWITCH ON') : 'SWITCH ON DISABLED',
52-
('SWITCH ON DISABLED') : 'READY TO SWITCH ON',
53-
('READY TO SWITCH ON') : 'SWITCHED ON',
54-
('SWITCHED ON', 'QUICK STOP ACTIVE', 'OPERATION ENABLED') : 'OPERATION ENABLED',
55-
('FAULT REACTION ACTIVE') : 'FAULT'
51+
('START'): 'NOT READY TO SWITCH ON',
52+
('FAULT', 'NOT READY TO SWITCH ON'): 'SWITCH ON DISABLED',
53+
('SWITCH ON DISABLED'): 'READY TO SWITCH ON',
54+
('READY TO SWITCH ON'): 'SWITCHED ON',
55+
('SWITCHED ON', 'QUICK STOP ACTIVE', 'OPERATION ENABLED'): 'OPERATION ENABLED',
56+
('FAULT REACTION ACTIVE'): 'FAULT'
5657
}
5758

5859
# Tansition table from the DS402 State Machine
@@ -110,35 +111,45 @@ class OperationMode(object):
110111
OPEN_LOOP_VECTOR_MODE = -2
111112

112113
CODE2NAME = {
113-
NO_MODE : 'NO MODE',
114-
PROFILED_POSITION : 'PROFILED POSITION',
115-
VELOCITY : 'VELOCITY',
116-
PROFILED_VELOCITY : 'PROFILED VELOCITY',
117-
PROFILED_TORQUE : 'PROFILED TORQUE',
118-
HOMING : 'HOMING',
119-
INTERPOLATED_POSITION : 'INTERPOLATED POSITION'
114+
NO_MODE: 'NO MODE',
115+
PROFILED_POSITION: 'PROFILED POSITION',
116+
VELOCITY: 'VELOCITY',
117+
PROFILED_VELOCITY: 'PROFILED VELOCITY',
118+
PROFILED_TORQUE: 'PROFILED TORQUE',
119+
HOMING: 'HOMING',
120+
INTERPOLATED_POSITION: 'INTERPOLATED POSITION',
121+
CYCLIC_SYNCHRONOUS_POSITION: 'CYCLIC SYNCHRONOUS POSITION',
122+
CYCLIC_SYNCHRONOUS_VELOCITY: 'CYCLIC SYNCHRONOUS VELOCITY',
123+
CYCLIC_SYNCHRONOUS_TORQUE: 'CYCLIC SYNCHRONOUS TORQUE',
120124
}
121125

122126
NAME2CODE = {
123-
'NO MODE' : NO_MODE,
124-
'PROFILED POSITION' : PROFILED_POSITION,
125-
'VELOCITY' : VELOCITY,
126-
'PROFILED VELOCITY' : PROFILED_VELOCITY,
127-
'PROFILED TORQUE' : PROFILED_TORQUE,
128-
'HOMING' : HOMING,
129-
'INTERPOLATED POSITION' : INTERPOLATED_POSITION
127+
'NO MODE': NO_MODE,
128+
'PROFILED POSITION': PROFILED_POSITION,
129+
'VELOCITY': VELOCITY,
130+
'PROFILED VELOCITY': PROFILED_VELOCITY,
131+
'PROFILED TORQUE': PROFILED_TORQUE,
132+
'HOMING': HOMING,
133+
'INTERPOLATED POSITION': INTERPOLATED_POSITION,
134+
'CYCLIC SYNCHRONOUS POSITION': CYCLIC_SYNCHRONOUS_POSITION,
135+
'CYCLIC SYNCHRONOUS VELOCITY': CYCLIC_SYNCHRONOUS_VELOCITY,
136+
'CYCLIC SYNCHRONOUS TORQUE': CYCLIC_SYNCHRONOUS_TORQUE,
130137
}
131138

132139
SUPPORTED = {
133-
'NO MODE' : 0x0,
134-
'PROFILED POSITION' : 0x1,
135-
'VELOCITY' : 0x2,
136-
'PROFILED VELOCITY' : 0x4,
137-
'PROFILED TORQUE' : 0x8,
138-
'HOMING' : 0x20,
139-
'INTERPOLATED POSITION' : 0x40
140+
'NO MODE': 0x0000,
141+
'PROFILED POSITION': 0x0001,
142+
'VELOCITY': 0x0002,
143+
'PROFILED VELOCITY': 0x0004,
144+
'PROFILED TORQUE': 0x0008,
145+
'HOMING': 0x0020,
146+
'INTERPOLATED POSITION': 0x0040,
147+
'CYCLIC SYNCHRONOUS POSITION': 0x0080,
148+
'CYCLIC SYNCHRONOUS VELOCITY': 0x0100,
149+
'CYCLIC SYNCHRONOUS TORQUE': 0x0200,
140150
}
141151

152+
142153
class Homing(object):
143154
CW_START = 0x10
144155
CW_HALT = 0x100
@@ -154,23 +165,23 @@ class Homing(object):
154165
HM_NO_HOMING_OPERATION = 0
155166
HM_ON_THE_NEGATIVE_LIMIT_SWITCH_AND_INDEX_PULSE = 1
156167
HM_ON_THE_POSITIVE_LIMIT_SWITCH_AND_INDEX_PULSE = 2
157-
HM_ON_THE_POSITIVE_HOME_SWITCH_AND_INDEX_PULSE = [3, 4]
158-
HM_ON_THE_NEGATIVE_HOME_SWITCH_AND_INDEX_PULSE = [5, 6]
168+
HM_ON_THE_POSITIVE_HOME_SWITCH_AND_INDEX_PULSE = (3, 4)
169+
HM_ON_THE_NEGATIVE_HOME_SWITCH_AND_INDEX_PULSE = (5, 6)
159170
HM_ON_THE_NEGATIVE_LIMIT_SWITCH = 17
160171
HM_ON_THE_POSITIVE_LIMIT_SWITCH = 18
161-
HM_ON_THE_POSITIVE_HOME_SWITCH = [19, 20]
162-
HM_ON_THE_NEGATIVE_HOME_SWITCH = [21, 22]
172+
HM_ON_THE_POSITIVE_HOME_SWITCH = (19, 20)
173+
HM_ON_THE_NEGATIVE_HOME_SWITCH = (21, 22)
163174
HM_ON_NEGATIVE_INDEX_PULSE = 33
164175
HM_ON_POSITIVE_INDEX_PULSE = 34
165176
HM_ON_CURRENT_POSITION = 35
166177

167178
STATES = {
168-
'IN PROGRESS' : [0x3400, 0x0000],
169-
'INTERRUPTED' : [0x3400, 0x0400],
170-
'ATTAINED' : [0x3400, 0x1000],
171-
'TARGET REACHED' : [0x3400, 0x1400],
172-
'ERROR VELOCITY IS NOT ZERO' : [0x3400, 0x2000],
173-
'ERROR VELOCITY IS ZERO' : [0x3400, 0x2400]
179+
'IN PROGRESS': (0x3400, 0x0000),
180+
'INTERRUPTED': (0x3400, 0x0400),
181+
'ATTAINED': (0x3400, 0x1000),
182+
'TARGET REACHED': (0x3400, 0x1400),
183+
'ERROR VELOCITY IS NOT ZERO': (0x3400, 0x2000),
184+
'ERROR VELOCITY IS ZERO': (0x3400, 0x2400),
174185
}
175186

176187

@@ -185,10 +196,17 @@ class BaseNode402(RemoteNode):
185196
:type object_dictionary: :class:`str`, :class:`canopen.ObjectDictionary`
186197
"""
187198

199+
TIMEOUT_RESET_FAULT = 0.4 # seconds
200+
TIMEOUT_SWITCH_OP_MODE = 0.5 # seconds
201+
TIMEOUT_SWITCH_STATE_FINAL = 0.8 # seconds
202+
TIMEOUT_SWITCH_STATE_SINGLE = 0.4 # seconds
203+
INTERVAL_CHECK_STATE = 0.01 # seconds
204+
TIMEOUT_HOMING_DEFAULT = 30 # seconds
205+
188206
def __init__(self, node_id, object_dictionary):
189207
super(BaseNode402, self).__init__(node_id, object_dictionary)
190-
self.tpdo_values = dict() # { index: TPDO_value }
191-
self.rpdo_pointers = dict() # { index: RPDO_pointer }
208+
self.tpdo_values = dict() # { index: TPDO_value }
209+
self.rpdo_pointers = dict() # { index: RPDO_pointer }
192210

193211
def setup_402_state_machine(self):
194212
"""Configure the state machine by searching for a TPDO that has the
@@ -204,7 +222,7 @@ def setup_402_state_machine(self):
204222
self.state = 'SWITCH ON DISABLED' # Why change state?
205223

206224
def setup_pdos(self):
207-
self.pdo.read() # TPDO and RPDO configurations
225+
self.pdo.read() # TPDO and RPDO configurations
208226
self._init_tpdo_values()
209227
self._init_rpdo_pointers()
210228

@@ -218,7 +236,7 @@ def _init_tpdo_values(self):
218236
self.tpdo_values[obj.index] = 0
219237

220238
def _init_rpdo_pointers(self):
221-
# If RPDOs have overlapping indecies, rpdo_pointers will point to
239+
# If RPDOs have overlapping indecies, rpdo_pointers will point to
222240
# the first RPDO that has that index configured.
223241
for rpdo in self.rpdo.values():
224242
if rpdo.enabled:
@@ -228,13 +246,13 @@ def _init_rpdo_pointers(self):
228246
self.rpdo_pointers[obj.index] = obj
229247

230248
def _check_controlword_configured(self):
231-
if 0x6040 not in self.rpdo_pointers: # Controlword
249+
if 0x6040 not in self.rpdo_pointers: # Controlword
232250
logger.warning(
233251
"Controlword not configured in node {0}'s PDOs. Using SDOs can cause slow performance.".format(
234252
self.id))
235253

236254
def _check_statusword_configured(self):
237-
if 0x6041 not in self.tpdo_values: # Statusword
255+
if 0x6041 not in self.tpdo_values: # Statusword
238256
raise ValueError(
239257
"Statusword not configured in node {0}'s PDOs. Using SDOs can cause slow performance.".format(
240258
self.id))
@@ -245,18 +263,18 @@ def reset_from_fault(self):
245263
if self.state == 'FAULT':
246264
# Resets the Fault Reset bit (rising edge 0 -> 1)
247265
self.controlword = State402.CW_DISABLE_VOLTAGE
248-
timeout = time.time() + 0.4 # 400 ms
249-
266+
timeout = time.monotonic() + self.TIMEOUT_RESET_FAULT
250267
while self.is_faulted():
251-
if time.time() > timeout:
268+
if time.monotonic() > timeout:
252269
break
253-
time.sleep(0.01) # 10 ms
270+
time.sleep(self.INTERVAL_CHECK_STATE)
254271
self.state = 'OPERATION ENABLED'
255-
272+
256273
def is_faulted(self):
257-
return self.statusword & State402.SW_MASK['FAULT'][0] == State402.SW_MASK['FAULT'][1]
274+
bitmask, bits = State402.SW_MASK['FAULT']
275+
return self.statusword & bitmask == bits
258276

259-
def homing(self, timeout=30, set_new_home=True):
277+
def homing(self, timeout=TIMEOUT_HOMING_DEFAULT, set_new_home=True):
260278
"""Function to execute the configured Homing Method on the node
261279
:param int timeout: Timeout value (default: 30)
262280
:param bool set_new_home: Defines if the node should set the home offset
@@ -271,23 +289,24 @@ def homing(self, timeout=30, set_new_home=True):
271289
self.state = 'OPERATION ENABLED'
272290
homingstatus = 'IN PROGRESS'
273291
self.controlword = State402.CW_OPERATION_ENABLED | Homing.CW_START
274-
t = time.time() + timeout
292+
t = time.monotonic() + timeout
275293
try:
276294
while homingstatus not in ('TARGET REACHED', 'ATTAINED'):
277295
for key, value in Homing.STATES.items():
278-
# check if the value after applying the bitmask (value[0])
279-
# corresponds with the value[1] to determine the current status
280-
bitmaskvalue = self.statusword & value[0]
281-
if bitmaskvalue == value[1]:
296+
# check if the Statusword after applying the bitmask
297+
# corresponds with the needed bits to determine the current status
298+
bitmask, bits = value
299+
if self.statusword & bitmask == bits:
282300
homingstatus = key
283-
if homingstatus in ('INTERRUPTED', 'ERROR VELOCITY IS NOT ZERO', 'ERROR VELOCITY IS ZERO'):
284-
raise RuntimeError ('Unable to home. Reason: {0}'.format(homingstatus))
285-
time.sleep(0.001)
286-
if time.time() > t:
301+
if homingstatus in ('INTERRUPTED', 'ERROR VELOCITY IS NOT ZERO',
302+
'ERROR VELOCITY IS ZERO'):
303+
raise RuntimeError('Unable to home. Reason: {0}'.format(homingstatus))
304+
time.sleep(self.INTERVAL_CHECK_STATE)
305+
if time.monotonic() > t:
287306
raise RuntimeError('Unable to home, timeout reached')
288307
if set_new_home:
289308
actual_position = self.sdo[0x6063].raw
290-
self.sdo[0x607C].raw = actual_position # home offset (0x607C)
309+
self.sdo[0x607C].raw = actual_position # Home Offset
291310
logger.info('Homing offset set to {0}'.format(actual_position))
292311
logger.info('Homing mode carried out successfully.')
293312
return True
@@ -334,16 +353,16 @@ def op_mode(self, mode):
334353
start_state = self.state
335354

336355
if self.state == 'OPERATION ENABLED':
337-
self.state = 'SWITCHED ON'
356+
self.state = 'SWITCHED ON'
338357
# ensure the node does not move with an old value
339358
self._clear_target_values() # Shouldn't this happen before it's switched on?
340359

341360
# operation mode
342361
self.sdo[0x6060].raw = OperationMode.NAME2CODE[mode]
343362

344-
timeout = time.time() + 0.5 # 500 ms
363+
timeout = time.monotonic() + self.TIMEOUT_SWITCH_OP_MODE
345364
while self.op_mode != mode:
346-
if time.time() > timeout:
365+
if time.monotonic() > timeout:
347366
raise RuntimeError(
348367
"Timeout setting node {0}'s new mode of operation to {1}.".format(
349368
self.id, mode))
@@ -354,7 +373,7 @@ def op_mode(self, mode):
354373
logger.warning('{0}'.format(str(e)))
355374
finally:
356375
self.state = start_state # why?
357-
logger.info('Set node {n} operation mode to {m}.'.format(n=self.id , m=mode))
376+
logger.info('Set node {n} operation mode to {m}.'.format(n=self.id, m=mode))
358377
return False
359378

360379
def _clear_target_values(self):
@@ -421,10 +440,8 @@ def state(self):
421440
- 'QUICK STOP ACTIVE'
422441
"""
423442
for state, mask_val_pair in State402.SW_MASK.items():
424-
mask = mask_val_pair[0]
425-
state_value = mask_val_pair[1]
426-
masked_value = self.statusword & mask
427-
if masked_value == state_value:
443+
bitmask, bits = mask_val_pair
444+
if self.statusword & bitmask == bits:
428445
return state
429446
return 'UNKNOWN'
430447

@@ -442,14 +459,14 @@ def state(self, target_state):
442459
:raise RuntimeError: Occurs when the time defined to change the state is reached
443460
:raise ValueError: Occurs when trying to execute a ilegal transition in the sate machine
444461
"""
445-
timeout = time.time() + 0.8 # 800 ms
462+
timeout = time.monotonic() + self.TIMEOUT_SWITCH_STATE_FINAL
446463
while self.state != target_state:
447464
next_state = self._next_state(target_state)
448465
if self._change_state(next_state):
449-
continue
450-
if time.time() > timeout:
466+
continue
467+
if time.monotonic() > timeout:
451468
raise RuntimeError('Timeout when trying to change state')
452-
time.sleep(0.01) # 10 ms
469+
time.sleep(self.INTERVAL_CHECK_STATE)
453470

454471
def _next_state(self, target_state):
455472
if target_state == 'OPERATION ENABLED':
@@ -463,9 +480,9 @@ def _change_state(self, target_state):
463480
except KeyError:
464481
raise ValueError(
465482
'Illegal state transition from {f} to {t}'.format(f=self.state, t=target_state))
466-
timeout = time.time() + 0.4 # 400 ms
483+
timeout = time.monotonic() + self.TIMEOUT_SWITCH_STATE_SINGLE
467484
while self.state != target_state:
468-
if time.time() > timeout:
485+
if time.monotonic() > timeout:
469486
return False
470-
time.sleep(0.01) # 10 ms
487+
time.sleep(self.INTERVAL_CHECK_STATE)
471488
return True

0 commit comments

Comments
 (0)