Please Help Test EasyGoPiGo3() methods with timeouts

I found Carl mindlessly sitting on his dock, long after he was supposed to drive off. When I touched him, he snapped out of his stupor.

Last Recharge:  2020-06-16 07:06|[new_juicer.py.undock]
 ---- Dismount 1211 at 9.5 v after 6.9 h recharge

It appears that a drive_cm(blocking=True) had failed to register as complete, and Carl was mindlessly checking his toenails for fungus, (or some other unexpected useless processor cycles).

I have created an EasyGoPiGo3() class with optional timeouts on:

  • def drive_cm(self, dist, blocking=True, timeout= 60)
  • def drive_inches(self, dist, blocking=True, timeout= 60)
  • def orbit(self, degrees, radius_cm=0, blocking=True, timeout= 60)
  • def reset_encoders(self, blocking=True, timeout= 60)
  • def turn_degrees(self, degrees, blocking=True, timeout= 60)

My version of EasyGoPiGo3() class and a sample test program are on github here.

Bring them down with:

curl -O https://github.com/slowrunner/Carl/blob/master/Projects/safeDrive/my_easygopigo3.py
curl -O https://github.com/slowrunner/Carl/blob/master/Projects/safeDrive/test.py

I would love help testing these functions. Here is an example using the timeout feature:

#!/usr/bin/env python3

import time
from my_easygopigo3 import EasyGoPiGo3

def main():
    egpg = EasyGoPiGo3(use_mutex=True)

    print("\n50cm with .25sec timeout TEST DRIVE WILL BEGIN IN 5 SECONDS")
    time.sleep(5)
    egpg.drive_cm(50, timeout=0.25)

    print("\nTEST COMPLETE")
2 Likes

I have had absolutely ZERO success with timeouts - they all go brain-dead on me - so take anything I say with a 20-ton grain of salt.

The one thing that jumps out at me is the combination of “blocking = TRUE” and “timeout = [some value]”  IMHO, they are mutually exclusive, right?  If “blocking = TRUE” then “timeout =” will never be seen because the process is blocking, waiting for completion.

I’m curious as to what you find.

1 Like

The defaults, when only a distance is passed, are to wait until either complete or time since started passes 60 seconds. It waits in 0.1 second sleeps. The while loop actually uses a Boolean principle Demorgan’s laws that “not A and not B” equals “not(A or B)”

When distance and a non-default timeout are passed, (so blocking defaults to True) it will wait until complete but only for time-out seconds. If the time-out is too short,the timeout will not allow completion, so most folks won’t use or notice the time-out, but will be protected. There is a case that happens every once in a while that the driving part of the command executed by the gopigo3.py code completes, but the easygopigo3.py code is waiting for a computed encoder value that will never be reached, and the user’s Bot will appear to have died. It will block until the user gives the bot a nudge that bumps the encoder in the right direction.

That is the case the time-out is designed to catch. The reason the time-out is a parm and not hard coded is to allow users to drive for very long distances or slow speeds for longer than 60 seconds. To drive for longer than 60 seconds the user must pass “timeout=120” or something bigger than 60 seconds.

1 Like

You have my curiosity riz. Silly me, I always thought that “blocking” meant “the rest of you guys can just go and pound sand until I decide, (if ever), that I am finished.”

How do you implement “cooperative” blocking? (As in “blocking, but I really didn’t mean ABSOLUTELY blocking.”)

A brief blocked-out example would be great, so I can grasp the idea.

Please accept my apologies for my obvious programming stupidity.

Thanks!

No problem. Here is the DI drive_cm() signature and the “blocking = True” wait:

def drive_cm(self, dist, blocking=True):
...
    if blocking:
            while self.target_reached(
                    StartPositionLeft + WheelTurnDegrees,
                    StartPositionRight + WheelTurnDegrees) is False:
                time.sleep(0.1)

So blocking is “on” by default, and it waits for both the left and right encoders to be +/- 5 clicks from the target amount.

Now here is my_easygopigo3 implementation of drive_cm() definition and the wait loop:

