ev3dev / ev3dev-lang-python

Pure python bindings for ev3dev
MIT License
425 stars 144 forks source link

Multithreading Attachment Motors While Driving #695

Closed ghost closed 4 years ago

ghost commented 4 years ago
**ev3dev version: 4.14.96-ev3dev-2.3.2-ev3
**ev3dev-lang-python version:
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name           Version      Architecture Description
+++-==============-============-===================-==========================
ii  micropython-ev 2.0.0~beta5  all          Python language bindings for ev3d
ii  python3-ev3dev 1.2.0        all          Python language bindings for ev3d
ii  python3-ev3dev 2.0.0~beta5  all          Python language bindings for ev3d

WHAT'S GOING ON: My FLL team has a robot that has 2 large drive motors and 1 med and 1 large motor that run an elevator and shuttle (think 2 dimensional forklift). They tried threading the attachment motor operations using _thread library in Micropython, and the FLL robot didn't behave as intended when attempting to drive straight using MoveSteering.on and instead veered left, consistently about 10-15 cm over a 60 cm move.

BACKGROUND: The kids have programmed "action" functions like Forward(distance=60, speed=40, ramp_up=True, ramp_down=True, brake_on=True) to move the robot forward 50 cm at 40% speed, ramping up and down, and braking at the end, for example.

The function essentially uses a 20ms loop by checking the time since the last motor command, then checks the heading error, computes a steering input using a PID controller and then uses a MoveSteering command to adjust the steering, and ramps up the speed until the set speed, stays at the set speed until a motor position reading is hit, and then ramps down until the final motor position is hit. Then it brakes and then releases the brake.

The function drives the robot nice and straight when Forward is used alone.

At the same time, the kids want to have the attachment motors operate to move forklift shuttle up/down and left/right into position. This function is called MoveLift(move_to_X=5, move_to_y=7, speed=50) to move the lift up 7cm and right 5cm at 50% speed, for example.

The lift function uses LargeMotor/MediumMotor.on_to_position to do the task of moving the lift.

They attempted to thread the lift move (after importing start_new_thread from _threading) while driving to one of the mission models on the table.

This will save valuable seconds if they can use the time driving to move the lift.

The Forward function is pretty much like I described above. The Move_Lift is shorter and is included below:

#*********************************************************************
#*************                Move_Lift                   ************
#*********************************************************************
def Move_Lift(To_X, To_Y, Lift_Speed):

    # DESCRIPTION:
    # Move_Lift - Moves lift to position X, Y from original position at speed Lift_Speed

    # EXAMPLE:

    # Move the up/down motor (Large motor - reversed) and let the next command start
    Y_Motor.on_to_position(Lift_Speed, position = (144 * To_Y), brake=True, block=False)

    # Adjust the medium motor speed down so it moves as fast as the large motor    
    X_Motor.on_to_position(Lift_Speed * (1050/1560), position = (144 * To_X), brake=True, block=True)

    # Let the motors finish their move
    Y_Motor.wait_while('running')
    X_Motor.wait_while('running')

    # Find the final motor positions
    Lift_X = X_Motor.position / 144
    Lift_Y = Y_Motor.position / 144

    return
#enddef

To run the functions at the same time, the kids programmed this (having imported start_new_thread from _thread):

# Start a thread to move the lift - Move_Lift(To_X=5, To_Y=8, Speed=50)
start_new_thread(Move_Lift, (5, 8, 50))

# Drive to the mission model
Robot_State = Forward(Distance=60, Speed=50, Ramp_Up=True, Ramp_Down=True, Brake_On=True, Robot_State) 

IGNORANT AMATEUR IDEAS OF WHAT IS GOING ON: Here's my thoughts on what might be wrong (and I'm just guessing at things - none of what I say is based on much knowledge of Python, threading, EV3DEV or coding in general):

  1. The Forward function uses a loop to check the time and not a sleep command, and so the processor is constantly in operation and the threading of functions doesn't have a time slot to execute and that messes up the command of the motors or the timing of the PID loop.

  2. The MoveLift function puts a block=True on the second motor move command so it will wait until that command and prior motor move commands are finished before moving on causing a problem.

  3. The LargeMotor/MediumMotor.wait_while("running") is causing problems

  4. Our 20ms loop is too short and there just isn't enough processor time.

  5. EV3 just can't multithread well

I'm just looking for thoughts on something obvious we might have done wrong, or what the next step in troubleshooting we might take to figure out what is wrong.

I wanted to have the kids run the program a few times yesterday with full troubleshooting output on, but there wasn't time. I will get one of them to do it tomorrow so we can see the timing of the motor commands and position of each of the drive motors at each iteration of the loop, as well as what the PID is commanding for steering.

I need to have some idea of what might be going wrong so I can guide the kids in troubleshooting and teach "why things work the way they do."

Thanks in advance for any ideas to try out.

WasabiFan commented 4 years ago

Firstly, glad to hear some things are working well!

About this particular issue -- your guesses seem very reasonable. It isn't clear to me why this would be. Some initial suggestions/questions:

This sounds like it's lower-level than the Python library (e.g. motor drivers), but it might be that the initial motor starts aren't sufficiently synchronized due to the separate thread.

ghost commented 4 years ago

Thanks for your thoughts and suggestions.

I don't know if this is happening without threads.

I is constantly veering, which my FPGA guy at work tells me that it might mean the CPU duty cycle for sending commands is pretty near my loop time and if the motors are being commanded sequentially, then one command might be getting shorted a few milliseconds.

The lift motors are being called inside a function and one is non-blocking and the other is. The forward function uses loops at 20ms to command steering updates (and speed changes during the ramps).

