Moving a Robot in Joint Space
A point-to-point (PTP) motion is a movement where the robot travels to a target configuration by following the shortest path in joint space. The motion is therefore the quickest possible, but the resulting end effector path in Cartesian space can be unpredictable.
This chapter specifically explains PTP movements. For detailed information on how to use these methods synchronously or asynchronously, see the asynchronous instructions section and the instructions chapter.
Performing a PTP Movement
A PTP movement is started with move_ptp() from the MovePtp trait. It requires a target of type JointPose,
CartesianPose or CartesianTarget and returns a motion builder. Optional parameters are chained onto the builder, and
start() queues the motion. The speed() modifier defines the general speed for the motion relative to the
possible maximum joint speed. It accepts a Factor, which ranges from 0.01 to 1.0, or a Percent, which ranges
from 1.0 to 100.0; start() rejects a lower value. Without speed(), the robot moves at full speed, a Factor of 1.0.
A CartesianPose stores its translation as Meters and its rotation as Radians, so the unit travels with every
component. Its constructor takes the unit types of the crate, so a bare number does not compile:
CartesianPose::new(Centimeters(55.0), Meters(0.0), Millimeters(300.0), Degrees(180.0), Radians(0.0), Radians(0.0))
is the same pose as CartesianPose::new(Meters(0.55), Meters(0.0), Meters(0.3), Radians(PI), Radians(0.0), Radians(0.0)).
A JointPose takes one unit value per joint as well, Radians or Degrees for a rotary axis and Meters,
Millimeters or Centimeters for a linear one.
Note
A CartesianPose is interpreted in the robot coordinate system.
A CartesianPose itself can be ambiguous with regard to the exact joint configuration of the robot. It is at the
discretion of the underlying robot control to choose a convenient joint configuration to reach the given pose. This
means that the configuration reached depends on the starting position. A CartesianTarget removes both ambiguities;
see Perform a PTP Movement Using a CartesianTarget.
Perform a PTP Movement Using a JointPose
To perform a PTP movement to a target defined in joint space, pass a JointPose as the target:
// Move at 25% speed.
robot
.move_ptp(poses::home())
.speed(Factor(0.25))
.start()?
.join()?;
info!("Home reached (Factor 0.25).");
// Move at 100% speed.
robot
.move_ptp(poses::vertical())
.speed(Percent(100.0))
.start()?
.join()?;
info!("Vertical reached (Percent 100).");
Perform a PTP Movement Using a CartesianPose
To perform a PTP movement to a target defined in Cartesian space, pass a CartesianPose as the target:
let target = CartesianPose::new(
Meters(0.1382),
Meters(0.550),
Meters(0.296),
Radians(PI),
Radians(0.0),
Radians(0.0),
);
robot
.move_ptp(target)
.move_ptp(target + z(Meters(0.1)))
.start()?
.join_timeout(Duration::from_secs(10))?;
robot
.move_ptp(target)
.speed(Percent(100.0))
.start()?
.join()?;
info!("Target (Cartesian) reached.");
Since a Cartesian pose is ambiguous for a six-axis robot, the resulting joint configuration at the target can differ depending on the starting pose of the robot.
Perform a PTP Movement with Blending
PTP movements can use blending by chaining .blending() with a Factor or Percent. The higher the blending
value, the more and earlier the blended path deviates from the original path that would reach the target pose. In
most cases this allows the robot to maintain a higher path velocity and complete the overall path faster.
The exact behavior and interpretation of the blending parameter as well as implicit limitations are highly dependent on the specific robot used.
Blending only works if the instruction that follows is known to the robot control in advance. Therefore, blended
motions are written as one block: .blending() is the transition into the next step, and all steps are sent
together when start() is called:
fn ptp_with_blending(robot: &impl MovePtp) -> Result<()> {
info!("--- PTP with blending ---");
robot.move_ptp(poses::home()).start()?.join()?;
robot
.move_ptp(poses::blending_1())
.blending(Percent(50.0))
.move_ptp(poses::blending_2())
.blending(Factor(0.5))
.move_ptp(poses::blending_3())
.start()?
.join()?;
info!("Blending sequence complete.");
Ok(())
}
Note how the block ends on a step without blending, and how waiting happens only once, for the whole block. The
type of the builder enforces it: after .blending() only a PTP or linear motion may follow, so a block that ends on
a blended step does not compile.
Amending Blending Parameters
A blending parameter can be amended after the motion instruction was sent with amend_blending() from the
AmendBlending trait. It applies to the most recently sent instruction:
fn amend_blending_example(robot: &(impl MovePtp + AmendBlending)) -> Result<()> {
info!("--- Amending blending parameters ---");
robot.move_ptp(poses::home()).start()?.join()?;
robot.move_ptp(poses::blending_1()).start()?;
robot.amend_blending(Percent(50.0))?;
robot.move_ptp(poses::blending_2()).start()?;
robot.amend_blending(Factor(0.5))?;
robot.move_ptp(poses::blending_3()).start()?.join()?;
info!("Amended blending sequence complete.");
Ok(())
}
In this example the robot blends the first two waypoints into their successors just like in the previous example, but the blending parameters are supplied after sending the motion instruction. This is the escape hatch for cases where the next target is only known while the robot is already moving. Inside a block it is never needed.
Amending fails with RobotArmError::NotAllowed when there is nothing to amend (NotAllowedError::NoMotionToAmend):
without a preceding motion, or once the previous motion has completed, since there is nothing left to blend out of;
a wait, a stop, a robot error or a reconnect end the previous motion as well. The robot control may also drop a blend
request that arrives just before the motion completes. Amending a motion that already carries blending fails with
NotAllowedError::BlendingAlreadySpecified, and an out-of-range value with RobotArmError::UnitLimit.
A typical use is a target that depends on a signal which arrives while the robot is already moving. The robot moves to a safe position; if the signal says that it may enter a restricted area beyond, the running motion is amended with blending and the motion into the area is sent, so the robot blends smoothly into it. Without the signal it just stops at the safe position:
fn blend_into_a_late_target(
robot: &(impl MovePtp + AmendBlending),
may_enter: &AtomicBool,
) -> Result<()> {
info!("--- Blending into a late target ---");
robot.move_ptp(poses::home()).start()?.join()?;
let safe_position = robot.move_ptp(poses::blending_1()).start()?;
if !may_enter.load(Ordering::Acquire) {
safe_position.join()?;
info!("The area is blocked; stopped at the safe position.");
return Ok(());
}
match robot.amend_blending(Percent(50.0)) {
Ok(()) => info!("The area is free; blending into it."),
// The signal came after the robot had reached the safe position:
// there is nothing left to blend out of, so it enters from there.
Err(RobotArmError::NotAllowed(NotAllowedError::NoMotionToAmend)) => {
info!("The area is free; entering it from the safe position.");
}
Err(err) => return Err(err),
}
robot.move_ptp(poses::blending_2()).start()?.join()?;
Ok(())
}
Perform a Relative PTP Movement
For moving the robot relative to its current pose, use move_ptp_relative(). Other than that, it accepts the same
modifiers as move_ptp().
In the example below, the robot moves 10 cm in the positive z-direction from its current pose. The z() helper
builds a CartesianPose in which every component is zero except z:
robot
.move_ptp_relative(z(Meters(0.1)))
.speed(Percent(100.0))
.start()?
.join()?;
info!("Moved +10cm in Z (relative).");
Perform a PTP Movement Using a CartesianTarget
A CartesianTarget wraps a CartesianPose and pins what a plain pose leaves open: the coordinate system the pose is
expressed in (with_cs()), the frame that arrives at it (with_arriving_cs()), and the joint configuration it is
reached in (with_configuration_vector()). CartesianTarget::new(pose) starts from the defaults of a plain pose: the
robot coordinate system, the tool arriving, and no configuration vector. move_ptp() and move_ptp_relative() take
it like any other target. See Coordinate Systems for an example with a user coordinate
system. Without a configuration vector, the robot control chooses the joint solution;
Configuration Vector explains the vector and moves to one target in two
postures.
Full Example
PTP example
//! PTP (point-to-point) motion examples.
//!
//! Demonstrates absolute joint moves, Cartesian moves, relative moves,
//! speed control, and blending.
mod common;
use std::f64::consts::PI;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use common::poses;
use tracing::info;
use voraus_robot_arm::error::NotAllowedError;
use voraus_robot_arm::prelude::*;
use voraus_robot_arm::types::coordinates::z;
/// Simple absolute PTP movement using a `JointPose`.
fn simple_ptp_with_joint_pose(robot: &impl MovePtp) -> Result<()> {
info!("--- PTP with JointPose ---");
// Move at 25% speed.
robot
.move_ptp(poses::home())
.speed(Factor(0.25))
.start()?
.join()?;
info!("Home reached (Factor 0.25).");
// Move at 100% speed.
robot
.move_ptp(poses::vertical())
.speed(Percent(100.0))
.start()?
.join()?;
info!("Vertical reached (Percent 100).");
Ok(())
}
/// Simple absolute PTP movement using a `CartesianPose`.
fn simple_ptp_with_cartesian_pose(robot: &impl MovePtp) -> Result<()> {
info!("--- PTP with CartesianPose ---");
robot
.move_ptp(poses::home_c())
.speed(Factor(0.25))
.start()?
.join()?;
info!("Home (Cartesian) reached.");
let target = CartesianPose::new(
Meters(0.1382),
Meters(0.550),
Meters(0.296),
Radians(PI),
Radians(0.0),
Radians(0.0),
);
robot
.move_ptp(target)
.move_ptp(target + z(Meters(0.1)))
.start()?
.join_timeout(Duration::from_secs(10))?;
robot
.move_ptp(target)
.speed(Percent(100.0))
.start()?
.join()?;
info!("Target (Cartesian) reached.");
Ok(())
}
/// Relative PTP movement.
fn relative_ptp(robot: &impl MovePtp) -> Result<()> {
info!("--- Relative PTP ---");
robot
.move_ptp(poses::home())
.speed(Factor(0.5))
.start()?
.join()?;
info!("Home reached.");
// Move 10 cm up (relative Z).
robot
.move_ptp_relative(z(Meters(0.1)))
.speed(Percent(100.0))
.start()?
.join()?;
info!("Moved +10cm in Z (relative).");
Ok(())
}
/// PTP with blending between waypoints.
///
/// The three waypoints form one block. `.blending()` is the transition
/// into the next step; the last step carries no blending and is where the
/// robot comes to rest.
fn ptp_with_blending(robot: &impl MovePtp) -> Result<()> {
info!("--- PTP with blending ---");
robot.move_ptp(poses::home()).start()?.join()?;
robot
.move_ptp(poses::blending_1())
.blending(Percent(50.0))
.move_ptp(poses::blending_2())
.blending(Factor(0.5))
.move_ptp(poses::blending_3())
.start()?
.join()?;
info!("Blending sequence complete.");
Ok(())
}
/// Amending blending parameters after an instruction was sent.
///
/// `amend_blending` applies to the most recently sent instruction and is
/// the escape hatch for cases where the next target is only known while
/// the robot is already moving. Inside a block it is never needed.
fn amend_blending_example(robot: &(impl MovePtp + AmendBlending)) -> Result<()> {
info!("--- Amending blending parameters ---");
robot.move_ptp(poses::home()).start()?.join()?;
robot.move_ptp(poses::blending_1()).start()?;
robot.amend_blending(Percent(50.0))?;
robot.move_ptp(poses::blending_2()).start()?;
robot.amend_blending(Factor(0.5))?;
robot.move_ptp(poses::blending_3()).start()?.join()?;
info!("Amended blending sequence complete.");
Ok(())
}
/// Blending into a target that is only known while the robot moves.
///
/// The robot moves to a safe position. While it moves, a signal decides
/// whether it may enter a restricted area beyond: if it may,
/// `amend_blending` adds blending to the running motion and the motion
/// into the area is sent, so the robot blends smoothly into it instead of
/// stopping at the safe position first. Without the signal the robot just
/// stops at the safe position. `may_enter` stands for the signal; a real
/// application reads a sensor or a PLC flag while the motion runs.
fn blend_into_a_late_target(
robot: &(impl MovePtp + AmendBlending),
may_enter: &AtomicBool,
) -> Result<()> {
info!("--- Blending into a late target ---");
robot.move_ptp(poses::home()).start()?.join()?;
let safe_position = robot.move_ptp(poses::blending_1()).start()?;
if !may_enter.load(Ordering::Acquire) {
safe_position.join()?;
info!("The area is blocked; stopped at the safe position.");
return Ok(());
}
match robot.amend_blending(Percent(50.0)) {
Ok(()) => info!("The area is free; blending into it."),
// The signal came after the robot had reached the safe position:
// there is nothing left to blend out of, so it enters from there.
Err(RobotArmError::NotAllowed(NotAllowedError::NoMotionToAmend)) => {
info!("The area is free; entering it from the safe position.");
}
Err(err) => return Err(err),
}
robot.move_ptp(poses::blending_2()).start()?.join()?;
Ok(())
}
fn main() -> voraus_robot_arm::error::Result<()> {
common::init_logging();
let robot = common::create_connected_robot()?;
simple_ptp_with_joint_pose(&robot)?;
simple_ptp_with_cartesian_pose(&robot)?;
relative_ptp(&robot)?;
ptp_with_blending(&robot)?;
amend_blending_example(&robot)?;
blend_into_a_late_target(&robot, &AtomicBool::new(true))?;
blend_into_a_late_target(&robot, &AtomicBool::new(false))?;
info!("Done.");
Ok(())
}
Trait Definitions
The trait carries the entry points as default methods, which return a MotionBlock builder; its required method is
what a robot type implements.
MovePtp
pub trait MovePtp: BlockQueuing {
/// Queue a fully specified PTP motion.
///
/// Prefer the builder; implement this method when adding a robot type.
///
/// # Returns
///
/// An [`InstructionHandle`] that resolves when the motion completes.
///
/// # Errors
///
/// Returns [`RobotArmError`](crate::error::RobotArmError) if the motion
/// cannot be queued.
#[doc(hidden)]
fn execute_ptp(&self, request: PtpRequest) -> Result<InstructionHandle>;
/// Move the robot point-to-point to an absolute target.
///
/// The robot interpolates in joint space: it takes the fastest path to
/// the target joint configuration, and the Cartesian path is not defined.
/// A plain [`CartesianPose`](crate::types::CartesianPose) is in the robot
/// coordinate system, and the robot control chooses the joint
/// configuration that reaches it. A
/// [`CartesianTarget`](crate::types::CartesianTarget) names its
/// coordinate system and arriving frame, and its optional configuration
/// vector selects the joint configuration.
///
/// Defaults: full speed, no blending. Change them with
/// [`speed`](MotionBlock::speed) and [`blending`](MotionBlock::blending),
/// chain further motions, and send the block with
/// [`start`](MotionBlock::start) or
/// [`start_awaiting_successor`](MotionBlock::start_awaiting_successor).
///
/// # Example
///
/// ```rust
/// # use voraus_robot_arm::prelude::*;
/// # fn demo(robot: &impl MovePtp, joints: JointPose, pose: CartesianPose) -> Result<()> {
/// robot.move_ptp(joints).speed(Factor(0.5)).start()?.join()?;
/// robot.move_ptp(pose).start()?.join()?;
/// # Ok(())
/// # }
/// ```
///
/// # Arguments
///
/// * `target` - The absolute target pose: a
/// [`JointPose`](crate::types::JointPose), a
/// [`CartesianPose`](crate::types::CartesianPose) or a
/// [`CartesianTarget`](crate::types::CartesianTarget) (see [`PoseTarget`]).
///
/// # Returns
///
/// A block of one PTP step, in the PTP state.
fn move_ptp(&self, target: impl Into<PoseTarget>) -> MotionBlock<'_, Self, state::Ptp>
where
Self: Sized,
{
MotionBlock::single(self, ptp_step(target.into(), false))
}
/// Move the robot point-to-point by an offset from the current pose.
///
/// The robot interpolates in joint space, as for
/// [`move_ptp`](MovePtp::move_ptp), and the Cartesian path is not
/// defined. A [`CartesianTarget`](crate::types::CartesianTarget) offset
/// names the coordinate system it is expressed in.
///
/// Defaults: full speed, no blending. Change them with
/// [`speed`](MotionBlock::speed) and [`blending`](MotionBlock::blending),
/// chain further motions, and send the block with
/// [`start`](MotionBlock::start) or
/// [`start_awaiting_successor`](MotionBlock::start_awaiting_successor).
///
/// # Example
///
/// ```rust
/// # use voraus_robot_arm::prelude::*;
/// # fn demo(robot: &impl MovePtp) -> Result<()> {
/// // Move 10 cm up.
/// robot.move_ptp_relative(z(Meters(0.1))).start()?.join()?;
/// # Ok(())
/// # }
/// ```
///
/// # Arguments
///
/// * `offset` - The offset from the current pose: a
/// [`JointPose`](crate::types::JointPose), a
/// [`CartesianPose`](crate::types::CartesianPose) or a
/// [`CartesianTarget`](crate::types::CartesianTarget) (see [`PoseTarget`]).
///
/// # Returns
///
/// A block of one PTP step, in the PTP state.
fn move_ptp_relative(&self, offset: impl Into<PoseTarget>) -> MotionBlock<'_, Self, state::Ptp>
where
Self: Sized,
{
MotionBlock::single(self, ptp_step(offset.into(), true))
}
}
AmendBlending
pub trait AmendBlending {
/// Amend blending parameters for the previous motion instruction.
///
/// # Arguments
///
/// * `request` - The blending value to attach to the previous instruction.
///
/// # Returns
///
/// `Ok(())` on success.
///
/// # Errors
///
/// Those of [`AmendBlending::amend_blending`].
#[doc(hidden)]
fn execute_amend_blending(&self, request: AmendBlendingRequest) -> Result<()>;
/// Amend blending parameters for the previous motion instruction.
///
/// The value is stored and sent with the next motion instruction, so
/// call this before queuing that motion. The call does not wait for the
/// robot; it only waits while another thread queues a block on the same
/// robot.
///
/// # Example
///
/// ```rust
/// # use voraus_robot_arm::prelude::*;
/// # fn demo(robot: &(impl MovePtp + AmendBlending), b1: JointPose, b2: JointPose) -> Result<()> {
/// robot.move_ptp(b1).start()?;
/// robot.amend_blending(Percent(50.0))?;
/// robot.move_ptp(b2).start()?.join()?;
/// # Ok(())
/// # }
/// ```
///
/// # Arguments
///
/// * `blending` - The blending of the previous motion: a
/// [`Percent`](crate::types::Percent) in `0.0..=100.0`, a
/// [`Factor`](crate::types::Factor) in `0.0..=1.0`, or a radius in
/// [`Meters`](crate::types::Meters).
///
/// # Returns
///
/// `Ok(())` on success.
///
/// # Errors
///
/// * Returns [`RobotArmError::NotAllowed`](crate::error::RobotArmError::NotAllowed)
/// with [`NotAllowedError::NoMotionToAmend`](crate::error::NotAllowedError::NoMotionToAmend)
/// if there is no motion instruction to amend: none was sent, or the
/// last one has ended, which a wait, a stop, a robot error or a
/// reconnect do as well.
/// * Returns [`RobotArmError::NotAllowed`](crate::error::RobotArmError::NotAllowed)
/// with [`NotAllowedError::BlendingAlreadySpecified`](crate::error::NotAllowedError::BlendingAlreadySpecified)
/// if the previous motion instruction already specified blending.
/// * Returns [`RobotArmError::UnitLimit`](crate::error::RobotArmError::UnitLimit)
/// if the value is out of range; nothing is changed then.
fn amend_blending(&self, blending: impl Into<BlendingValue>) -> Result<()>
where
Self: Sized,
{
self.execute_amend_blending(AmendBlendingRequest {
blending: blending.into(),
})
}
}