Skip to content

Commit e92a4e4

Browse files
committed
small 402 refactor
Some small changes to p402.py to make it a little easier to understand. I have two open questions about setup_402_state_machine(): why is it necessary to enter the "Pre-Operational" NMT state to read the pdo configurations. And why change state to "Switch on Disabled"? Shouldn't the present state be kept?
1 parent d4e7c71 commit e92a4e4

File tree

1 file changed

+116
-111
lines changed

1 file changed

+116
-111
lines changed

canopen/profiles/p402.py

Lines changed: 116 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
66

77
logger = logging.getLogger(__name__)
88

9-
109
class State402(object):
11-
12-
# Control word 0x6040 commands
10+
# Controlword (0x6040) commands
1311
CW_OPERATION_ENABLED = 0x0F
1412
CW_SHUTDOWN = 0x06
1513
CW_SWITCH_ON = 0x07
@@ -47,7 +45,7 @@ class State402(object):
4745
'QUICK STOP ACTIVE' : [0x6F, 0x07]
4846
}
4947

50-
# Transition path to enable the DS402 node
48+
# Transition path to get to the 'OPERATION ENABLED' state
5149
NEXTSTATE2ENABLE = {
5250
('START') : 'NOT READY TO SWITCH ON',
5351
('FAULT', 'NOT READY TO SWITCH ON') : 'SWITCH ON DISABLED',
@@ -137,9 +135,7 @@ class OperationMode(object):
137135
'INTERPOLATED POSITION' : 0x40
138136
}
139137

140-
141138
class Homing(object):
142-
143139
CW_START = 0x10
144140
CW_HALT = 0x100
145141

@@ -187,65 +183,73 @@ class BaseNode402(RemoteNode):
187183

188184
def __init__(self, node_id, object_dictionary):
189185
super(BaseNode402, self).__init__(node_id, object_dictionary)
190-
191-
self.is_statusword_configured = False
192-
193-
#: List of values obtained by the configured TPDOs in a dictionary {object (hex), value}
194-
self.tpdo_values = {}
195-
#! list of mapped objects configured in the RPDOs in a dictionary {object (hex, pointer (RPDO object) }
196-
self.rpdo_pointers = {}
186+
self.tpdo_values = dict() # { index: TPDO_value }
187+
self.rpdo_pointers = dict() # { index: RPDO_pointer }
197188