Simple program summary:

start_new_thread(MoveLift, (args))
  [which calls MoveLift(args)
       Within MoveLift
           MotorX.on_to_position(block=False)
           MotorY.on_to_position(block=True)
           MotorX.wait_while(moving)
           MotorY.wait_while(moving)]

Forward(args)
    While final distance is not met
       Check if time>20ms
           Reset timer
           Check heading error
           Get steering input from PID
           MoveSteering(args) # block=False by default - correct???
       Endif
       Brake if distance measure is met
    Endwhile

Next Command
. . .

Plan for Testing:

  1. We will run the program as is and take data.
  2. We will run a simple program with just a MoveSteering and Motor.on_to_position commands threaded and take data.
  3. We will modify the Forward function to drive the lift motors inside the non-ramped part of the function using Motor.on_to_position commands (non-blocked)

I'll report back what we find. This is a big enough problem to have the kids put time on it to solve.

We have to build a new robot that is smaller and lighter for the next competition in January, so we can spend a day on test drudgery.

Thanks again,

Mike

WasabiFan commented 4 years ago

Sounds good. If you're repeatedly restarting the motors in your own loop, that might suggest the issue is truly caused by the CPU's inability to keep up. It would be interesting to delay your loop more so it only runs, say, every 50ms or 100ms and see if the problem is reduced.

rhempel commented 4 years ago

One additional suggestion is to not use MoveSteering when you are doing your own PID control - just apply the power in % directly to the motors for unregulated speed.

MoveSteering is usually OK if you set it and forget it but in a loop you are essentially restarting the regulation all the time.

ghost commented 4 years ago

Ralph - thanks for the tip. I assume that restarting the regulation all the time puts a delay in the motor response?

So, we would use something like this in our PID Loop:

while not Midcourse_Completed:

      . . .  code
      . . .  error computation and PID engine call

      if Steering_Correction > 0: # steer right by increasing left motor speed
           LeftMotor.on(speed=Target_Speed + Correction_Delta_V)
           RightMotor.on(speed=Target_Speed)

     if Steering correction < 0: # steer left by increase right motor speed
           RightMotor.on(speed=Target_Speed + Correction_Delta_V)
           LeftMotor.on(speed=Target_Speed)

      . . . remaining code

Where Correction_Delta_V is some amount proportional to our Steering_Correction coming out of our PID engine.

Thanks, Ralph, for providing your advice.

ghost commented 4 years ago

Kaelin - We did some tests last night - turns out there probably isn't a code or deeper problem.

We ran with and without threading our Move_Lift and then Forward.

It ran the same spot (more or less...it's an EV3) both ways, 5 times in a row.

I think it is a PID tuning problem and a design problem...not a code problem based on this.

We'll continue to test to see what's up.

  1. Reducing the cycle time may help everything.

  2. Changing the MoveSteering to directly driving each motor might improve overall behavior, per Ralph's advice.

Our robot is pretty heavy and we're using the ball bearing casters. They dig into the mat with a 1 kg robot. We are doing some design mods, so a heavy piece was off the front of the robot and that changed everything.

We were so busy that when it went all wonky, the kids decided they would just move on.

Thank you for giving us your thoughts on our issue...we'll do more testing next time before writing.

Kaelin, Ralph, and David:

You guys are awesome. The kids enjoy using Python instead of EV3-G and are really growing in sophistication.

My kids think you are all gods for responding to our noob questions.

Thank you.

ghost commented 4 years ago

Thanks for all the ideas.

This will be a great learning experience for the team.

ghost commented 4 years ago

The kids fixed the Forward function per your suggestions.

Running the motors directly and using the output of the PID to add/subtract speed from the right wheel made the robot drive the straightest it has ever driven.

Reducing the loop to to 50ms didn't affect the performance negatively and may have improved it.

WasabiFan commented 4 years ago

In general I'd prefer running loops tighter than 50ms, but in this case I don't think the EV3 CPU can actually achieve that, especially given all the context switching that has to be done.

ghost commented 4 years ago

We can try tightening the loops up.

They just rebuilt and skinnied up the robot and that will change the dynamics. They'll have to test a whole bunch.

Even 40ms loop would be a significant change.

On Thu, Dec 19, 2019, 11:58 PM Kaelin Laundry notifications@github.com wrote:

In general I'd prefer running loops tighter than 50ms, but in this case I don't think the EV3 CPU can actually achieve that, especially given all the context switching that has to be done.

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/ev3dev/ev3dev-lang-python/issues/695?email_source=notifications&email_token=AFBYYSVC4T4N6AKZKZ44UADQZRGGNA5CNFSM4JZYETIKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEHL4CSY#issuecomment-567787851, or unsubscribe https://github.com/notifications/unsubscribe-auth/AFBYYSWFMIO6Y56OTD2UBDLQZRGGNANCNFSM4JZYETIA .

ghost commented 4 years ago

@rhempel @dwalton76 @dlech @WasabiFan

Update on the end result of the advice in this thread: My team sends their thanks. They just finished their District FLL tournament and are advancing to State and won robot design, in large part to the programming in Python using EV3DEV. At least 2 other teams advancing from our tournament were using Python, but the Lego version.

The kids loosened their loops in their drive functions to 50ms, changed the PID to drive the right wheel directly rather than us MoveSteering, and multithreaded their lift commands and driving moves which bought back enough time to complete every mission but 2 on the table.

Thank you again for your willingness to help them understand how things work.

WasabiFan commented 4 years ago

Glad to hear it! Let us know if there's anything else we can help with, or suggestions for improvement.