fleetlessfleetlessdocs
Reference/fleetless.yaml/Overview

📄 fleetless.yaml

One file configures one robot: the values it exposes, the actions and services it offers, the topics clients may send to, the cameras it streams.

The file says nothing about the robot’s identity — no name, no id, no organisation. A file downloaded from one robot uploads unchanged onto another. Comments and key order survive the round trip, which is why this is a file rather than four forms.

yaml
fleetless: 1

datapoints:
  battery_soc:
    topic: /battery
    type: sensor_msgs/msg/BatteryState
    field: percentage
    description: Main battery charge.
    numeric:
      scale: 100
      unit: "%"

publishers:
  drive:
    topic: /cmd_vel
    type: geometry_msgs/msg/Twist
    description: Velocity command. If sending stops, the robot stops.
    message:
      linear: { x: "${linear_speed}", y: 0.0, z: 0.0 }
      angular: { x: 0.0, y: 0.0, z: 0.0 }
    parameters:
      linear_speed:
        type: float64
        min_value: -0.5
        max_value: 0.5
    failsafe:
      timeout_ms: 500
      message:
        linear: { x: 0.0, y: 0.0, z: 0.0 }
        angular: { x: 0.0, y: 0.0, z: 0.0 }
    quiet_timeout_ms: 2000

That is a complete document. Every other key in the format is optional or lives under one of these.

Sections

section what it holds
datapoints Values the robot publishes: one field of one topic, or a whole topic.
alerts A state machine on one datapoint’s value, nested under it.
messages, parameters The message template a call sends, and the holes a caller fills.
actions, services Things the robot does on request, long-running and short.
publishers Topics clients may send to, with the failsafe that stops them.
cameras Video the robot streams, and the stills the cloud serves from it.
low_bandwidth Rate and camera limits the bridge applies while the link is bad.

The header

yaml
fleetless: 1

Required, an integer, and conventionally the first line. It decides how everything below is read, so a file that omits it — or names a version this cloud does not know — is refused rather than half understood.

The key is deliberately not version. The console counts published states as v12 to v13, and two numbers both called version would be the likeliest confusion the format could offer.

Names

Slugs, parameter names and shared message names share one grammar:

^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$

Lowercase, underscore-joined, letter-initial, 2 to 63 characters. Valid: battery_soc, dock, linear_speed. Refused: Battery, 1st_cam, battery-soc, battery__soc, dock_.

Slugs are one namespace across the five exposure sections — datapoints, actions, services, publishers and cameras. A datapoint and a camera cannot share a slug, which is what lets a role grant name {robot, slug} without ever naming a kind. Shared message names are their own namespace, and a parameter name is scoped to the entry that declares it.

Three slugs are reserved, and naming one in any of the five sections is refused as reserved_slug:

slug why
bridge_state Built in on every robot: whether the bridge is connected, and its latency when it is.
robot_details Built in: the static properties a developer maintains about the robot.
history GET /api/robots/:id/jobs/history is a real route, so a job slug spelled history would sit at the same address.

A slug is stable and decoupled from ROS names. Renaming a topic on the robot changes one line here and breaks no client. Changing a slug breaks every client, every role grant and every MCP call that named it.

ROS names, ROS types and field paths keep ROS’s own spelling — they are not ours to restyle.

Two rules

Omission is the only way to say “not set”. An explicit null is refused, at any depth of a message body as much as at a top-level key. One spelling, not two.

An unknown key is an error. The refusal is unknown_key, one issue per key, so it names the offending key rather than its parent. A typo in a field name that is silently ignored is the most expensive kind of silence.

Save vs. publish

A draft is stored whenever the text parses as YAML, whatever it says. Text that is not this format at all is kept with no document behind it, so a half-written file is never lost. The editor lists the findings as you type, and publish is what enforces them.

refusal code where
The text is not YAML at all invalid_yaml save, 422
The text is YAML that cannot be stored, such as an anchor cycle unstorable_yaml save, 422
The draft is YAML but not a configuration draft_not_a_document publish, 422
The document has blocking findings validation_error publish, 422

The line and column of a syntax error travel in the refusal’s details, so an editor can put a marker on the offending line rather than merely declining. A slug rename asked of a draft that is not a document answers draft_not_a_document as well, there as a 409.

Not every finding blocks. A topic the last introspection did not see, or a type it has never resolved, is a warning: the robot may simply not have connected yet.

What reaches the robot

The robot is sent the document unmutated — every key, including every one in the right-hand column. The table is about where a field has its effect, never about what is transmitted.

the robot acts on it only the cloud acts on it
topic, type, field, rate_throttle_hz retention.enabled, retention.interval_seconds
numeric.scale, numeric.offset numeric.unit, numeric.decimals
retention.max_buffer_values a datapoint’s description
actions, services, publishers, messages chart, alerts
cameras, credentials included

A published version reaches the bridge only when something in the left column changed. Otherwise a corrected alert threshold would push a configuration to a robot, which is churn for nothing on a link that is often a phone connection. The comparison is a projection of the robot-affecting fields, so a key the format grows later is cloud-only until somebody says otherwise.

The split inside numeric is worth reading twice. scale and offset are arithmetic the robot performs before sending, so changing them changes what the robot does. unit and decimals are labels the cloud puts on the result.

Descriptions split along the same line and not the way you would guess. A datapoint’s description is cloud-only, but an action, service, publisher or camera is compared whole, so rewording one of those descriptions does push a new configuration.

Alerts and charts therefore take effect only on publish. That is the price of one door, and in exchange every change to them is versioned, comparable and revertible.

Limits

what maximum
datapoints, actions, services, publishers, messages 200 entries each
cameras 50 entries
parameters per entry 50
a slug, parameter name or message name 63 characters
a description 2000 characters
a parameter description 500 characters
numeric.unit 32 characters
an alert name 120 characters
a ROS name, ROS type name or field path 255 characters
a camera url 2048 characters
a camera device 128 characters
a credential username or password 128 characters