198189
def setup_402_state_machine(self):
199-
"""Configured the state machine by searching for the PDO that has the
200-
StatusWord mappend.
201-
:raise ValueError: If the the node can't finde a Statusword configured
190+
"""Configure the state machine by searching for a TPDO that has the
191+
StatusWord mapped.
192+
:raise ValueError: If the the node can't find a Statusword configured
202193
in the any of the TPDOs
203194
"""
204-
# the node needs to be in pre-operational mode
205-
self.nmt.state = 'PRE-OPERATIONAL'
206-
self.pdo.read() # read all the PDOs (TPDOs and RPDOs)
207-
#
195+
self.nmt.state = 'PRE-OPERATIONAL' # Why is this necessary?
196+
self.setup_pdos()
197+
self._check_controlword_configured()
198+
self._check_statusword_configured()
199+
self.nmt.state = 'OPERATIONAL'
200+
self.state = 'SWITCH ON DISABLED' # Why change state?
201+
202+
def setup_pdos(self):
203+
self.pdo.read() # TPDO and RPDO configurations
204+
self._init_tpdo_values()
205+
self._init_rpdo_pointers()
206+
207+
def _init_tpdo_values(self):
208208
for tpdo in self.tpdo.values():
209209
if tpdo.enabled:
210210
tpdo.add_callback(self.on_TPDOs_update_callback)
211211
for obj in tpdo:
212212
logger.debug('Configured TPDO: {0}'.format(obj.index))
213213
if obj.index not in self.tpdo_values:
214214
self.tpdo_values[obj.index] = 0
215-
#
215+
216+
def _init_rpdo_pointers(self):
217+
# If RPDOs have overlapping indecies, rpdo_pointers will point to
218+
# the first RPDO that has that index configured.
216219
for rpdo in self.rpdo.values():
217220
for obj in rpdo:
218221
logger.debug('Configured RPDO: {0}'.format(obj.index))
219222
if obj.index not in self.rpdo_pointers:
220-
self.rpdo_pointers[obj.index] = obj
223+
self.rpdo_pointers[obj.index] = obj
221224

222-
# Check if the Controlword is configured
223-
if 0x6040 not in self.rpdo_pointers:
224-
logger.warning('Controlword not configured in the PDOs of this node, using SDOs to set Controlword')
225+
def _check_controlword_configured(self):
226+
if 0x6040 not in self.rpdo_pointers: # Controlword
227+
logger.warning(
228+
"Controlword not configured in node {0}'s PDOs. Using SDOs can cause slow performance.".format(
229+
self.id))
225230

226-
# Check if the Statusword is configured
227-
if 0x6041 not in self.tpdo_values:
228-
raise ValueError('Statusword not configured in this node. Unable to access node status.')
229-
230-
# Set nmt state and set the DS402 not to switch on disabled
231-
self.nmt.state = 'OPERATIONAL'
232-
self.state = 'SWITCH ON DISABLED'
231+
def _check_statusword_configured(self):
232+
if 0x6041 not in self.tpdo_values: # Statusword
233+
raise ValueError(
234+
"Statusword not configured in node {0}'s PDOs. Using SDOs can cause slow performance.".format(
235+
self.id))
233236

234237
def reset_from_fault(self):
235238
"""Reset node from fault and set it to Operation Enable state
236239
"""
237240
if self.state == 'FAULT':
238-
# particular case, it resets the Fault Reset bit (rising edge 0 -> 1)
241+
# Resets the Fault Reset bit (rising edge 0 -> 1)
239242
self.controlword = State402.CW_DISABLE_VOLTAGE
240-
timeout = time.time() + 0.4 # 400 milliseconds
241-
# Check if the Fault Reset bit is still = 1
242-
while self.statusword & (State402.SW_MASK['FAULT'][0] == State402.SW_MASK['FAULT'][1]):
243+
timeout = time.time() + 0.4 # 400 ms
244+
245+
while self.is_faulted():
243246
if time.time() > timeout:
244247
break
245-
time.sleep(0.01) # 10 milliseconds
248+
time.sleep(0.01) # 10 ms
246249
self.state = 'OPERATION ENABLED'
247-
else:
248-
logger.info('The node its not at fault. Doing nothing!')
250+
251+
def is_faulted(self):
252+
return self.statusword & State402.SW_MASK['FAULT'][0] == State402.SW_MASK['FAULT'][1]
249253

250254
def homing(self, timeout=30, set_new_home=True):
251255
"""Function to execute the configured Homing Method on the node
@@ -255,8 +259,7 @@ def homing(self, timeout=30, set_new_home=True):
255259
:return: If the homing was complet with success
256260
:rtype: bool
257261
"""
258-
result = False
259-
previus_opm = self.op_mode
262+
previus_op_mode = self.op_mode
260263
self.state = 'SWITCHED ON'
261264
self.op_mode = 'HOMING'
262265
# The homing process will initialize at operation enabled
@@ -278,16 +281,16 @@ def homing(self, timeout=30, set_new_home=True):
278281
if time.time() > t:
279282
raise RuntimeError('Unable to home, timeout reached')
280283
if set_new_home:
281-
offset = self.sdo[0x6063].raw
282-
self.sdo[0x607C].raw = offset
283-
logger.info('Homing offset set to {0}'.format(offset))
284+
actual_position = self.sdo[0x6063].raw
285+
self.sdo[0x607C].raw = actual_position # home offset (0x607C)
286+
logger.info('Homing offset set to {0}'.format(actual_position))
284287
logger.info('Homing mode carried out successfully.')
285-
result = True
288+
return True
286289
except RuntimeError as e:
287290
logger.info(str(e))
288291
finally:
289-
self.op_mode = previus_opm
290-
return result
292+
self.op_mode = previus_op_mode
293+
return False
291294

292295
@property
293296
def op_mode(self):
@@ -316,46 +319,51 @@ def op_mode(self, mode):
316319
- 'CYCLIC SYNCHRONOUS TORQUE'
317320
- 'OPEN LOOP SCALAR MODE'
318321
- 'OPEN LOOP VECTOR MODE'
319-
320322
"""
321323
try:
322-
logger.info('Changing Operation Mode to {0}'.format(mode))
323-
state = self.state
324-
result = False
325-
326324
if not self.is_op_mode_supported(mode):
327-
raise TypeError('Operation mode not suppported by the node.')
325+
raise TypeError(
326+
'Operation mode {0} not suppported on node {1}.'.format(mode, self.id))
327+
328+
start_state = self.state
328329

