Skip to content

Commit 0d2b4b6

Browse files
acolombaf-silva
andauthored
DS402: Replace delay loops with waiting for TPDO reception. (#263)
* ds402: Increase delay to check status on homing start. The Statusword is examined immediately after setting the Controlword command to start homing. That is very likely to fail because of the round-trip time until the Statusword is actually updated from a TPDO. To work around that, the delay between each check of the Statusword should be moved before the actual comparison, and its default value increased. Introduce a new constant TIMEOUT_CHECK_HOMING to configure that with a default value of 100 ms. This replaces the previously used INTERVAL_CHECK_STATE which is only 10 ms by default. An even better solution would be to wait for the Statusword to be updated by a received PDO, but that would be much more complex. * Apply interval to is_homed() method as well. Same problem as in the homing() method, PDO updates of the Statusword need at least one SYNC / PDO cycle duration. * Factor out common _homing_status() method. Move the common code from is_homed() and homing() to a method for internal use. Add a comment why the delay is necessary and how it should possibly be replaced by an RPDO reception check. Should the latter be implemented, there will be only one place to change it. * p402: Add blocking method check_statusword(). In contrast to the property getter, this method will not simply return the cached last value, but actually wait for an updated TPDO reception. If no TPDO is configured, or it is not expected periodically, the usual statusword getter takes over and returns either the last received value or queries via SDO as a fallback. The timeout for receiving a TPDO can be configured individually per call, defaulting to 0.2 seconds (TIMEOUT_CHECK_TPDO). * p402: Wait for TPDO instead of timed statusword checking. Several methods loop around waiting for some expected statusword value, usually calling time.sleep() with the INTERVAL_CHECK_STATE or INTERVAL_CHECK_HOMING constants. Replace that with a call to check_statusword(), which will only block until the TPDO is received. This allows for possibly shorter delays, because the delay can be cut short when the TPDO arrives in between. But rate limiting the checking loops is still achieved through the blocking behavior of check_statusword(). If no TPDO is configured, the statusword will fall back to checking via SDO, which will also result in periodic checks, but only rate limited by the SDO round-trip time. Anyway this is not a recommended mode of operation, as the warning in _check_statusword_configured() points out. * p402: Fix wrong reports of INTERRUPTED homing procedure. While the statusword checks work properly now, there is no delay anymore to make sure the controlword RPDO which starts the homing procedure is already received before we start checking the homing status. So the first reading might easily return INTERRUPTED, meaning that the controlword change has just not been applied yet. Add one extra call for check_statusword() to wait for one extra cycle in case of periodic transmissions. In effect, the success checking logic starts looking at the second received statusword after setting the controlword. Co-authored-by: André Filipe Silva <[email protected]>
1 parent 7462d0b commit 0d2b4b6

File tree

1 file changed

+41
-17
lines changed

1 file changed

+41
-17
lines changed

canopen/profiles/p402.py

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -293,13 +293,24 @@ def reset_from_fault(self):
293293
while self.is_faulted():
294294
if time.monotonic() > timeout:
295295
break
296-
time.sleep(self.INTERVAL_CHECK_STATE)
296+
self.check_statusword()
297297
self.state = 'OPERATION ENABLED'
298298

299299
def is_faulted(self):
300300
bitmask, bits = State402.SW_MASK['FAULT']
301301
return self.statusword & bitmask == bits
302302

303+
def _homing_status(self):
304+
"""Interpret the current Statusword bits as homing state string."""
305+
# Wait to make sure a TPDO was received
306+
self.check_statusword()
307+
status = None
308+
for key, value in Homing.STATES.items():
309+
bitmask, bits = value
310+
if self.statusword & bitmask == bits:
311+
status = key
312+
return status
313+
303314
def is_homed(self, restore_op_mode=False):
304315
"""Switch to homing mode and determine its status.
305316
@@ -310,12 +321,8 @@ def is_homed(self, restore_op_mode=False):
310321
previous_op_mode = self.op_mode
311322
if previous_op_mode != 'HOMING':
312323
logger.info('Switch to HOMING from %s', previous_op_mode)
313-
self.op_mode = 'HOMING'
314-
homingstatus = None
315-
for key, value in Homing.STATES.items():
316-
bitmask, bits = value
317-
if self.statusword & bitmask == bits:
318-
homingstatus = key
324+
self.op_mode = 'HOMING' # blocks until confirmed
325+
homingstatus = self._homing_status()
319326
if restore_op_mode:
320327
self.op_mode = previous_op_mode
321328
return homingstatus in ('TARGET REACHED', 'ATTAINED')
@@ -335,17 +342,14 @@ def homing(self, timeout=TIMEOUT_HOMING_DEFAULT, restore_op_mode=False):
335342
self.op_mode = 'HOMING'
336343
# The homing process will initialize at operation enabled
337344
self.state = 'OPERATION ENABLED'
338-
homingstatus = 'IN PROGRESS'
339-
self.controlword = State402.CW_OPERATION_ENABLED | Homing.CW_START
345+
homingstatus = 'UNKNOWN'
346+
self.controlword = State402.CW_OPERATION_ENABLED | Homing.CW_START # does not block
347+
# Wait for one extra cycle, to make sure the controlword was received
348+
self.check_statusword()
340349
t = time.monotonic() + timeout
341350
try:
342351
while homingstatus not in ('TARGET REACHED', 'ATTAINED'):
343-
for key, value in Homing.STATES.items():
344-
# check if the Statusword after applying the bitmask
345-
# corresponds with the needed bits to determine the current status
346-
bitmask, bits = value
347-
if self.statusword & bitmask == bits:
348-
homingstatus = key
352+
homingstatus = self._homing_status()
349353
if homingstatus in ('INTERRUPTED', 'ERROR VELOCITY IS NOT ZERO',
350354
'ERROR VELOCITY IS ZERO'):
351355
raise RuntimeError('Unable to home. Reason: {0}'.format(homingstatus))
@@ -463,6 +467,26 @@ def statusword(self):
463467
logger.warning('The object 0x6041 is not a configured TPDO, fallback to SDO')
464468
return self.sdo[0x6041].raw
465469

470+
def check_statusword(self, timeout=None):
471+
"""Report an up-to-date reading of the statusword (0x6041) from the device.
472+
473+
If the TPDO with the statusword is configured as periodic, this method blocks
474+
until one was received. Otherwise, it uses the SDO fallback of the ``statusword``
475+
property.
476+
477+
:param timeout: Maximum time in seconds to wait for TPDO reception.
478+
:raises RuntimeError: Occurs when the given timeout expires without a TPDO.
479+
:return: Updated value of the ``statusword`` property.
480+
:rtype: int
481+
"""
482+
if 0x6041 in self.tpdo_pointers:
483+
pdo = self.tpdo_pointers[0x6041].pdo_parent
484+
if pdo.is_periodic:
485+
timestamp = pdo.wait_for_reception(timeout or self.TIMEOUT_CHECK_TPDO)
486+
if timestamp is None:
487+
raise RuntimeError('Timeout waiting for updated statusword')
488+
return self.statusword
489+
466490
@property
467491
def controlword(self):
468492
"""Send a state change command using PDO or SDO.
@@ -518,7 +542,7 @@ def state(self, target_state):
518542
continue
519543
if time.monotonic() > timeout:
520544
raise RuntimeError('Timeout when trying to change state')
521-
time.sleep(self.INTERVAL_CHECK_STATE)
545+
self.check_statusword()
522546

523547
def _next_state(self, target_state):
524548
if target_state == 'OPERATION ENABLED':
@@ -536,5 +560,5 @@ def _change_state(self, target_state):
536560
while self.state != target_state:
537561
if time.monotonic() > timeout:
538562
return False
539-
time.sleep(self.INTERVAL_CHECK_STATE)
563+
self.check_statusword()
540564
return True

0 commit comments

Comments
 (0)