Lifecycle of a Robot
A robot transitions through multiple states during its operation, often for safety and convenience purposes. The robot’s state is also essential for error handling within the automation system it is part of. Decisions must be based on the robot’s current state, so it is crucial to both read and manipulate this state.
The state of a robot is the main state of its robot control, a RobotState. The Lifecycle trait moves the robot
between these states: it connects to the robot control, switches the regulators on and off, and disconnects again.
Reading the state is part of the GetRobotState trait, which every robot implements; see
Reading the Robot State.
Note
To interact with a robot successfully, this library assumes a stable network connection to the robot control. Ensure that the robot control is reachable from the machine running this library.
The States of a Robot
The robot control reports one of the states of the RobotState enum:
State |
Description |
|---|---|
|
The robot control starts and waits for its peripheral hardware. |
|
Initialized, regulators off: the robot cannot move. |
|
Regulators on, the robot is not moving and waits for instructions. |
|
The interpolator produces set values for a motion. |
|
A critical stop: the robot decelerates to a standstill. |
|
Gravitation compensation mode. |
|
A collision was detected and the configured reaction runs. |
|
An error occurred; the robot stays in this state until it is reset. |
|
The robot control clears its errors. |
|
The robot control stops the robot, switches the regulators off and terminates. |
RobotState::is_enabled() tells whether the regulators are on (Ready, Active, Stopping, GravComp and
CollisionReaction), and RobotState::accepts_motions() whether the robot control takes motion instructions (Ready
and Active). The enum is #[non_exhaustive], so a match on it needs a wildcard arm; the two helpers cover the
common questions without one.
The typical transitions are:
not connected --connect()--> Standby --enable()--> Ready
^ |
+-----disable()------+
connect() returns once the robot control has booted to Standby, waiting at most the command timeout, so a
control that boots for longer needs a longer timeout; a robot control that is enabled when the robot connects is
disabled first. enable() returns once the robot control takes motion instructions, so a motion can be
started right after it. Error is entered by the robot control itself and left through the error handler, see
Resetting Errors. A robot in the error state still connects, but enable() rejects it with
RobotArmError::WrongState until it is reset. The errors name the state the robot control was in, for example
WrongState { operation: "enable", state: CollisionReaction }.
A robot that enters the error state during enable() or disable() makes the call return
RobotArmError::StateNotReached at once rather than after the command timeout.
Whether the robot is connected is not a state of the robot control but of the robot instance, and is_connected()
tells it. A connected robot counts as not connected once its connection is lost: the robot control closed it, or it
dropped and could not be restored. A request that fails or times out does not count as a lost connection by itself;
it fails with its own error while the robot stays connected. Once the connection is lost, pending motions end with
RobotArmError::CommunicationEnded, the lifecycle calls and the state readers fail with
RobotArmError::NotConnected, the other calls with the backend’s error, and connect() connects the robot again, no
disconnect() needed first.
Creating a Robot
Create a robot instance of the desired type with VorausIndustrialRobotArm::new(); it talks OPC UA to the
voraus.core.
Every lifecycle transition waits for the robot control to reach the target state. The wait is bounded by the robot’s
command timeout, which also bounds every other command (stop, pause, time override, and the configuration
calls). It defaults to DEFAULT_COMMAND_TIMEOUT and can be changed with set_command_timeout() from the
CommandTimeout trait. The command timeout bounds only these waits, not the requests to the control themselves.
Those are bounded by the limits of the connection, a ConnectionTimeouts value the robot’s backend takes before it
connects. They default to DEFAULT_CONNECT_TIMEOUT for establishing the connection, DEFAULT_REQUEST_TIMEOUT for each
read or call and DEFAULT_DISCONNECT_TIMEOUT for closing the connection, all in the constants module. To change them
for one robot, build it before connecting with a backend that takes them, for the voraus.core as
VorausIndustrialRobotArm::with_backend(OpcUaBackend::with_timeouts(timeouts)); VorausErrorHandler::with_timeouts()
does the same for the error handler.
robot.set_command_timeout(Duration::from_secs(10));
Checking the Connection
is_connected() tells whether the robot is connected. After creation, it is not:
// Initially not connected.
info!("Connected: {}", robot.is_connected()); // false
Connecting to a Robot
To establish a connection, the host of the robot control (an IP address or a hostname) and its port must be
provided. The example reads them from the environment variables VORAUS_CORE_HOST and VORAUS_CORE_PORT:
robot.connect(&common::robot_host(), common::robot_port())?;
info!("Current state: {:?}", robot.robot_state()?); // Standby
Enabling the Robot
Most actions require the robot to be in a moveable state. This is done with enable(), which returns once the robot
control takes motion instructions:
robot.enable()?;
info!("Current state: {:?}", robot.robot_state()?); // Ready
Afterwards, the main application logic can be implemented using the functionality provided by this library, together with any other Rust code that helps to solve the given problem.
Once the robot is no longer required to be moveable, switch back with disable(). Since switching the regulators off
while the axes still move could harm the mechanics, disable() first waits until the robot stands still, bounded by the
command timeout, and returns DidNotStop with the robot still enabled if it does not come to rest in time:
robot.disable()?;
info!("Current state: {:?}", robot.robot_state()?); // Standby
Disconnecting
Finally, disconnect() closes the connection:
robot.disconnect()?;
info!("Connected: {}", robot.is_connected()); // false
disable() and disconnect() contain cleanup, so they should always run. disconnect() disables a robot that is
still enabled and stops a running heartbeat session, in that order, before it closes the connection, so the heartbeat
stays armed while the robot comes to rest; a pending stop or pause does not carry over to the next connection. A failed
disable or heartbeat stop, such as a robot that does not come to rest in time and therefore stays enabled, is logged and
the connection is closed anyway. A robot that is still enabled keeps its heartbeat tracker on the control, and so does
one behind a dead link, where the tracker cannot be removed; the tracker then times out and puts the robot into the
error state until it is reset (see resetting errors). The robot structs implement Drop to
guarantee the cleanup: dropping a connected robot disconnects it. Failures are logged. In most applications the robot
is simply left to go out of scope. Call the methods explicitly when the outcome matters. Note that the cleanup does
blocking network I/O, which can take longer than the command timeout if the control stopped answering (see the
connection limits above), and that std::process::exit skips it, since it ends the process without running
destructors.
A Note on Receivers
The lifecycle methods take &mut self: connect, enable, disable, and disconnect are state transitions of the robot
handle, and exclusive access prevents two threads from racing them. Motion and query methods take &self, because
issuing instructions or reading state does not change what the handle is, and applications routinely do both from
several threads at once.
Full Example
Lifecycle example
//! Lifecycle example: connect, enable, read the state, disable, disconnect.
mod common;
use std::time::Duration;
use tracing::info;
use voraus_robot_arm::prelude::*;
fn main() -> voraus_robot_arm::error::Result<()> {
common::init_logging();
// create_connected_robot() returns an already-enabled robot. For the
// lifecycle demo we want to start from the disconnected state, so we only
// create the robot here.
let mut robot = common::create_robot();
// Every command that waits for the control (connect, enable, stop, ...)
// is bounded by one robot-level timeout. Default: 5 s.
robot.set_command_timeout(Duration::from_secs(10));
// Initially not connected.
info!("Connected: {}", robot.is_connected()); // false
// Connect to the robot. Returns once the control has booted.
robot.connect(&common::robot_host(), common::robot_port())?;
info!("Current state: {:?}", robot.robot_state()?); // Standby
// Enable the robot (regulators on, ready to move).
robot.enable()?;
info!("Current state: {:?}", robot.robot_state()?); // Ready
// <configure your application here>
// Disable the robot (regulators off, still connected).
robot.disable()?;
info!("Current state: {:?}", robot.robot_state()?); // Standby
// Disconnect explicitly to observe the result. Without this call the
// robot's `Drop` would disable (if needed) and disconnect, logging any
// failure.
robot.disconnect()?;
info!("Connected: {}", robot.is_connected()); // false
Ok(())
}
Trait Definitions
Lifecycle
pub trait Lifecycle {
/// Connect to the robot at the given host and port.
///
/// Returns once the robot control has booted to
/// [`Standby`](crate::types::RobotState::Standby). A control found
/// enabled is disabled first. A robot in the error state connects
/// successfully and stays in `Error`; reset it with
/// [`VorausErrorHandler`](crate::voraus_error_handler::VorausErrorHandler)
/// before calling [`enable`](Self::enable). A failed connect leaves the
/// robot disconnected, so the call can be retried.
///
/// # Arguments
///
/// * `host` - The host name or IP address of the robot control.
/// * `port` - The TCP port the robot control listens on.
///
/// # Returns
///
/// `Ok(())` on success.
///
/// # Errors
///
/// Returns [`RobotArmError::NotAllowed`](crate::error::RobotArmError::NotAllowed)
/// if the robot is already connected,
/// [`RobotArmError::StateNotReached`](crate::error::RobotArmError::StateNotReached)
/// if the robot control does not reach `Standby` or `Error` within the
/// command timeout, at once if it shuts down,
/// [`RobotArmError::Backend`](crate::error::RobotArmError::Backend) if the
/// connection cannot be established, or the error a call to the robot
/// control failed with.
fn connect(&mut self, host: &str, port: u16) -> Result<()>;
/// Enable the robot (regulators on, ready to move).
///
/// Returns once the robot control takes motion instructions, see
/// [`RobotState::accepts_motions`](crate::types::RobotState::accepts_motions).
///
/// # Returns
///
/// `Ok(())` on success.
///
/// # Errors
///
/// Returns [`RobotArmError::NotConnected`](crate::error::RobotArmError::NotConnected)
/// if the robot is not connected,
/// [`RobotArmError::WrongState`](crate::error::RobotArmError::WrongState)
/// if the robot control is not in
/// [`Standby`](crate::types::RobotState::Standby), or
/// [`RobotArmError::StateNotReached`](crate::error::RobotArmError::StateNotReached)
/// if it does not take motions within the command timeout, at once if it
/// enters the error state.
fn enable(&mut self) -> Result<()> {
self.send_enable()?.confirm()
}
/// Send the enable.
///
/// The call returns before the robot is enabled. Call
/// [`confirm`](CommandHandle::confirm) on the returned handle to wait
/// until the robot is enabled and accepts instructions. The handle
/// borrows the robot mutably, so no other lifecycle call can run before
/// it is confirmed or dropped. If you drop the handle, the next
/// lifecycle call can still see the state before the enable. A
/// [`send_disable`](Self::send_disable) right after can then fail with
/// [`RobotArmError::WrongState`](crate::error::RobotArmError::WrongState)
/// or reach the control while it still enables.
///
/// # Returns
///
/// The handle that confirms the enable. [`CommandHandle::confirm`]
/// returns
/// [`RobotArmError::StateNotReached`](crate::error::RobotArmError::StateNotReached)
/// at once if the robot enters the error state, and after the command
/// timeout if it does not take motions.
///
/// # Errors
///
/// Returns [`RobotArmError::NotConnected`](crate::error::RobotArmError::NotConnected)
/// if the robot is not connected,
/// [`RobotArmError::WrongState`](crate::error::RobotArmError::WrongState)
/// if the robot control is not in
/// [`Standby`](crate::types::RobotState::Standby), or the error the
/// enable call failed with.
fn send_enable(&mut self) -> Result<CommandHandle<'_>>;
/// Disable the robot (regulators off, still connected).
///
/// The robot control refuses to switch the regulators off while the
/// axes move, so the call first waits until the robot stands still,
/// bounded by the command timeout.
///
/// # Returns
///
/// `Ok(())` on success.
///
/// # Errors
///
/// Returns [`RobotArmError::NotConnected`](crate::error::RobotArmError::NotConnected)
/// if the robot is not connected,
/// [`RobotArmError::WrongState`](crate::error::RobotArmError::WrongState)
/// if the regulators are off, see
/// [`RobotState::is_enabled`](crate::types::RobotState::is_enabled),
/// [`RobotArmError::DidNotStop`](crate::error::RobotArmError::DidNotStop)
/// without switching the regulators off if the robot does not come to
/// rest within the command timeout, or
/// [`RobotArmError::StateNotReached`](crate::error::RobotArmError::StateNotReached)
/// if the robot control does not reach
/// [`Standby`](crate::types::RobotState::Standby) within the command
/// timeout.
fn disable(&mut self) -> Result<()> {
self.send_disable()?.confirm()
}
/// Wait until the robot stands still, then send the disable.
///
/// The call returns before the robot is disabled. The wait for
/// standstill happens before the call returns, because the robot
/// control refuses the disable while the axes move. Call
/// [`confirm`](CommandHandle::confirm) on the returned handle to wait
/// until the robot is disabled. If you drop the handle, the next
/// lifecycle call can still see the state before the disable. A
/// [`send_enable`](Self::send_enable) right after can then fail with
/// [`RobotArmError::WrongState`](crate::error::RobotArmError::WrongState)
/// or reach the control while it still disables.
///
/// # Returns
///
/// The handle that confirms the disable. [`CommandHandle::confirm`]
/// returns
/// [`RobotArmError::StateNotReached`](crate::error::RobotArmError::StateNotReached)
/// at once if the robot enters the error state, and after the command
/// timeout if the robot control does not reach
/// [`Standby`](crate::types::RobotState::Standby).
///
/// # Errors
///
/// Returns [`RobotArmError::NotConnected`](crate::error::RobotArmError::NotConnected)
/// if the robot is not connected,
/// [`RobotArmError::WrongState`](crate::error::RobotArmError::WrongState)
/// if the robot is not enabled,
/// [`RobotArmError::DidNotStop`](crate::error::RobotArmError::DidNotStop)
/// without switching the regulators off if the robot does not come to
/// rest within the command timeout, or the error the disable call failed
/// with.
fn send_disable(&mut self) -> Result<CommandHandle<'_>>;
/// Disconnect from the robot.
///
/// A robot that is still enabled is disabled and a running heartbeat
/// session is stopped first, in that order, so the heartbeat stays
/// armed while the robot comes to rest. A failed disable or heartbeat
/// stop, such as a robot that does not come to rest in time, is
/// logged and the connection is closed anyway. A robot that is still
/// enabled keeps a heartbeat tracker on the control, which then times
/// out into its heartbeat error. A pending stop or pause does not carry
/// over to the next connection, and the robot can be connected again
/// with [`connect`](Self::connect).
///
/// # Returns
///
/// `Ok(())` on success.
///
/// # Errors
///
/// Returns [`RobotArmError::NotConnected`](crate::error::RobotArmError::NotConnected)
/// if the robot is already disconnected, or the error the session
/// teardown failed with.
fn disconnect(&mut self) -> Result<()>;
/// Whether the robot is connected.
///
/// # Returns
///
/// `false` before [`connect`](Self::connect), after
/// [`disconnect`](Self::disconnect) and once the connection is lost.
fn is_connected(&self) -> bool;
}
CommandTimeout
pub trait CommandTimeout {
/// How long commands wait for the robot control to confirm them.
///
/// # Returns
///
/// The currently configured command timeout.
fn command_timeout(&self) -> Duration;
/// Set how long commands wait for the robot control to confirm them.
///
/// # Arguments
///
/// * `timeout` - The new command timeout.
fn set_command_timeout(&mut self, timeout: Duration);
}