Moving the Arm: Trajectories, Grippers, and Teleoperation¶
Summary¶
This chapter teaches how to make the arm move well. You will write joint-space moves, timed control loops, smooth trajectories, gripper control, and pick-and-place routines, and use a leader arm to teleoperate a follower. After this chapter, you will be able to program a repeatable pick-and-place task.
Concepts Covered¶
This chapter covers the following 28 concepts from the learning graph:
| Concept | Concept Impact Score |
|---|---|
| Time Module | 105 |
| Control Loop | 104 |
| Joint-Space Motion | 103 |
| Single-Joint Move | 97 |
| Multi-Joint Move | 96 |
| Waypoint | 96 |
| Waypoint List | 95 |
| Teleoperation Loop | 64 |
| Interpolation | 49 |
| Gripper Control | 40 |
| Trajectory | 32 |
| Pick and Place | 32 |
| Linear Interpolation | 11 |
| Acceleration Limit | 7 |
| Grasp Force | 5 |
| Trapezoidal Velocity Profile | 4 |
| Control Rate | 4 |
| Timing Jitter | 3 |
| Blocking vs Non-Blocking Code | 3 |
| Threads | 2 |
| Pre-Grasp Approach | 2 |
| Latency | 2 |
| Smooth Velocity Profile | 1 |
| Stop Flag | 1 |
| Gripper Width | 1 |
| Lift and Retreat | 1 |
| State Machine | 1 |
| Leader-Follower Mapping | 1 |
Prerequisites¶
This chapter builds on concepts from:
- Chapter 1: Setting Up Python for Robotics
- Chapter 2: Anatomy of a Robot Arm
- Chapter 3: Electricity, Power, and Safety Basics
- Chapter 5: Actuators and Sensors
- Chapter 8: Building and Calibrating the SO-ARM100
- Chapter 10: A Python Hardware Library for Robot Arms
Time to Move Me, Smoothly!
Until now I could read my joints and jump to a pose. In this chapter you will teach me to move: at a steady rhythm, in smooth curves, through a list of stops, and all the way to picking something up and putting it down. Let's move it!
The Arm class of Chapter 10 can say where the joints are and send them a target. If you ask it to go from 0 to 90 degrees, it sends 90 and the servo races there as fast as it can, with a jolt at the start and a jolt at the end. That is a fine way to test a joint and a poor way to carry a full cup of tea. Good motion needs three more things: a sense of time, so that the program does something many times a second at a steady pace; a plan, so that the joint moves in a smooth curve; and a routine, so that many such moves add up to a useful task.
The chapter follows that order. It starts with the loop that everything else rests on, the control loop that repeats at a fixed rate. Then it moves joints, one and then many, along straight and smooth paths. It adds the gripper and ties the pieces into a pick-and-place routine, and it ends with teleoperation, in which a person's hand on a leader arm is the plan. As always, every program runs first on the fake arm of Chapter 10, with no power, and Chapter 3's rule applies when you move to the real arm: first moves are small and slow.
Time and the Control Loop¶
The Time Module¶
The time module is Python's way of asking "what time is it?" and "wait a while". It has four functions that matter here, and picking the right one is the first skill of this chapter.
| Function | What it does | Use it for |
|---|---|---|
time.sleep(seconds) |
Pauses the program for about that long | Waiting between steps |
time.perf_counter() |
A clock with very fine resolution that only goes forward | Timing how long something takes |
time.monotonic() |
A clock that never goes backward, with coarser resolution | Timeouts and deadlines |
time.time() |
The wall-clock time, in seconds since 1970 | Time stamps in a log |
The wall clock, time.time(), can jump: the computer corrects its clock over the network, or the user changes the time zone. A control loop that measured its steps with it could see a step that lasted minus two seconds. The two other clocks cannot jump, so measure durations with time.perf_counter().
Here is a worked example that shows the limit of time.sleep. The code asks to sleep for 10 milliseconds, 20 times, and measures what really happened:
On a typical laptop the result is a little over 10 ms every time, and sometimes much more, because the operating system decides when your program runs again. The program asked for a minimum, and the computer promised only that it would not return early. The rest of this section is about building a loop that works anyway.
The Control Loop¶
A control loop is a piece of code that does three things over and over at a steady rate: read the state of the world, decide what to do, and write a command. For an arm, reading is read_pose(), deciding is "where should each joint be now?", and writing is move_to(). The PID controller inside a servo (Chapter 5) has the same shape, and it runs inside the motor much faster than your program can. Your loop runs on the computer, and 50 times a second is plenty for the arm in this book. A loop does not have to read first. The trajectory player later in this chapter only writes, and the teleoperation loop reads one arm and writes to another.
The tempting way to write a loop is to do the work and then sleep for one period. It has a hidden flaw: the work takes time too, so every cycle lasts the work plus the sleep. At 50 Hz the period is 20 ms. If the work takes 8 ms, each cycle takes 28 ms, and the loop runs at 36 Hz and not 50. The better way is to schedule each cycle from the start time: the nth cycle is due at start + n × period, and the loop sleeps only for the time that is left. The work then fits inside the period, and the rate is exact on average.
The last two lines handle the case where the work takes longer than the period. A loop that tried to catch up by rushing through the next cycles would send a burst of commands, so the version above skips ahead and accepts a lower rate. The lab turns this into the function run_loop.
A Loop Is a Promise About Time
A fixed-rate loop is a promise: "I will look at the world and answer, on schedule, every 20 milliseconds." Everything smooth about a robot's motion comes from keeping that promise. If the work cannot fit in the period, no cleverness will help, and the honest answer is a lower rate or less work per tick.
Control Rate, Timing Jitter, and Latency¶
The control rate is how many times per second the loop runs, in hertz (Hz), and the period is its inverse: \( \text{period} = 1 / \text{rate} \). A rate of 50 Hz has a period of 20 ms, and 60 Hz has a period of 16.7 ms. LeRobot's teleoperation command defaults to 60 frames per second, and its recording examples use 30. A higher rate gives smoother motion and a faster reaction, and it needs more work per second from the computer and the bus. At 1,000,000 baud a byte takes 10 microseconds (Chapter 4), so a short packet to one servo takes well under a millisecond, and for this arm the limit is usually the program and not the wires.
Timing jitter is the variation in the time between loop cycles. A loop at 50 Hz has a planned period of 20 ms, and some cycles last 19 ms and others 24 ms. A little jitter is harmless, since the motors smooth it out. A lot of it makes the motion stutter, and it grows when the computer is busy with something else. The lab measures the worst jitter of a loop.
Latency is the delay between something happening and the arm's response to it. In teleoperation, it is the time from the leader's joint moving to the follower's joint starting to move. It has three parts: the wait for the next loop cycle (up to one period), the time to read and write on the bus, and the time that the follower's own motor takes to react. At 50 Hz the first part alone is up to 20 ms.
The next MicroSim practises the arithmetic of loops. You predict the rate of a naive loop and of a scheduled loop for different amounts of work.
Diagram: Loop Rate Calculator¶
Run the Loop Rate Calculator MicroSim fullscreen
Loop Rate Calculator
Type: microsim
sim-id: loop-rate-calculator
Library: p5.js
Status: Specified
Bloom Level: Apply
Bloom Verb: calculate
Learning Objective: The learner will calculate the period of a control loop and the rate that a naive loop and a scheduled loop actually achieve, for six loops, to within 0.1 of the unit shown, with at least 5 of 6 correct on the first attempt.
Prerequisites: control loop, control rate, period, naive loop, scheduled loop, work time (all defined in the section "Time and the Control Loop" above this block).
Evidence of Mastery: For each of six problems the learner types a number in the unit shown and commits. An answer is correct when it is within 0.1 of the Correct column in Content. Mastery is 5 of 6 correct on the first attempt. Changing the target rate and the work time in Explore mode is exploration, not evidence.
Misconceptions: (1) A loop that sleeps one period after its work runs at the target rate. (It runs slower, since each cycle is the work plus the sleep.) (2) A scheduled loop can always keep the target rate. (It cannot if the work takes longer than the period.) (3) A faster target rate is always better. (The work must fit inside the period.)
Instructional Rationale: An Apply-level calculate objective needs repeated use of a formula on new numbers with an immediate check. The three formulas are short, and comparing the naive and scheduled answers for the same numbers shows why the scheduled loop is better.
Content:
Explore mode has two quantities the learner can change, and shows the period, the rate of a naive loop and the rate of a scheduled loop, with a bar of the cycle that is work and the part that is waiting.
| Quantity | Min | Max | Step | Default | Unit |
|---|---|---|---|---|---|
| Target rate | 10 | 200 | 10 | 50 | Hz |
| Work per cycle | 0 | 50 | 1 | 8 | ms |
Formulas: period (ms) = 1000 / target rate. Naive rate (Hz) = 1000 / (work + period). Scheduled rate (Hz) = the smaller of the target rate and 1000 / work. Six problems in this fixed order:
| # | Problem | Unit asked | Correct | Why (shown as feedback) |
|---|---|---|---|---|
| 1 | What is the period of a 50 Hz loop? | ms | 20.0 | 1000 / 50 = 20 ms. |
| 2 | What is the period of a 60 Hz loop? | ms | 16.7 | 1000 / 60 = 16.67 ms. |
| 3 | A naive loop targets 50 Hz and its work takes 5 ms. What rate does it achieve? | Hz | 40.0 | Each cycle is 5 + 20 = 25 ms, and 1000 / 25 = 40 Hz. |
| 4 | A scheduled loop targets 50 Hz and its work takes 5 ms. What rate does it achieve? | Hz | 50.0 | The work fits inside the 20 ms period, so the loop keeps the target rate. |
| 5 | A scheduled loop targets 50 Hz and its work takes 25 ms. What rate does it achieve? | Hz | 40.0 | The work is longer than the period, so each cycle is 25 ms and 1000 / 25 = 40 Hz. |
| 6 | A naive loop targets 100 Hz and its work takes 4 ms. What rate does it achieve? | Hz | 71.4 | The period is 10 ms, each cycle is 4 + 10 = 14 ms, and 1000 / 14 = 71.4 Hz. |
Provenance: The formulas are from the chapter sections "The Control Loop" and "Control Rate, Timing Jitter, and Latency". The numbers are illustrative values written for this sim.
Rules: period = 1000 / rate. naive = 1000 / (work + period). scheduled = min(rate, 1000 / work) and equals the target rate when work <= period. When the work is 0 ms the scheduled rate equals the target rate. An answer is correct when |typed - correct| <= 0.1.
Learner Activity:
- In Explore mode the learner changes the target rate and the work time and watches the two achieved rates. The learner should notice that the naive loop falls below the target as soon as there is any work, and that the scheduled loop stays at the target until the work is longer than the period.
- The learner switches to the six problems. Problem 1 is shown.
- The learner types a number and presses Check to commit.
- The sim shows whether the answer was correct and the Why text, then moves on. After problem 6 it shows the score.
Feedback: Six problems, fixed order, one attempt each. Correct: "Correct:
Starting State: Explore mode with a target rate of 50 Hz and 8 ms of work, showing a period of 20 ms, a naive rate of 35.7 Hz and a scheduled rate of 50 Hz.
Chapter Anchors: The chapter states that 50 Hz has a period of 20 ms, that 60 Hz has a period of 16.7 ms, and that a naive loop with 8 ms of work at 50 Hz runs at 36 Hz. The sim has six problems and mastery is 5 of 6.
Blocking, Threads, and the Stop Flag¶
The difference between blocking vs non-blocking code is whether the program can do anything else while it waits. Code is blocking when it cannot. time.sleep(2) blocks: for two seconds the program is deaf, and a stop request that arrives during the sleep is not noticed until it ends. A loop that runs for a minute, as a teleoperation loop does, blocks the main program for a minute. Non-blocking code returns at once and lets the program carry on, either by doing a small piece of work each time it is called, or by running the work somewhere else.
Threads are separate paths of execution inside one program, and each path is a thread. The standard library's threading module starts one with threading.Thread(target=function) and start(). The control loop can run in a thread, while the main program waits for a key, draws a picture, or talks to an agent (Chapter 15). Two threads need a safe way to talk, and the simplest is a stop flag, a threading.Event. The loop checks stop.is_set() at the top of every cycle, and the main program calls stop.set() to ask it to finish. It is polite and it is bounded: the loop notices within one period, and it always shuts down cleanly. A thread has one big rule for robots. A loop that moves the arm must never be killed from outside in the middle of a command. Always stop it with a flag, so that its last act is the clean finally of Chapter 10.
A Loop That Cannot Be Stopped Is a Hazard
Every loop that moves the arm needs a way out that you can reach: a stop flag, a time limit, or both. A loop with neither can only be stopped by the E-stop, and the E-stop should be the last resort and not the usual way. Put the flag in before you write the rest of the loop.
Moving Joints¶
Joint-Space Motion¶
Joint-space motion is movement described by the joint angles alone. You say "shoulder pan 60 degrees, elbow 30 degrees", and each joint goes there. You never say where the hand ends up in the room, because the pose of Chapter 2 is exactly this list of angles. The alternative is Cartesian motion, which describes the hand's position in space and works backward to the joints, and Chapter 12 covers it. Joint-space motion is simpler and it needs no maths, and it is the right choice when you know the pose you want, such as "home" or "pick above the bin". Its drawback is that equal joint steps do not make the hand move in a straight line.
Linear Interpolation and the Single-Joint Move¶
Interpolation is finding the values between two known values. Linear interpolation does it with a straight line: the value a fraction \( t \) of the way from \( a \) to \( b \) is
A single-joint move is the simplest use: one joint goes from a start angle to an end angle over a chosen time, with a new target sent at every tick of the control loop. A worked example is shoulder pan from 0 to 60 degrees in 2 seconds at 50 Hz. There are \( 2 \times 50 = 100 \) steps, and the target at step \( i \) is \( 0 + 60 \times i / 100 \), which rises by 0.6 degrees at each tick. The speed is \( 60 / 2 = 30 \) degrees per second. In code, lerp(a, b, t) is one line, and a trajectory is a list of such values, one per tick.
Multi-Joint Moves and the Speed Limit¶
A multi-joint move moves several joints at once, and the natural wish is that they all start together and arrive together, so that the arm moves as one. The rule is that every joint uses the same \( t \) at every tick, so each joint covers its own distance in the same time. That means the joints go at different speeds: the one with the longest way to go sets the pace, and the others go proportionally slower. Take a move in which the shoulder pan goes 60 degrees and the elbow 30 degrees, with a speed limit of 30 degrees per second. The pan, with the longer way, sets the time: \( 60 / 30 = 2 \) seconds. The elbow takes the same 2 seconds for 30 degrees, so it moves at 15 degrees per second. In general
where \( d_i \) is the distance of joint \( i \). The speed limit is the safety rule of Chapter 5 in motion form: it is the largest speed that you allow any joint, chosen well below what the servo could do.
Trajectories¶
A trajectory is a path with timing: the pose that every joint should have at each moment. A list of poses at a fixed rate is a trajectory, and the Trajectory dataclass of the lab holds exactly that, with its duration. The same path can be driven slowly or quickly, so the same path gives many trajectories. The velocity profile, next, is the part of the trajectory that decides how the joint speeds up and slows down.
Velocity Profiles and the Acceleration Limit¶
The linear move has a flaw you can hear. At the first tick the joint goes from standing still to 30 degrees per second at once, which means an infinite acceleration, and the same happens at the end. The servo, the gears and the printed parts feel this as a jolt. A velocity profile is the shape of the speed over time, and a good profile starts and ends at zero speed.
The smooth velocity profile replaces \( t \) with the ramp \( s(t) = 3t^2 - 2t^3 \). It starts and ends at zero speed and is steepest in the middle, where its speed is 1.5 times the average. It is one line of code, and it is a good default. Chapter 19 develops better curves, such as the minimum-jerk polynomial. The price of the smooth profile is time: to keep the same peak speed, it needs 1.5 times as long as the linear move.
The trapezoidal velocity profile is the classic profile of industrial robots. The joint speeds up at a constant acceleration until it reaches the speed limit, cruises at that speed, and slows down at the same rate. Plotted, the speed is a trapezoid. The acceleration limit \( a_{max} \), in degrees per second squared, is the largest change of speed that you allow, and it is what keeps the start and stop gentle. For a distance \( D \), a speed limit \( v \) and an acceleration limit \( a \), if \( D \ge v^2 / a \) then
If the distance is too short to reach the speed limit, the speed never reaches \( v \), and the profile is a triangle: \( t_{accel} = \sqrt{D / a} \), \( T = 2\, t_{accel} \) and the peak speed is \( a\, t_{accel} \). A worked example: \( D = 90 \) degrees, \( v = 60 \) degrees per second and \( a = 120 \) degrees per second squared. Because \( 90 \ge 60^2 / 120 = 30 \), the profile is a trapezoid, with \( t_{accel} = 60 / 120 = 0.5 \) s, \( t_{cruise} = (90 - 60 \times 0.5) / 60 = 1.0 \) s, and \( T = 2.0 \) s. For \( D = 20 \) degrees the profile is a triangle, with \( t_{accel} = \sqrt{20 / 120} = 0.41 \) s, \( T = 0.82 \) s and a peak of 49 degrees per second.
| Profile | Speed at the start | Speed in the middle | Time for the same peak speed | Use it for |
|---|---|---|---|---|
| Linear | Jumps to full speed | Constant | Shortest | Testing, tiny moves |
| Smooth | Starts at zero | 1.5 times the average | 1.5 times the linear time | A good default |
| Trapezoidal | Starts at zero, rises steadily | Constant at the limit | Between the two | Speed and acceleration limits you can state |
Next, a MicroSim for the trapezoid arithmetic.
Diagram: Trapezoid Profile Calculator¶
Run the Trapezoid Profile Calculator MicroSim fullscreen
Trapezoid Profile Calculator
Type: microsim
sim-id: trapezoid-profile-calculator
Library: p5.js
Status: Specified
Bloom Level: Apply
Bloom Verb: calculate
Learning Objective: The learner will calculate the acceleration time, cruise time, total time and peak speed of a joint move under a trapezoidal velocity profile, for six problems, to within the tolerance shown, with at least 5 of 6 correct on the first attempt.
Prerequisites: trajectory, velocity profile, speed limit, acceleration limit, trapezoidal velocity profile (all defined in the section "Velocity Profiles and the Acceleration Limit" above this block).
Evidence of Mastery: For each of six problems the learner types a number in the unit shown and commits. An answer is correct when it is within the tolerance in the Tolerance column of Content. Mastery is 5 of 6 correct on the first attempt. Changing the distance and limits in Explore mode is exploration, not evidence.
Misconceptions: (1) The move always reaches the speed limit. (A short move never does, and its profile is a triangle.) (2) The time is the distance divided by the speed limit. (The speed-up and slow-down add time.) (3) A higher acceleration limit changes the cruise speed. (It only shortens the ramps.)
Instructional Rationale: An Apply-level calculate objective needs repeated use of a formula on new numbers with an immediate check. Including a short move that makes a triangle forces the learner to test the condition before choosing the formula.
Content:
Explore mode has three quantities the learner can change, and shows the speed against time as a trapezoid or a triangle, with the four times and the peak speed.
| Quantity | Min | Max | Step | Default | Unit |
|---|---|---|---|---|---|
| Distance | 5 | 180 | 5 | 90 | degrees |
| Speed limit | 10 | 120 | 10 | 60 | degrees per second |
| Acceleration limit | 20 | 400 | 20 | 120 | degrees per second squared |
Rules for the problems: if D >= v^2 / a the profile is a trapezoid with t_accel = v / a, t_cruise = (D - v x t_accel) / v and T = 2 x t_accel + t_cruise. Otherwise it is a triangle with t_accel = sqrt(D / a), t_cruise = 0, T = 2 x t_accel and a peak speed of a x t_accel. Six problems in this fixed order:
| # | Problem | Unit asked | Correct | Tolerance | Why (shown as feedback) |
|---|---|---|---|---|---|
| 1 | D = 90 degrees, v = 60 deg/s, a = 120 deg/s^2. What is the total time? | seconds | 2.00 | 0.01 | 90 >= 30, so it is a trapezoid: T = 90 / 60 + 60 / 120 = 2.00 s. |
| 2 | The same move. How long is the speed-up? | seconds | 0.50 | 0.01 | t_accel = 60 / 120 = 0.50 s. |
| 3 | The same move. How long is the cruise? | seconds | 1.00 | 0.01 | t_cruise = (90 - 60 x 0.5) / 60 = 1.00 s. |
| 4 | D = 20 degrees, v = 60 deg/s, a = 120 deg/s^2. What is the total time? | seconds | 0.82 | 0.01 | 20 < 30, so it is a triangle: t_accel = sqrt(20 / 120) = 0.408 s and T = 0.82 s. |
| 5 | The same move. What is the peak speed? | deg/s | 49.0 | 0.1 | The peak is 120 x 0.408 = 49.0 deg/s, below the 60 deg/s limit. |
| 6 | D = 60 degrees, v = 30 deg/s, a = 60 deg/s^2. What is the total time? | seconds | 2.50 | 0.01 | 60 >= 15, so it is a trapezoid: T = 60 / 30 + 30 / 60 = 2.50 s. |
Provenance: The formulas are from the chapter section "Velocity Profiles and the Acceleration Limit". The problems are illustrative values written for this sim. Problems 1 to 5 are the chapter's worked examples.
Rules: An answer is correct when |typed - correct| <= the tolerance. The explore ranges cover every value in the problems. The sim chooses the trapezoid or the triangle by testing D >= v^2 / a.
Learner Activity:
- In Explore mode the learner changes the distance and the limits and watches the speed graph change between a trapezoid and a triangle. The learner should notice that making the distance shorter turns the trapezoid into a triangle at D = v^2 / a.
- The learner switches to the six problems. Problem 1 is shown.
- The learner types a number and presses Check to commit.
- The sim shows whether the answer was correct, the working and the Why text, then moves on. After problem 6 it shows the score.
Feedback: Six problems, fixed order, one attempt each. Correct: "Correct:
Starting State: Explore mode with D = 90, v = 60 and a = 120, showing a trapezoid with a total time of 2.00 s.
Chapter Anchors: The chapter's worked examples are D = 90 (T = 2.0 s, speed-up 0.5 s, cruise 1.0 s) and D = 20 (T = 0.82 s, peak 49 degrees per second). The sim has six problems and mastery is 5 of 6.
Waypoints and Waypoint Lists¶
A waypoint is a named pose that the arm passes through, with an optional pause once it gets there. A waypoint list is the ordered list of them that makes up a task: "home", "above the object", "at the object", "above the bin". A task written as waypoints is easy to read and to change, because you can edit a single pose without touching the code that moves between them. The lab's Waypoint dataclass holds the name, the pose and the pause.
A waypoint list has one rule that saves arms. Check the whole list before anything moves. A list with a pose outside a joint's limit should fail on the first line of the program, with a message that names the waypoint and the joint, and not on the fifth waypoint with the arm half way through a task. The lab's check_waypoints returns every problem in the list at once. It uses the limits that Joint holds, which are the limits that you measured in Chapter 8.
Name Every Waypoint
A waypoint called pick_above_bin tells you what it is for, and pose_4 tells you nothing. When something goes wrong you will read the names in an error message, and when a joint is wrong you will fix one line and not hunt through numbers.
Grippers and Pick and Place¶
Gripper Control, Width, and Force¶
Gripper control treats the gripper as one more joint, with its own target at every tick, in the percent that Chapter 10 gave it: 0 is closed and 100 is open, from the calibration. The gripper width is how far apart the fingers are, and the percent is a measure of it. To turn the percent into millimeters, measure the opening with calipers (Chapter 7) at 0 percent and at 100 percent, and interpolate between the two. The width matters because a grasp works only if the opening is wider than the object when approaching and the fingers close until they touch it.
Grasp force is how hard the fingers squeeze, and the gripper limits it with a torque limit (Chapter 5). The LeRobot follower's configuration gives the gripper a torque limit of 50 percent and an overload torque of 25 percent, so that a gripper that closes on an object cannot burn out its motor by pushing forever. The squeeze that you want is just enough to hold the object, and the force limit is also the safety limit.
A closed gripper that has something in it behaves differently from an empty one, and a program can tell. Command 0 percent, wait for the move to end, and read the gripper. An empty gripper reads about 0. A gripper that holds an object stops where the object stops it, at the object's width, for example 20 percent. That is the same idea as the stall check of Chapter 5, and it is the program's only way to know whether a grasp worked. The lab uses a threshold of 5 percent.
The Pick-and-Place State Machine¶
Pick and place is the most common arm task: pick something up at one place and put it down at another. It breaks into steps that must come in a fixed order, and each step has a reason.
| Step | What the arm does | Why |
|---|---|---|
| Approach | Move above the object with the gripper open | The pre-grasp approach comes from above so that the fingers do not hit the object on the way in |
| Descend | Go straight down to the object | The fingers surround it |
| Grasp | Close the gripper and check that it holds something | A missed grasp must be found now, not after the move |
| Lift | Go straight back up with the object | The lift and retreat moves keep the arm clear of the table and the object |
| Transport | Move above the place | Nothing is dragged across the table |
| Lower | Go down to the place | |
| Release | Open the gripper | |
| Retreat | Go back up, clear of the object | The arm does not knock what it just put down |
A state machine is a program built around a list of states and rules for the next state. Here each step above is a state, and the rule is mostly "the next one in the list". Its power is in the exceptions: the grasp state can send the machine back to the approach state to try again, or to a failed state when the tries run out. Writing the task as a state machine keeps these decisions in one place, with one function per state and a table that maps each state to its function, and it prints the state it is in, which makes a task easy to watch and debug. Python's enum module makes the states a named list that cannot be mistyped.
Check the Grasp Before the Lift
The cheapest place to find a missed grasp is the moment the gripper closes. Read the gripper right then, and let the state machine retry or stop, and you will not carry an empty gripper across the table or put "nothing" down very carefully.
The next MicroSim practises the order of the steps.
Diagram: Pick and Place Sequencer¶
Run the Pick and Place Sequencer MicroSim fullscreen
Pick and Place Sequencer
Type: microsim
sim-id: pick-and-place-sequencer
Library: p5.js
Status: Specified
Bloom Level: Analyze
Bloom Verb: organize
Learning Objective: The learner will organize the eight steps of a pick-and-place task into their correct order, with at least 7 of the 8 steps in their correct positions within three attempts.
Prerequisites: pick and place, pre-grasp approach, lift and retreat, gripper control, state machine (all defined in the section "The Pick-and-Place State Machine" above this block).
Evidence of Mastery: The learner arranges eight shuffled step cards into an order and presses Check. An arrangement is scored by the number of cards in their correct position, as given in the Position column in Content. Mastery is 7 or more of 8 cards in the correct position within three attempts. Reading the explanations in Explore mode is exploration, not evidence.
Misconceptions: (1) The arm should go straight to the object's height from home. (It approaches from above first, so that the fingers do not hit the object.) (2) The gripper closes at the same time as the arm descends. (It closes after the arm has stopped at the object.) (3) The arm can move away as soon as the gripper opens. (It must retreat upward, clear of what it placed.)
Instructional Rationale: An Analyze-level organize objective asks the learner to structure the parts of a procedure by their dependencies. Ordering the cards forces the learner to reason about which step must happen before which, and the count of correct positions shows which dependency was missed.
Content:
The eight steps, with the correct position of each:
| Position | Step | Why it goes here (shown as feedback) |
|---|---|---|
| 1 | Move above the object with the gripper open. | The approach from above keeps the fingers clear of the object. |
| 2 | Descend to the object. | The fingers must surround the object before they close. |
| 3 | Close the gripper and check that it holds something. | The grasp is checked now, so that a miss is found before the move. |
| 4 | Lift straight up with the object. | Lifting first keeps the object clear of the table. |
| 5 | Move above the place. | The object travels at a safe height. |
| 6 | Lower to the place. | The object is put down gently. |
| 7 | Open the gripper. | The object is released only when it is in place. |
| 8 | Retreat upward. | The arm goes up before it moves away, so it does not knock the object. |
Provenance: The steps and their order are from the chapter section "The Pick-and-Place State Machine".
Rules: An arrangement has exactly one correct order, the one in the Position column. The score is the number of cards whose position equals their correct position. Mastery is a score of 7 or 8. The three attempts are separate: each attempt starts from the card order the learner left.
Learner Activity:
- In Explore mode the learner reads the eight steps in the correct order, each with its explanation.
- The learner switches to the exercise. The eight cards are shown in a shuffled order.
- The learner moves the cards into the order they would do them and presses Check.
- The sim shows the score and marks the cards that are in a wrong position, without revealing the right place for each. The learner can rearrange and check again, up to three attempts in all.
- After the last attempt, or after a score of 8, the sim shows the correct order with the explanations.
Feedback: One exercise, up to three attempts. After each attempt: "
Starting State: Explore mode with the correct order shown, and the prompt "Put the steps in the order you would do them." ready for the exercise.
Chapter Anchors: The chapter's table has eight steps: approach, descend, grasp, lift, transport, lower, release and retreat. The sim has eight steps, three attempts, and mastery is 7 of 8 in the correct position.
Teleoperation¶
The Teleoperation Loop¶
Teleoperation is controlling a robot from a distance by hand, and Chapter 2 introduced the leader and follower pair for it. The teleoperation loop is the control loop that does it. At every tick it reads the leader arm's pose, turns it into a target for the follower, and sends the target. Nothing is planned. The person is the planner, and the loop has only to be fast and safe. LeRobot's lerobot-teleoperate command is this loop, with the leader and follower named on the command line, and it defaults to 60 cycles per second:
lerobot-teleoperate \
--robot.type=so101_follower \
--robot.port=/dev/tty.usbmodem58760431541 \
--robot.id=my_awesome_follower_arm \
--teleop.type=so101_leader \
--teleop.port=/dev/tty.usbmodem58760431551 \
--teleop.id=my_awesome_leader_arm
Your own version, in the lab, does the same thing with the Arm interface, so it works with any pair of arms that have drivers. Two things keep it safe. The follower's command goes through execute with a step limit, so that if the leader jumps, the follower catches up at a limited speed and does not snap to the new pose. And the loop has a stop flag, so that it ends cleanly.
Leader-Follower Mapping¶
The leader-follower mapping is the rule that turns a leader joint's value into a follower joint's value. When both arms have been calibrated (Chapter 8) and have the same geometry, the rule is the identity: the follower's elbow goes to wherever the leader's elbow is, and that is the whole point of calibration. A mapping with two numbers covers the rest: \( \text{follower} = \text{scale} \times \text{leader} + \text{offset} \). A scale of -1 mirrors a joint, which is handy when a leader is mounted facing the other way. A scale of 0.5 halves a motion for fine work, and an offset corrects a known difference between the arms. The lab's JointMap dataclass holds the two numbers for each joint, and map_pose applies them to a whole pose.
Latency matters most here, because the person feels it. At 50 Hz the follower can be up to 20 ms late from waiting for the next cycle, and the bus and the motors add more. A person usually stops noticing a delay of a few tens of milliseconds, and sees a lag when it grows to a tenth of a second. If the loop cannot hold its rate, because the work per tick is too long, the follower falls behind, and the way to fix it is the one that the loop section gave: less work per tick, or a lower rate.
Lab: Move the Fake Arm¶
In this lab you will write the pieces of the chapter and run them on the fake arm: a fixed-rate control loop, planned moves, a pick-and-place state machine with a pretend object, and a teleoperation loop in a thread. Nothing needs hardware. The new Python ideas are time, threading and enum. The programs print only numbers that are the same on every run, such as a rate that is within 5 percent of the target, because timing differs a little from one computer to the next.
Step 1. Activate the project.
Step 2. Write the control loop. run_loop is the scheduled loop of the chapter: it calls a function step(tick, seconds) at the chosen rate until it has made the number of calls that you asked for, the duration has passed, or the stop flag is set, scheduling each tick from the start time, and returns the real time between ticks. naive_loop is the do-the-work-then-sleep version, for comparison. The two small functions after them turn a list of periods into a rate and a worst jitter. Create armlab/loop.py:
Step 3. Compare the loops. The script runs a naive loop and a scheduled loop with 8 ms of work, and then a scheduled loop with 30 ms of work, which is more than the 20 ms period. Create loop_demo.py:
Target: 50 Hz, a period of 20 ms
1. Naive loop (work, then sleep 20 ms), 8 ms of work: below 40 Hz: True
2. Scheduled loop, 8 ms of work: within 5% of 50 Hz: True
worst jitter under 10 ms: True
3. Scheduled loop, 30 ms of work: runs at about 1 / 0.030 = 33 Hz, below 40 Hz: True
it fell behind on every tick instead of trying to catch up
The naive loop falls below 40 Hz, though it was asked for 50, because each cycle is 28 ms or more. The scheduled loop holds 50 Hz with small jitter. And in the overloaded case the loop does not rush or crash. It settles at the rate that the work allows.
Step 4. Write the motion module. It holds the pieces of the sections on moving joints. Read each function in turn: lerp is linear interpolation, smooth is the smooth ramp, trapezoid_times and trapezoid_fraction are the formulas of the trapezoid, plan_move builds a Trajectory for a multi-joint move with the joint that has the longest way to go setting the time, follow sends a trajectory to an arm at its rate, and Waypoint and check_waypoints are the waypoint list and its check. Create armlab/motion.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | |
Step 5. Plan moves without moving anything. The script plans the multi-joint move of the chapter, compares the three profiles on a 90 degree move, and prints the trapezoid numbers. Create trajectory_demo.py:
1. A multi-joint move: shoulder pan 60 degrees, elbow 30 degrees, speed limit 30 degrees per second
101 poses, 2.00 s, ends at {'shoulder_pan': 60.0, 'elbow_flex': 30.0}
shoulder pan speed 30 deg/s, elbow speed 15 deg/s: they arrive together
2. Three profiles for a 90 degree move (speed limit 60 deg/s, acceleration limit 120 deg/s^2)
profile time speed at 0.1 s speed at middle largest speed
linear 1.50 s 60 deg/s 60 deg/s 60 deg/s
smooth 2.26 s 9 deg/s 60 deg/s 60 deg/s
trapezoid 2.00 s 11 deg/s 60 deg/s 60 deg/s
3. The trapezoid in numbers
90 degrees: speed up 0.50 s, cruise 1.00 s, total 2.00 s, peak 60 deg/s
20 degrees: speed up 0.41 s, cruise 0.00 s, total 0.82 s, peak 49 deg/s
Look at the second table. All three profiles have the same peak speed, 60 degrees per second. The linear profile is at full speed at 0.1 s, which means it jumped there. The smooth profile is at 9 degrees per second and the trapezoid at 11, so both start gently, and the price is time: 2.26 s and 2.00 s against 1.50 s. The numbers in the last section are the worked examples of the chapter.
Step 6. Write the pick-and-place state machine. State is an enum with one member per step, plus DONE and FAILED. Task holds the arm, the four poses, the speed and the retry limit. One small function per state moves the arm and returns the next state, and HANDLERS is the table from a state to its function. _grasp is where the decision is made: it closes the gripper, reads it, and returns LIFT if the reading is above 5 percent, APPROACH for another try, or FAILED when the tries are used up. Create armlab/pick.py:
Step 7. Make a pretend object, and run a pick-and-place that works and one that misses. FakeArmWithObject is a fake arm whose gripper cannot close past 20 percent while an object is in the way, which is how a real gripper stalls on an object. It lives in its own small module, so that the next chapter can use it too. Create armlab/fakeworld.py:
The script checks the waypoints first, runs the task, and then runs it again with the object missing. The poses are illustrative joint targets within the limits, since Chapter 12 will compute poses from the position of a real object. Create pick_demo.py:
1. The object is there
APPROACH
DESCEND
GRASP
LIFT
TRANSPORT
LOWER
RELEASE
RETREAT
DONE
result: DONE, 188 commands sent, about 4 s
2. The object is missing
APPROACH
DESCEND
GRASP
the gripper closed fully (0%): nothing was grasped
APPROACH
DESCEND
GRASP
the gripper closed fully (0%): nothing was grasped
FAILED
result: FAILED, 188 commands sent, about 4 s
In the first run the machine goes through all eight states to DONE. In the second, the gripper closes to 0 percent, the grasp check finds nothing, the machine tries once more, and then stops in FAILED without lifting anything. That decision, made by a three-line check, is the difference between a task that fails safely and one that carries nothing across the table. The number of commands is the number of control ticks that the moves needed.
Check Before You Commit
The state machine did not hope that the grasp worked. It looked at the gripper and chose the next state from what it saw. Reading the world before each commitment, and having a branch for "it did not work", is the idea that turns a script into a task you can trust.
Step 8. Write the teleoperation loop. JointMap and map_pose are the leader-follower mapping. teleop_loop reads the leader, maps its pose, and sends it to the follower with execute and a step limit, and it measures how long each copy takes. Create armlab/teleop.py:
Step 9. Run teleoperation in a thread. The script makes two fake arms (the follower's bus is slowed by 2 ms per write, as a real one is) and starts the loop in a thread. The main program plays the part of the hand: it moves the leader's shoulder pan from 0 to 40 degrees over a second, and then jumps it to 90. The follower may move only 5 degrees per tick, so the second move shows the step limit at work. At the end the main program sets the stop flag and waits for the thread. Create teleop_demo.py:
1. The hand moves the leader's shoulder pan from 0 to 40 degrees over 1 second
2. Then the hand jumps the leader to 90 degrees at once. The follower may move 5 degrees a tick
the follower caught up in about 0.2 s
follower 90 degrees, leader 90 degrees
ticks sent between 55 and 70: True; slowest copy under 20 ms: True
3. The mapping can also mirror a joint: follower = -1 x leader + 0
leader 30 degrees -> follower -30 degrees
The jump of 50 degrees at 5 degrees per tick takes 10 ticks, which at 50 Hz is 0.2 seconds, and that is what the script reports. The main program stayed free while the loop ran, because the loop was in a thread, and the stop flag ended it cleanly. To use your own loop with the real arms, build a FeetechArm for each of the leader and the follower from Chapter 10, with each one's own port and calibration file.
Step 10. Record your work.
git add .
git commit -m "Add the control loop, trajectories, waypoints, pick and place, and teleoperation"
git log --oneline
Check Your Work¶
Your project is complete when all of these are true:
python loop_demo.pyprintsTruefor the naive loop being below 40 Hz and for the scheduled loop being within 5 percent of 50 Hz.python trajectory_demo.pyprints a trapezoid time of2.00 sfor the 90 degree move.python pick_demo.pyends the first run inDONEand the second inFAILED.python teleop_demo.pyprintsfollower 90 degrees, leader 90 degrees.- Earlier scripts (
arm_demo.py,safe_demo.py) still run. git log --onelineshows an eleventh commit.
Challenge: A Slow Start¶
The linear move jumps to full speed. Add a function ramped_move(start, end, rate_hz, v_max, ramp_s) to armlab/motion.py that is a linear move with a speed ramp at the start only: the speed rises from 0 to v_max over ramp_s seconds, then stays constant. Test it on a 60 degree move at 30 degrees per second with a ramp of 0.5 seconds, and print the speed at the first three ticks.
Click to see one solution
A ramp that starts at zero speed and reaches \( v \) in time \( r \) covers \( v r / 2 \) degrees while it ramps. After that the joint moves at \( v \). One way to write it is to compute the distance travelled as a function of time and divide by the total:
For 60 degrees at 30 degrees per second with a 0.5 second ramp, the ramp covers 7.5 degrees, the cruise covers 52.5 degrees in 1.75 seconds, and the total is 2.25 seconds. At 50 Hz the first three ticks are about 0.06, 0.24 and 0.54 degrees, so the speeds are about 3, 9 and 15 degrees per second, rising steadily instead of jumping to 30.
Summary and Key Takeaways¶
You can now move an arm smoothly, in a loop, through a list of waypoints, and by hand.
- The time module offers
sleep,perf_counter,monotonicandtime. Useperf_counterto measure. A control loop reads, decides and writes at a fixed control rate, and scheduling each tick from the start keeps the rate. Timing jitter is the variation between ticks and latency is the delay between cause and response. - Code that waits is blocking. A thread lets the loop run in the background, and a stop flag (
threading.Event) is the safe way to end it. - Joint-space motion specifies joint angles. Interpolation, in its simplest form linear interpolation, gives the values between two poses. A single-joint move and a multi-joint move use the same \( t \) for every joint, and the joint with the longest way to go sets the time.
- A trajectory is a path with timing. The smooth velocity profile and the trapezoidal velocity profile start and end at zero speed, and the acceleration limit keeps the start gentle. For a trapezoid, \( T = D / v + v / a \).
- A waypoint is a named pose and a waypoint list is a task. Check the whole list before anything moves.
- Gripper control treats the gripper as a joint in percent, and gripper width and grasp force come from its calibration and its torque limit. A gripper that stops above 0 percent when told to close holds something.
- Pick and place is a state machine: a pre-grasp approach, a descent, a checked grasp, a lift and retreat, a transport, and a release, with retries and a failure state.
- The teleoperation loop reads a leader, applies the leader-follower mapping, and writes a follower, with a step limit and a stop flag.
You Can Move Me Like a Pro!
You wrote a steady control loop, planned moves with gentle starts and stops, followed a list of waypoints, picked something up and checked the grasp, and made one arm copy another in real time. That is everything a program needs to make me move. Let's move it on to Chapter 12!