def drive_cm(self, dist, blocking=True, timeout=60):
...
        if blocking:
            while self.target_reached(
                    StartPositionLeft + WheelTurnDegrees,
                    StartPositionRight + WheelTurnDegrees) is False:
                time.sleep(0.1)
                if (time.time() - startTime) > timeout:
                    self.stop()
                    debug("drive_cm() timeout occured")
                    break

Let’s walk through this step by step:

  1. Added a new parameter “timeout” with the default value of 60 seconds if not passed by user
  2. The test for “reached both encoder target values” stands as is per the original -
    so if blocking was requested (the default), and the target is reached,
    the while loop does not need to execute,
    thus the method returns to the user’s program
  3. But if blocking was requested (again the default), and the target has not been reached yet,
    First wait 0.1 seconds
    then check how long we have been waiting, and if longer than the allowed timeout (default 60 secs)
    then we tell the motors to stop - may already have stopped, but just to be sure call stop()
    and break out of the while loop that is waiting,
    which will cause an “early” return from the method.

In the original DI code,

  • if the motors had stopped because the gopigo3 hardware read the encoders once each as reaching their targets,
  • and it has been long enough for the bot to stop moving after the motors shut off,
  • and perhaps the bot continued to roll a little long or bounced back a little too much
    during the 0.1 second sleep of the “watcher”
    we might have to wait till the batteries run down and beyond,
    for both encoders to finally get to or return to the +/- 5 click targets
     

But in the new “my” version:

  • after waking up from that 0.1 second sleep, we’re going to check the current time to see
  • if we’ve been waiting longer than the timeout, and
    if so “we’re out a here (early)”, “do not pass go”, “do not collect one at-a-boy”

So this change will not affect most users:
Example with default EasyGoPiGo3() initialized speed of 300 dps which is roughly 174 mm_per_sec

  • drive_cm(100) # drive 1m which will take 5.8 seconds, stop, and then return to users program
  • drive_cm(600) # drive 6 meters will take 29 seconds, stop and return
    (This is actually a bad idea because the bot should not be sleep walking. It should be checking to make sure the dog did not notice the bot moving and come sit in front to play.)
     
    but if a non-pet-owning-user sets the speed to 150 (dps) with egpg.set_speed(150) like I do
    so starts and stops are more gentle and turns are more accurate,
    then we start to see the timeout getting in the way on that long run:

First let’s verify how far “Carl” goes each second at 150 deg_per_sec:

    distance_in_1_second = 150 deg_per_second / 360 deg_per_wheel_rev * wheel_circumference
    wheel_circumference = pi * wheel_diameter
or
    distance_in_1_second_at_150dps = (150/360*pi*66.5) ~ 87 mm_per_second

so

  • drive_cm(100) # drive 1m will take 11.5 seconds, stop, and
    then return to users program no prob

but

  • drive_cm(600) # drive 6 meters which will should take 69 seconds,
    but will timeout at 60 seconds having only traveled 5.2 meters
     

so to accomplish the desired 6m run, the user must extend the timeout:

  • drive_cm(600, timeout=120) # allow two minutes to go the 6m

 

Summary

  • Change is invisible to most users
  • Change will ensure bots do not sit mindlessly forever in the infrequent special situation
  • If users complain the bot is driving accurately up to some very long drive, but always stopping after 1 minute:
    we suggest they add “timeout=120” to their drive_cm(), drive_inches(), orbit, turn_degrees()
    or better yet stop sleep walking.
2 Likes

Thought I’d see how reality might be - test for drive_cm(100) took 12 seconds when timed by watching the bot move and clicking the start/stop of my stopwatch.

Close enough.

1 Like

Nice to see this implemented! It was on my list of things to do, eventually.
it wasn’t considered a high priority change, mostly because it’s hard to explain to new learners but, if you’re okay with it, I would merge your changes into the main EasyGoPiGo3 library.

Cleo

1 Like

I hear ya there. I don’t have a problem with merging it - my worry is that it might not be bullet proof.

I have tested it by forcing it to drive longer than the timeout, but the actual condition it is designed to protect against is not easily reproducible. The low level drive commands have to achieve the target but the higher level “waiting routine” has to not be looking when the targets are met and the bot either has one wheel short of the target range or it achieved it but rolls out or bounces back out of the target tolerance. I don’t know how to force this to test it.