Mission (Open-RMF)
Description
The Open-RMF adapter of the Mission Measurement for Open-RMF's TaskState (the document
rmf-web's API server maintains per task). Emits a mission_start Record when a task's status
first leaves queued/standby into an active state, and a mission_end Record once it reaches a
terminal status, in the same mission_start/mission_end Record schema #387 defines --
mission_id sourced from Open-RMF's own booking.id, mission_type from category (Open-RMF
tasks have a real category/type, unlike nav2's bare goal).
Unlike the nav2 adapters (mission_nav2/mission_nav2_follow_waypoints/
mission_nav2_through_poses), Open-RMF exposes no ROS action or topic for task lifecycle. This
Measurement instead consumes TaskState as a stream of JSON documents over a plain ws://
connection, using the reusable dc_common::WebSocketJsonClient. The websocket protocol
rmf-web's API server actually speaks on its own endpoints (Socket.IO framing) is out of scope
-- websocket_url must point at an endpoint that re-emits the task-state feed as one TaskState
JSON object per plain text frame; bridging the real rmf-web wire protocol is a separate piece of
infrastructure this Measurement assumes already exists, not something it does itself.
This Measurement is a passive observer. It never dispatches, cancels, or otherwise mutates any Open-RMF task -- it only watches the state stream.
Status mapping
Open-RMF's task model is richer than a flat start/end pair: TaskState.status is a
continuously-updated 12-value enum (uninitialized, blocked, error, failed, queued,
standby, underway, delayed, skipped, canceled, killed, completed). This
Measurement still boils it down to two Records per mission:
- a
mission_startRecord, the first time abooking.idis observed leavingqueued/standbyinto an active state --underway,delayed, or (once already active)blocked/error. - a
mission_endRecord, once thatbooking.idreaches a terminal status:completed,failed,canceled,killed, orskipped.
outcome on the end Record is one of succeeded, failed, cancelled, aborted:
completed→succeeded, failed→failed, canceled→cancelled, killed→aborted.
Two cases the acceptance criteria asked to be resolved explicitly, not silently defaulted:
skippedis treated as task-terminal, and maps to outcomecancelled. It is defined in the samestatusenumtask_state.jsonuses for the task's own top-levelstatusfield, the same enum that also carries in-progress phases, so a task can legitimately end its life withstatus: "skipped". Open-RMF'sskippedmeans the task's work was bypassed rather than performed, which is closer to DC'scancelled(the work simply didn't complete) than tosucceeded(the work was done) or a failure outcome. DC's outcome contract (#305/#387) has exactly four values; this Measurement maps into those four rather than adding a fifthskippedoutcome.blockedanderrorare treated as transient. Both describe a task Open-RMF is still actively trying to resolve or recover (a blocked path, a recoverable fault) -- they are absent fromtask_state.json's terminal set, so its task manager never settles on either as the end of the task's life. A task observed asblocked/errorstays open; only a later terminal status closes it, and it may still resolve back tounderway.
reason is populated on a best-effort basis, carried verbatim from whichever part of TaskState
Open-RMF actually populated for that outcome -- dispatch.errors (or the top-level detail) for
failed, cancellation.labels for cancelled, killed.labels for aborted. Unlike nav2's
always-present error_msg, Open-RMF does not guarantee one of these for every outcome, so reason
(and error_code, only ever populated for failed) may be absent even on a mission_end Record.
duration_sec prefers Open-RMF's own unix_millis_start_time/unix_millis_finish_time when the
source provided both; it falls back to this Measurement's own locally observed start/end
timestamps otherwise.
What this Measurement does not represent
- No phase-level detail.
TaskState.phases/active/completed/pending(which step of a multi-phase task is running) is not carried into the Record. Only the task's own top-levelstatusdrivesmission_start/mission_end. - No interruptions.
TaskState.interruptions(temporary holds placed on a task, distinct from cancellation) is not represented at all -- an interrupted-then-resumed task simply keeps running from this Measurement's point of view. - No dispatch/assignment detail. Which fleet or robot a task was assigned to
(
TaskState.assigned_to/dispatch.assignment) is not carried into the Record.
A task still active when the websocket connection drops or collection stops simply never gets a
matching mission_end Record: nothing downstream can average an interval that was never closed,
because there is no mission_end Record to average -- matching #387's open-interval handling.
Symmetrically, a task whose first-ever observed sample is already active (started before this
Measurement connected) or already terminal (finished before this Measurement connected) is not
tracked at all: no mission_start Record (the active-transition boundary was never observed) and
consequently no mission_end Record either.
Parameters
| Parameter | Default | Description |
|---|---|---|
websocket_url | (required) | ws://host[:port][/path] of the endpoint streaming TaskState JSON, one object per text frame. |
reconnect_initial_backoff_ms | 500 | Delay before the first reconnect attempt after a dropped connection. |
reconnect_max_backoff_ms | 30000 | Reconnect delay never grows past this, however many attempts fail in a row. |
Schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MissionOpenRmf",
"properties": {
"event": { "type": "string", "enum": ["mission_start", "mission_end"] },
"mission_id": { "type": "string" },
"mission_type": { "type": "string" },
"sequence": { "type": "integer", "minimum": 1 },
"outcome": { "type": "string", "enum": ["succeeded", "failed", "cancelled", "aborted"] },
"reason": { "type": "string" },
"error_code": { "type": "integer", "minimum": 0 },
"duration_sec": { "type": "number", "minimum": 0 }
},
"required": ["event", "mission_id", "sequence"],
"type": "object"
}
Configuration
...
mission_open_rmf:
plugin: "dc_measurements/MissionOpenRmf"
topic_output: "/dc/measurement/mission_open_rmf"
group_key: "mission"
websocket_url: "ws://rmf-task-state-bridge:8080/task_states"
Example output
Start:
{
"event": "mission_start",
"mission_id": "delivery.dispenser_1.dispatch-14",
"mission_type": "delivery",
"sequence": 3
}
end (succeeded):
{
"event": "mission_end",
"mission_id": "delivery.dispenser_1.dispatch-14",
"mission_type": "delivery",
"sequence": 4,
"outcome": "succeeded",
"duration_sec": 214.7
}
end (cancelled, cancellation labels present):
{
"event": "mission_end",
"mission_id": "patrol.loop_a.dispatch-22",
"mission_type": "patrol",
"sequence": 9,
"outcome": "cancelled",
"reason": "operator; dashboard",
"duration_sec": 42.1
}
end (failed, dispatch error present):
{
"event": "mission_end",
"mission_id": "delivery.dispenser_2.dispatch-31",
"mission_type": "delivery",
"sequence": 15,
"outcome": "failed",
"reason": "dispenser_unavailable: dispenser_1 did not respond",
"error_code": 12,
"duration_sec": 8.9
}