Instructions
The key concepts introduced the difference between commands and instructions. This chapter covers the instruction side in depth: the handle an instruction returns, how to wait for it, and how several instructions form a block.
Commands vs. Instructions
Type |
Returns |
Blocks? |
Examples |
|---|---|---|---|
Command |
|
Yes, until the control confirms |
|
Instruction |
|
No |
|
Working with Instruction Handles
An instruction is queued with start(), which returns an InstructionHandle immediately. The robot executes the
instruction in the background while your program continues. join() on the handle blocks until the instruction has
ended and returns its outcome. join_timeout() does the same but gives up after a deadline, for a motion that takes
longer than expected and should be reported rather than waited out:
fn async_instruction(robot: &impl MovePtp) -> Result<()> {
info!("--- Asynchronous instruction ---");
let vertical: InstructionHandle = robot.move_ptp(poses::vertical()).start()?;
info!("Instruction sent (robot is moving...)");
// Do other work here while the robot moves...
match vertical.join_timeout(Duration::from_secs(30)) {
Ok(()) => info!("VERTICAL reached."),
Err(e) => info!("Motion ended: {e}"),
}
Ok(())
}
A timeout does not affect the robot. The instruction keeps running; only the wait ends.
“Background” means a worker thread of this library, not async/await. The handle offers:
Method |
Effect |
|---|---|
|
Block until the instruction ends; consume the handle and return its outcome. |
|
Same, but give up with |
|
Block with a deadline but keep the handle; return |
|
Non-blocking check whether the instruction has ended. |
|
Check or wait whether the robot control executes the instruction; see Instruction Activity. |
Dropping a handle detaches the instruction. The robot keeps executing it; only its outcome goes unobserved.
Instruction Activity
A queued instruction does not start at once when other instructions are still ahead of it. is_active() tells
without blocking whether the robot control is executing the instruction right now: it is false before the
instruction starts and again after it ended. wait_active() blocks until the instruction starts:
fn instruction_activity(robot: &impl MovePtp) -> Result<()> {
info!("--- Instruction activity ---");
let home = robot.move_ptp(poses::home()).start()?;
home.wait_active()?;
info!("Moving to HOME, active: {}", home.is_active());
home.join()?;
info!("HOME reached.");
Ok(())
}
wait_active() returns the instruction’s error if it failed or was aborted before it started. Activity is sampled
periodically, so an instruction shorter than one sample can end without ever being seen active; wait_active() then
returns Ok as well. wait_active_timeout(d) waits at most d and returns None on a timeout, like
wait_timeout(d). For a block, is_active() is true while any step executes, and wait_active() waits for any
step. In a blended block, a step stops being active when the blend into the next step begins.
Activity follows the interpolator of the robot control, not the axes. The axes start a little later and can still move after the instruction ended. To wait for the axes, see Waiting for Standstill.
Synchronous Pattern
To execute instructions strictly one after another, call join() right after each start(). The next instruction
is only sent once the previous one has finished:
robot.move_ptp(poses::home()).start()?.join()?;
info!("HOME reached.");
robot.move_ptp(poses::vertical()).start()?.join()?;
info!("VERTICAL reached.");
Instruction Blocks
Several instructions that belong together are written as one block: a single builder chain in which each step
is another move_* or wait_time call. Nothing is sent before start(). Then all steps go out back to back, and
the robot control knows every step in advance:
fn instruction_block_with_blending(robot: &impl MovePtp) -> Result<()> {
info!("--- Instruction block with blending ---");
let block = robot
.move_ptp(poses::home_c())
.move_ptp(poses::blending_1())
.blending(Percent(50.0))
.move_ptp(poses::blending_2())
.blending(Percent(50.0))
.move_ptp(poses::blending_3())
.start()?;
info!("All instructions sent.");
// One handle for the whole block; it resolves when the last step ends.
match block.join() {
Ok(()) => info!("BLENDING_3 reached."),
Err(e) => info!("Block ended: {e}"),
}
Ok(())
}
One handle, a BlockHandle, covers the block and waits like an InstructionHandle. It is done when the last step
is done or when any step failed. It reports the InstructionFailed error of the step that caused a robot error if
there is one, otherwise the first failure in step order. If the robot is stopped while the block executes, the
handle resolves with StopTriggered. To see what happened to each step, steps() returns one handle per step in
send order: a completed step reports Ok, the culprit InstructionFailed, and a step that never ran because of it
OtherInstructionFailed.
Steps of different kinds can be mixed freely, for example a PTP approach blended into a linear move, or a robot-side wait between two motions:
fn mixed_block(robot: &(impl MovePtp + MoveLinear)) -> Result<()> {
info!("--- Mixed PTP/linear block ---");
robot
.move_ptp(poses::home_c())
.blending(Percent(20.0))
.move_linear(poses::blending_1(), common::SLOW_TRAVEL_SPEED)
.start()?
.join()?;
info!("Mixed block complete.");
Ok(())
}
robot
.move_ptp(poses::home())
.wait_time(Duration::from_secs(2))
.move_ptp(poses::vertical())
.start()?
.join()?;
info!("Vertical reached after a 2 s robot-side wait.");
Errors
start() validates the block first (speed range, linear speed, blending value, motion profile) and fails at once
when a step is invalid or cannot be queued, for example because the robot is stopped. Nothing is sent in that case,
not even the steps before the one that failed.
Errors of the execution itself, such as an aborted motion, are reported by join(). The stop example shows both, a
rejected new instruction and an aborted pending one:
match robot.move_ptp(poses::home()).start() {
Err(RobotArmError::NotAllowed(_)) => info!("Expected: robot is stopped."),
Err(e) => info!("Unexpected error: {e}"),
Ok(_) => info!("Instruction accepted (unexpected)."),
}
// Pending handles resolve with StopTriggered.
match move_to_zero.join() {
Err(RobotArmError::StopTriggered(_)) => info!("Pending instruction aborted."),
Err(e) => info!("Unexpected error: {e}"),
Ok(()) => info!("Instruction completed (unexpected)."),
}
Blending
Blending lets the robot pass a waypoint without stopping. It only works if the instruction that follows the blended
one is known to the robot control in advance. This rule is part of the builder’s type. .blending() is the
transition into the next step: after it,
only another motion can follow. A block can never end on a blended step, and a wait can never be blended into. The
mistake does not compile:
// Does not compile: no terminal after `.blending()`.
robot.move_ptp(poses::blending_1()).blending(Percent(50.0)).start()?.join()?;
robot.move_ptp(poses::blending_2()).start()?.join()?;
Instead, put the blended step and the step it blends into in the same block:
robot
.move_ptp(poses::blending_1())
.blending(Percent(50.0))
.move_ptp(poses::blending_2())
.start()?
.join()?;
info!("BLENDING_2 reached (blended through BLENDING_1).");
Every motion accepts Factor and Percent as blending values. Linear and circular motions also accept an absolute
radius in Meters. A PTP motion has no
blending radius, so a Meters value before a PTP step, from the block or from amend_blending(), is dropped with a
warning and the robot stops at the target; blend into a PTP step with Factor or Percent instead. The control cannot
blend between a PTP and a circular motion, in either direction, and the builder’s types follow: after
.move_ptp(..).blending(..) a circular step does not compile, and after .move_circular(..).blending(..) a PTP
step does not compile. A value from amend_blending() that would blend such a pair is dropped with a warning.
When the next target is only known while the robot is already moving, a block cannot be formed. For that case,
amend_blending() adds the blending parameter to the most recently sent instruction afterwards; see
Amending Blending Parameters.
Advanced
A block that ends on a blended step can be sent with start_awaiting_successor() instead of start(). The robot
then holds at the start of the blended step until the successor is sent as a separate instruction. This exists for
bindings that queue one instruction at a time; applications should form blocks. The builder’s types cannot see the
separate successor, so a blend between a PTP and a circular motion is then dropped with a warning, and the robot stops
at the target of the blended step.
Full Example
Instructions example
//! Asynchronous instruction patterns.
//!
//! Demonstrates the difference between synchronous and asynchronous
//! instruction execution, instruction activity, instruction blocks, and
//! blending behavior.
mod common;
use std::time::Duration;
use common::poses;
use tracing::info;
use voraus_robot_arm::prelude::*;
/// Asynchronous instruction example.
///
/// `start()` queues the instruction and returns an [`InstructionHandle`]
/// immediately. The robot moves in the background and you can do other
/// work before calling `join()` to block until the motion completes. This
/// section uses `join_timeout()` instead, which additionally gives up with
/// [`RobotArmError::Timeout`] if the motion takes longer than expected.
fn async_instruction(robot: &impl MovePtp) -> Result<()> {
info!("--- Asynchronous instruction ---");
let vertical: InstructionHandle = robot.move_ptp(poses::vertical()).start()?;
info!("Instruction sent (robot is moving...)");
// Do other work here while the robot moves...
match vertical.join_timeout(Duration::from_secs(30)) {
Ok(()) => info!("VERTICAL reached."),
Err(e) => info!("Motion ended: {e}"),
}
Ok(())
}
/// Instruction activity.
///
/// `wait_active()` blocks until the robot starts executing the instruction,
/// which can be later than `start()` returns when other instructions are
/// still ahead of it. `is_active()` asks without blocking.
fn instruction_activity(robot: &impl MovePtp) -> Result<()> {
info!("--- Instruction activity ---");
let home = robot.move_ptp(poses::home()).start()?;
home.wait_active()?;
info!("Moving to HOME, active: {}", home.is_active());
home.join()?;
info!("HOME reached.");
Ok(())
}
/// Synchronous instruction pattern.
///
/// `start()` queues the instruction and `join()` blocks until it completes,
/// so the next instruction is only sent afterwards.
fn synchronous_instructions(robot: &impl MovePtp) -> Result<()> {
info!("--- Synchronous instructions ---");
robot.move_ptp(poses::home()).start()?.join()?;
info!("HOME reached.");
robot.move_ptp(poses::vertical()).start()?.join()?;
info!("VERTICAL reached.");
Ok(())
}
/// Instruction block with blending.
///
/// A block is one builder chain. All steps are sent together when the
/// terminal is called, so the robot knows every blended transition in
/// advance. `.blending()` belongs to the transition into the next step:
/// after it, another step *must* follow, so a block can never end on a
/// blended motion.
fn instruction_block_with_blending(robot: &impl MovePtp) -> Result<()> {
info!("--- Instruction block with blending ---");
let block = robot
.move_ptp(poses::home_c())
.move_ptp(poses::blending_1())
.blending(Percent(50.0))
.move_ptp(poses::blending_2())
.blending(Percent(50.0))
.move_ptp(poses::blending_3())
.start()?;
info!("All instructions sent.");
// One handle for the whole block; it resolves when the last step ends.
match block.join() {
Ok(()) => info!("BLENDING_3 reached."),
Err(e) => info!("Block ended: {e}"),
}
Ok(())
}
/// Blending, then waiting for the block.
///
/// `join()` waits on the handle of the whole block, so the blended step and
/// the step it blends into are always sent together. The builder requires a
/// step after `.blending()`, so a block never ends on a blended step.
fn blending_then_join(robot: &impl MovePtp) -> Result<()> {
info!("--- Blending, then join ---");
robot.move_ptp(poses::home_c()).start()?.join()?;
robot
.move_ptp(poses::blending_1())
.blending(Percent(50.0))
.move_ptp(poses::blending_2())
.start()?
.join()?;
info!("BLENDING_2 reached (blended through BLENDING_1).");
Ok(())
}
fn main() -> voraus_robot_arm::error::Result<()> {
common::init_logging();
let robot = common::create_connected_robot()?;
// Start from home.
robot.move_ptp(poses::home()).start()?.join()?;
async_instruction(&robot)?;
instruction_activity(&robot)?;
synchronous_instructions(&robot)?;
instruction_block_with_blending(&robot)?;
blending_then_join(&robot)?;
info!("Done.");
Ok(())
}