329330
if self.state == 'OPERATION ENABLED':
330-
self.state = 'SWITCHED ON'
331-
# to make sure the node does not move with a old value in another mode
332-
# we clean all the target values for the modes
333-
self.sdo[0x60FF].raw = 0.0 # target velocity
334-
self.sdo[0x607A].raw = 0.0 # target position
335-
self.sdo[0x6071].raw = 0.0 # target torque
336-
# set the operation mode in an agnostic way, accessing the SDO object by ID
331+
self.state = 'SWITCHED ON'
332+
# ensure the node does not move with an old value
333+
self._clear_target_values() # Shouldn't this happen before it's switched on?
334+
335+
# operation mode
337336
self.sdo[0x6060].raw = OperationMode.NAME2CODE[mode]
338-
t = time.time() + 0.5 # timeout
337+
338+
timeout = time.time() + 0.5 # 500 ms
339339
while self.op_mode != mode:
340-
if time.time() > t:
341-
raise RuntimeError('Timeout setting the new mode of operation at node {0}.'.format(self.id))
342-
result = True
340+
if time.time() > timeout:
341+
raise RuntimeError(
342+
"Timeout setting node {0}'s new mode of operation to {1}.".format(
343+
self.id, mode))
344+
return True
343345
except SdoCommunicationError as e:
344346
logger.warning('[SDO communication error] Cause: {0}'.format(str(e)))
345347
except (RuntimeError, ValueError) as e:
346348
logger.warning('{0}'.format(str(e)))
347349
finally:
348-
self.state = state # set to last known state
349-
logger.info('Mode of operation of the node {n} is {m}.'.format(n=self.id , m=mode))
350-
return result
350+
self.state = start_state # why?
351+
logger.info('Set node {n} operation mode to {m}.'.format(n=self.id , m=mode))
352+
return False
353+
354+
def _clear_target_values(self):
355+
# [target velocity, target position, target torque]
356+
for target_index in [0x60FF, 0x607A, 0x6071]:
357+
if target_index in self.sdo.keys():
358+
self.sdo[target_index].raw = 0
351359

352360
def is_op_mode_supported(self, mode):
353361
"""Function to check if the operation mode is supported by the node
354362
:param int mode: Operation mode
355363
:return: If the operation mode is supported
356364
:rtype: bool
357365
"""
358-
mode_support = (self.sdo[0x6502].raw & OperationMode.SUPPORTED[mode])
366+
mode_support = self.sdo[0x6502].raw & OperationMode.SUPPORTED[mode]
359367
return mode_support == OperationMode.SUPPORTED[mode]
360368

361369
def on_TPDOs_update_callback(self, mapobject):
@@ -380,11 +388,11 @@ def statusword(self):
380388

381389
@property
382390
def controlword(self):
383-
raise RuntimeError('This property has no getter.')
391+
raise RuntimeError('The Controlword is write-only.')
384392

