66
77logger = logging .getLogger (__name__ )
88
9-
109class 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-
141138class 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 .
190+ """Configure the state machine by searching for a TPDO that has the
191+ StatusWord mapped .
201192 :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 complete 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 supported 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 illegal transition in the state 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 timeout
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 milliseconds 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