Monitor a running plan
MoveOnMap and MoveOnGlobe return immediately with an ExecutionID and run
the actual motion in the background. Your code has to poll that execution to
learn when it completes, fails, or stops. This page gives the polling pattern
in Go and Python. For the built-in Move, which blocks until the motion
finishes, the pattern collapses to a try/except: covered at the end.
Before you start
MoveOnMapandMoveOnGlobeare not implemented by the built-in motion service. You need the navigation service (which calls them internally) or a motion-service module that provides them. The built-in motion service returns “not supported” for both.- For background on the methods and states, see Plan monitoring.
The polling pattern
- Start a non-blocking motion. Save the returned
ExecutionID. - Poll
GetPlanon an interval, passing the component name and theExecutionID. - Inspect the returned
current_plan_with_status.status.state. - Exit the loop on any terminal state:
SUCCEEDED,STOPPED, orFAILED.
import asyncio
from viam.services.motion import MotionClient
from viam.proto.common import GeoPoint
from viam.proto.service.motion import PlanState
motion_service = MotionClient.from_robot(machine, "my-motion-service")
# Start a non-blocking motion.
destination = GeoPoint(latitude=40.6640, longitude=-73.9387)
execution_id = await motion_service.move_on_globe(
component_name="my-base",
destination=destination,
movement_sensor_name="my-gps",
)
# Poll until the plan reaches a terminal state.
terminal = {
PlanState.PLAN_STATE_SUCCEEDED,
PlanState.PLAN_STATE_STOPPED,
PlanState.PLAN_STATE_FAILED,
}
while True:
response = await motion_service.get_plan(
component_name="my-base",
execution_id=execution_id,
)
status = response.current_plan_with_status.status
print(f"state={status.state} reason={status.reason or '-'}")
if status.state in terminal:
break
await asyncio.sleep(1)
if status.state == PlanState.PLAN_STATE_SUCCEEDED:
print("Arrived at destination")
else:
print(f"Motion did not complete: {status.state}")
Go callers can either poll directly, or use the
PollHistoryUntilSuccessOrError helper that wraps the loop and returns
on the first terminal state.
import (
"context"
"time"
geo "github.com/kellydunn/golang-geo"
"go.viam.com/rdk/services/motion"
)
// Start a non-blocking motion.
executionID, err := motionService.MoveOnGlobe(ctx, motion.MoveOnGlobeReq{
ComponentName: "my-base",
Destination: geo.NewPoint(40.6640, -73.9387),
MovementSensorName: "my-gps",
})
if err != nil {
logger.Fatal(err)
}
// Block until the plan reaches a terminal state.
err = motion.PollHistoryUntilSuccessOrError(
ctx,
motionService,
time.Second,
motion.PlanHistoryReq{
ComponentName: "my-base",
ExecutionID: executionID,
},
)
if err != nil {
logger.Warnf("motion did not complete: %v", err)
} else {
logger.Info("Arrived at destination")
}
Stop a running plan
Call StopPlan with the component name to cancel an in-progress motion.
The plan’s final state becomes STOPPED.
await motion_service.stop_plan(component_name="my-base")
err = motionService.StopPlan(ctx, motion.StopPlanReq{ComponentName: "my-base"})
Replanning and ExecutionID
If the motion service replans during execution (for example, after the
robot deviates from the path), it creates a new Plan but keeps the
same ExecutionID. This means:
GetPlanwith theExecutionIDreturns the latest plan incurrent_plan_with_statusand earlier plans inreplan_history.- Polling the
execution_idcontinues to work across replans. - The plan state transitions you observe reflect the most recent plan.
List plans across components
ListPlanStatuses returns every plan currently running or recently completed
on the machine, keyed by component name. Two common uses: a dashboard that
shows all in-progress motion at once, and a shutdown routine that stops every
component without having to track the ExecutionID of each one.
statuses = await motion_service.list_plan_statuses()
for s in statuses:
print(f"{s.component_name}: {s.status.state}")
statuses, err := motionService.ListPlanStatuses(ctx, motion.ListPlanStatusesReq{})
if err != nil {
logger.Fatal(err)
}
for _, s := range statuses {
logger.Infof("%s: %v", s.ComponentName, s.Status.State)
}
ListPlanStatuses has a 24-hour retention window. Plans from executions that
reached a terminal state more than 24 hours ago are dropped from the list, and
any restart of viam-server clears the history regardless of age. If you need
longer-lived plan audit, log the ExecutionID and plan state to your own
store at each poll.
If you are using the builtin Move instead
The builtin motion service’s Move call blocks until the motion
completes or fails. Polling is not needed; handle the return value
directly.
try:
await motion_service.move(
component_name="my-arm",
destination=destination,
)
print("Motion succeeded")
except Exception as e:
print(f"Motion failed: {e}")
_, err = motionService.Move(ctx, motion.MoveReq{
ComponentName: "my-arm",
Destination: destination,
})
if err != nil {
logger.Warnf("motion failed: %v", err)
} else {
logger.Info("motion succeeded")
}
What’s next
- Plan monitoring: method, type, and state reference.
- Navigate to a waypoint:
end-to-end example using the navigation service, which internally
uses
MoveOnGlobeand exposes its own progress API. - Move an arm to a pose:
the builtin
Moveequivalent for arms and gantries.
Was this page helpful?
Glad to hear it! If you have any other feedback please let us know:
We're sorry about that. To help us improve, please tell us what we can do better:
Thank you!