385393
@controlword.setter
386394
def controlword(self, value):
387-
"""Helper function enabling the node to send the state using PDO or SDO objects
395+
"""Send the state using PDO or SDO objects.
388396
:param int value: State value to send in the message
389397
"""
390398
if 0x6040 in self.rpdo_pointers:
@@ -396,9 +404,7 @@ def controlword(self, value):
396404
@property
397405
def state(self):
398406
"""Attribute to get or set node's state as a string for the DS402 State Machine.
399-
400407
States of the node can be one of:
401-
402408
- 'NOT READY TO SWITCH ON'
403409
- 'SWITCH ON DISABLED'
404410
- 'READY TO SWITCH ON'
@@ -407,54 +413,53 @@ def state(self):
407413
- 'FAULT'
408414
- 'FAULT REACTION ACTIVE'
409415
- 'QUICK STOP ACTIVE'
416+
"""
417+
for state, mask_val_pair in State402.SW_MASK.items():
418+
mask = mask_val_pair[0]
419+
state_value = mask_val_pair[1]
420+
masked_value = self.statusword & mask
421+
if masked_value == state_value:
422+
return state
423+
return 'UNKNOWN'
410424

425+
@state.setter
426+
def state(self, target_state):
427+
""" Defines the state for the DS402 state machine
411428
States to switch to can be one of:
412-
413429
- 'SWITCH ON DISABLED'
414430
- 'DISABLE VOLTAGE'
415431
- 'READY TO SWITCH ON'
416432
- 'SWITCHED ON'
417433
- 'OPERATION ENABLED'
418434
- 'QUICK STOP ACTIVE'
419-
420-
"""
421-
for key, value in State402.SW_MASK.items():
422-
# check if the value after applying the bitmask (value[0])
423-
# corresponds with the value[1] to determine the current status
424-
bitmaskvalue = self.statusword & value[0]
425-
if bitmaskvalue == value[1]:
426-
return key
427-
return 'UNKNOWN'
428-
429-
@state.setter
430-
def state(self, new_state):
431-
""" Defines the state for the DS402 state machine
432-
:param string new_state: Target state
433-
:param int timeout:
435+
:param string target_state: Target state
434436
:raise RuntimeError: Occurs when the time defined to change the state is reached
435-
:raise TypeError: Occurs when trying to execute a ilegal transition in the sate machine
437+
:raise ValueError: Occurs when trying to execute a ilegal transition in the sate machine
436438
"""
437-
t_to_new_state = time.time() + 8 # 800 milliseconds tiemout
438-
while self.state != new_state:
439-
try:
440-
if new_state == 'OPERATION ENABLED':
441-
next_state = State402.next_state_for_enabling(self.state)
442-
else:
443-
next_state = new_state
444-
# get the code from the transition table
445-
code = State402.TRANSITIONTABLE[ (self.state, next_state) ]
446-
# set the control word
447-
self.controlword = code
448-
# timeout of 400 milliseconds to try set the next state
449-
t_to_next_state = time.time() + 0.4
450-
while self.state != next_state:
451-
if time.time() > t_to_next_state:
452-
break
453-
time.sleep(0.01) # 10 milliseconds of sleep
454-
except KeyError:
455-
raise ValueError('Illegal transition from {f} to {t}'.format(f=self.state, t=new_state))
456-
# check the timeout
457-
if time.time() > t_to_new_state:
439+
timeout = time.time() + 0.8 # 800 ms
440+
while self.state != target_state:
441+
next_state = self._next_state(target_state)
442+
if _change_state(next_state):
443+
continue
444+
if time.time() > timeout:
458445
raise RuntimeError('Timeout when trying to change state')
459-
time.sleep(0.01) # 10 miliseconds of sleep
446+
time.sleep(0.01) # 10 ms
460447

448+
def _next_state(self, target_state):
449+
if target_state == 'OPERATION ENABLED':
450+
return State402.next_state_for_enabling(self.state)
451+
else:
452+
return target_state
453+
454+
def _change_state(self, target_state):
455+
try:
456+
self.controlword = State402.TRANSITIONTABLE[(self.state, target_state)]
457+
except KeyError:
458+
raise ValueError(
459+
'Illegal state transition from {f} to {t}'.format(f=self.state, t=target_state))
460+
timeout = time.time() + 0.4 # 400 ms
461+
while self.state != target_state:
462+
if time.time() > timeout:
463+
return False
464+
time.sleep(0.01) # 10 ms
465+
return True

0 commit comments

Comments
 (0)