Actions API

HORUS actions model long-running tasks with real-time feedback and cancellation. Define an action with the action! macro, build a server with ActionServerBuilder, and send goals with ActionClientNode or SyncActionClient.

Defining an Action

use horus::prelude::*;

action! {
    /// Navigate to a target pose.
    NavigateToPose {
        goal {
            x: f64,
            y: f64,
            theta: f64,
        }
        feedback {
            distance_remaining: f64,
            estimated_time_sec: f64,
        }
        result {
            success: bool,
            final_x: f64,
            final_y: f64,
        }
    }
}

This generates four types:

  • NavigateToPoseGoal — goal struct
  • NavigateToPoseFeedback — feedback struct
  • NavigateToPoseResult — result struct
  • NavigateToPose — zero-sized marker implementing the Action trait

Action Trait

MethodReturnsDescription
name()&'static strAction name (used as topic prefix)
goal_topic()String"{name}.goal"
cancel_topic()String"{name}.cancel"
result_topic()String"{name}.result"
feedback_topic()String"{name}.feedback"
status_topic()String"{name}.status"

GoalStatus

Lifecycle of a goal:

VariantTerminal?Description
PendingNoReceived but not yet processed
ActiveNoCurrently executing
SucceededYesCompleted successfully
AbortedYesFailed due to server error
CanceledYesCanceled by client request
PreemptedYesPreempted by higher-priority goal
RejectedYesValidation failed at acceptance
MethodReturnsDescription
is_active()boolPending or Active
is_terminal()boolReached a final state
is_success()boolSucceeded
is_failure()boolAborted, Canceled, Preempted, or Rejected

GoalPriority

ConstantValueDescription
HIGHEST0Critical goals
HIGH64Above normal
NORMAL128Default priority
LOW192Below normal
LOWEST255Background tasks

Lower value = higher priority. is_higher_than(other) compares priorities.


PreemptionPolicy

Controls what happens when a new goal arrives while one is active:

VariantDescription
RejectNewNew goals rejected while one is active
PreemptOldNew goals cancel active goals (default)
PriorityHigher priority preempts lower priority
Queue { max_size }Queue goals up to max_size

GoalResponse / CancelResponse

Server returns these from goal acceptance and cancel callbacks:

TypeVariantsMethods
GoalResponseAccept, Reject(String)is_accepted(), is_rejected(), rejection_reason()
CancelResponseAccept, Reject(String)is_accepted(), is_rejected(), rejection_reason()

ActionServerConfig

FieldTypeDefaultDescription
max_concurrent_goalsOption<usize>Some(1)Max simultaneous goals (None = unlimited)
feedback_rate_hzf6410.0Rate limit for feedback publishing
goal_timeoutOption<Duration>NoneAuto-abort after timeout
preemption_policyPreemptionPolicyPreemptOldHow to handle competing goals
result_history_sizeusize100How many results to keep in history

Builder methods: new(), unlimited_goals(), max_goals(n), feedback_rate(hz), timeout(dur), preemption(policy), history_size(n).


ActionError

VariantDescription
GoalRejected(String)Server rejected the goal
GoalCanceledGoal was canceled
GoalPreemptedGoal was preempted by higher priority
GoalTimeoutGoal timed out
ServerUnavailableNo action server running
CommunicationError(String)Topic I/O failure
ExecutionError(String)Server execution error
InvalidGoal(String)Goal validation failed
GoalNotFound(GoalId)Goal ID not recognized

ActionServerBuilder

MethodReturnsDescription
ActionServerBuilder::<A>::new()SelfCreate a new builder
on_goal(callback)SelfGoal acceptance callback (Fn(&Goal) -> GoalResponse)
on_cancel(callback)SelfCancel request callback (Fn(GoalId) -> CancelResponse)
on_execute(callback)SelfExecution callback (Fn(ServerGoalHandle) -> GoalOutcome)
with_config(config)SelfApply full ActionServerConfig
max_concurrent_goals(max)SelfShorthand for max concurrent goals
feedback_rate(rate_hz)SelfShorthand for feedback rate
goal_timeout(timeout)SelfShorthand for goal timeout
preemption_policy(policy)SelfShorthand for preemption policy
build()ActionServerNode<A>Build the server node

ActionServerNode

Implements Node — add it to a Scheduler to process goals.

MethodReturnsDescription
builder()ActionServerBuilder<A>Create a new builder
metrics()ActionServerMetricsGet server metrics snapshot

ActionServerMetrics

FieldTypeDescription
goals_receivedu64Total goals received
goals_acceptedu64Goals that passed acceptance
goals_rejectedu64Goals rejected by on_goal
goals_succeededu64Successfully completed
goals_abortedu64Aborted by server
goals_canceledu64Canceled by client
goals_preemptedu64Preempted by higher priority
active_goalsusizeCurrently executing
queued_goalsusizeWaiting in queue

ServerGoalHandle

Handle passed to the on_execute callback. Use it to publish feedback and finalize the goal.

Query Methods

MethodReturnsDescription
goal_id()GoalIdUnique goal identifier
goal()&A::GoalThe goal data
priority()GoalPriorityGoal priority level
status()GoalStatusCurrent status
elapsed()DurationTime since execution started
is_cancel_requested()boolClient requested cancellation
is_preempt_requested()boolHigher-priority goal wants to preempt
should_abort()booltrue if canceled or preempted — check this in loops

Action Methods

