Actions
Actions are for long-running tasks. They provide Goal semantics, streaming Feedback, and a final Result.
Golang Support
The mt_action library is written in Rust but it can be used from Golang as well. See our Go Guide on how to create a Go project for Minot.
mt_action implements the ROS2 action goal lifecycle natively over Minot Services and Pub/Sub. It handles:
- Goal Acceptance: The client picks the goal ID. A goal whose ID is already known is rejected.
- Goal Execution: Goals run one after another in the background, in the order they were accepted.
- Feedback Streaming: The running goal sends progress updates to every client.
- Result Reporting: A result request waits until the goal reaches a terminal state, then returns it once.
- Cancellation: Canceling one goal, all goals accepted before a given time, or all goals.
An action on /my_action uses these topics:
| Topic | Kind |
|---|---|
/my_action/_action/send_goal |
service |
/my_action/_action/cancel_goal |
service |
/my_action/_action/get_result |
service |
/my_action/_action/feedback |
topic, with the QoS you choose |
/my_action/_action/status |
topic, reliable |
A goal moves through these states:
| State | Meaning |
|---|---|
Accepted |
Waiting to run |
Executing |
Running |
Canceling |
A cancel request arrived, the action is stopping |
Succeeded, Aborted, Canceled |
Terminal, the result is ready |
Server
An action is a struct that implements the Action trait. perform_goal runs one goal. It sends feedback through feedback, watches stoken for cancellation, and must leave the goal in a terminal state.
use mt_action::runner::{Action, FeedbackSender, Goal};
use mt_action::{ActionServer, messages::GoalState};
use mt_pubsub::{Node, NodeConfig, Qos};
use ros2_interfaces_jazzy_rkyv::std_msgs::msg;
use std::sync::Arc;
use tokio::select;
use tokio::time::{Duration, sleep};
use tokio_util::sync::CancellationToken;
struct Countdown;
// Feedback, goal and result types
impl Action<msg::String, msg::String, msg::String> for Countdown {
async fn perform_goal(
&self,
goal: &mut Goal<msg::String, msg::String>,
stoken: CancellationToken,
feedback: FeedbackSender<msg::String>,
) -> anyhow::Result<()> {
for step in 1..=5u32 {
select! {
_ = sleep(Duration::from_secs(1)) => {
feedback.send(msg::String { data: format!("{}% complete", step * 20) }).await?;
}
_ = stoken.cancelled() => {
goal.state = GoalState::Canceled;
return Ok(());
}
}
}
goal.state = GoalState::Succeeded;
goal.result = Some(msg::String {
data: format!("Done with: {}", goal.data.data),
});
Ok(())
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let node = Arc::new(Node::create(NodeConfig::new("action_server")).await?);
let server = ActionServer::new(
node.clone(),
"/my_action".to_owned(),
Countdown,
Qos::Reliable, // feedback QoS
)
.await?;
// Runs until the node shuts down.
ActionServer::start(server, node.shutdown_token()).await?;
Ok(())
}
To abort a goal, set goal.state = GoalState::Aborted and return Ok(()).
Client
The client sends a goal, listens to its feedback, and waits for the result.
use anyhow::anyhow;
use mt_action::{ActionClient, messages::*};
use mt_pubsub::{Node, NodeConfig, Qos};
use ros2_interfaces_jazzy_rkyv::std_msgs::msg;
use std::sync::Arc;
use tokio::time::Duration;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let node = Arc::new(Node::create(NodeConfig::new("action_client")).await?);
// Feedback, goal and result types, and the feedback QoS
let client: Arc<ActionClient<msg::String, msg::String, msg::String>> =
ActionClient::new(node.clone(), "/my_action".to_owned(), Qos::Reliable).await?;
if !ActionClient::wait_for_action_server(client.clone(), Duration::from_secs(5)).await {
return Err(anyhow!("Action server not ready"));
}
// Feedback of every running goal
let feedback_client = client.clone();
tokio::spawn(async move {
while let Ok(fb) = ActionClient::get_feedback(feedback_client.clone()).await {
println!("Goal {}: {}", fb.id, fb.msg.data);
}
});
let goal = ActionSendGoalRequest {
id: 1,
goal: msg::String { data: "Start task".to_owned() },
};
let response = ActionClient::send_goal(client.clone(), goal, None)
.await
.map_err(anyhow::Error::msg)?;
if !response.accepted {
return Err(anyhow!("Goal rejected"));
}
// Waits until the goal is Succeeded, Aborted or Canceled
let result = ActionClient::get_result(client.clone(), ActionGetResultRequest { id: 1 }, None)
.await
.map_err(anyhow::Error::msg)?;
println!("{:?}: {:?}", result.status, result.result.map(|r| r.data));
Ok(())
}
send_goal, cancel_goal and get_result are service calls. They take the same optional timeout as a service request and return Err(String) on failure. The result of a goal is handed out once. The server forgets the goal after delivering it.
Canceling
ActionCancelGoalRequest selects the goals to cancel. Only goals that are Accepted or Executing are affected.
id |
time |
Cancels |
|---|---|---|
Some(id) |
None |
That goal |
None |
Some(t) |
Every goal accepted at or before t (Unix milliseconds) |
Some(id) |
Some(t) |
That goal, and every goal accepted at or before t |
None |
None |
All goals |
let response = ActionClient::cancel_goal(
client.clone(),
ActionCancelGoalRequest { id: Some(1), time: None },
None,
)
.await
.map_err(anyhow::Error::msg)?;
println!("Canceling {:?}", response.goals);
get_status returns the state of every goal each time it changes.
Complete clients and servers are in mt_action/examples/ in the Minot repository.