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

Result<()>

Yes, until the control confirms

stop_motion(), pause_motion(), enable()

Instruction

InstructionHandle from start()

No

move_ptp(), move_linear(), wait_time()

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

join()

Block until the instruction ends; consume the handle and return its outcome.

join_timeout(d)

Same, but give up with RobotArmError::Timeout after d.

wait_timeout(d)

Block with a deadline but keep the handle; return None on a timeout, after which it can be waited on again, otherwise Some of the outcome. Use it to do something periodically while waiting, for example to log progress every few seconds.

is_done()

Non-blocking check whether the instruction has ended.

is_active(), wait_active()

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