MethodReturnsDescription
publish_feedback(feedback)()Send feedback to client (rate-limited)
succeed(result)GoalOutcome<A>Complete successfully
abort(result)GoalOutcome<A>Abort with error
canceled(result)GoalOutcome<A>Acknowledge cancellation
preempted(result)GoalOutcome<A>Acknowledge preemption

GoalOutcome

VariantDescription
Succeeded(A::Result)Goal completed successfully
Aborted(A::Result)Server aborted execution
Canceled(A::Result)Client canceled
Preempted(A::Result)Preempted by higher priority

Methods: status() returns GoalStatus, into_result() extracts the result.


Server Example

use horus::prelude::*;

action! {
    MoveArm {
        goal { target_x: f64, target_y: f64, target_z: f64 }
        feedback { progress: f64, current_x: f64, current_y: f64, current_z: f64 }
        result { success: bool, final_x: f64, final_y: f64, final_z: f64 }
    }
}

let server = ActionServerNode::<MoveArm>::builder()
    .on_goal(|goal| {
        // Validate the goal
        if goal.target_z < 0.0 {
            GoalResponse::Reject("Z must be non-negative".into())
        } else {
            GoalResponse::Accept
        }
    })
    .on_cancel(|_goal_id| CancelResponse::Accept)
    .on_execute(|handle| {
        let goal = handle.goal();
        let mut progress = 0.0;

        while progress < 1.0 {
            // Check for cancellation
            if handle.should_abort() {
                return handle.canceled(MoveArmResult {
                    success: false,
                    final_x: 0.0, final_y: 0.0, final_z: 0.0,
                });
            }

            progress += 0.01;
            handle.publish_feedback(MoveArmFeedback {
                progress,
                current_x: goal.target_x * progress,
                current_y: goal.target_y * progress,
                current_z: goal.target_z * progress,
            });

            std::thread::sleep(10_u64.ms());
        }

        handle.succeed(MoveArmResult {
            success: true,
            final_x: goal.target_x,
            final_y: goal.target_y,
            final_z: goal.target_z,
        })
    })
    .preemption_policy(PreemptionPolicy::Priority)
    .goal_timeout(30_u64.secs())
    .build();

let mut scheduler = Scheduler::new();
scheduler.add(server).order(0).build();

ActionClientBuilder

MethodReturnsDescription
ActionClientBuilder::<A>::new()SelfCreate a new builder
on_feedback(callback)SelfFeedback callback (Fn(GoalId, &Feedback))
on_result(callback)SelfResult callback (Fn(GoalId, GoalStatus, &Result))
on_status(callback)SelfStatus change callback (Fn(GoalId, GoalStatus))
build()ActionClientNode<A>Build the client node

ActionClientNode

Implements Node — add it to a Scheduler alongside the server.

MethodReturnsDescription
builder()ActionClientBuilder<A>Create a new builder
send_goal(goal)Result<ClientGoalHandle, ActionError>Send with NORMAL priority
send_goal_with_priority(goal, priority)Result<ClientGoalHandle, ActionError>Send with specific priority
cancel_goal(goal_id)()Request cancellation
goal_status(goal_id)Option<GoalStatus>Query goal status
active_goals()Vec<GoalId>All active goal IDs
active_goal_count()usizeNumber of active goals
metrics()ActionClientMetricsGet client metrics

ActionClientMetrics

FieldTypeDescription
goals_sentu64Total goals sent
goals_succeededu64Successfully completed
goals_failedu64Aborted, canceled, preempted, or rejected
cancels_sentu64Cancel requests sent
active_goalsusizeCurrently active

ClientGoalHandle

Handle returned by send_goal(). Use it to monitor progress and get results.

Query Methods

MethodReturnsDescription
goal_id()GoalIdUnique goal identifier
priority()GoalPriorityGoal priority level
status()GoalStatusCurrent status
is_active()boolStill executing
is_done()boolReached terminal state
is_success()boolCompleted successfully
elapsed()DurationTime since goal was sent
time_since_update()DurationTime since last status change
result()Option<A::Result>Get result if completed
last_feedback()Option<A::Feedback>Most recent feedback

Blocking Methods

MethodReturnsDescription
await_result(timeout)Option<A::Result>Block until result or timeout
await_result_with_feedback(timeout, callback)Result<A::Result, ActionError>Block with feedback callback
cancel()()Send cancel request to server

SyncActionClient (Blocking)

Standalone blocking client — does not need a Scheduler.

MethodReturnsDescription
SyncActionClient::<A>::new()HorusResult<Self>Create and initialize
send_goal_and_wait(goal, timeout)Result<A::Result, ActionError>Send goal, block until result
send_goal_and_wait_with_feedback(goal, timeout, callback)Result<A::Result, ActionError>Block with feedback
cancel_goal(goal_id)()Request cancellation

Type alias: ActionClient<A> = SyncActionClient<A>

Example

use horus::prelude::*;

// Simple blocking usage (no scheduler needed)
let client = SyncActionClient::<MoveArm>::new()?;

let result = client.send_goal_and_wait_with_feedback(
    MoveArmGoal { target_x: 1.0, target_y: 2.0, target_z: 0.5 },
    30_u64.secs(),
    |feedback| {
        println!("Progress: {:.0}%", feedback.progress * 100.0);
    },
)?;

if result.success {
    println!("Arm reached ({:.1}, {:.1}, {:.1})",
        result.final_x, result.final_y, result.final_z);
}

GoalId

Unique identifier for each goal (Uuid-backed).

MethodReturnsDescription
GoalId::new()SelfGenerate a new unique ID
GoalId::from_uuid(uuid)SelfCreate from existing UUID
as_uuid()&UuidGet underlying UUID

See Also