Robot Namespaces

OLO uses robot namespaces to support both single-robot and multi-robot deployments. A namespace identifies the robot that a handle should operate on. In ROS terms, it is the prefix before robot topics and nodes, for example /{namespace}/joint_states.

Most robot programs should start from Client.robot(). It resolves the namespace once and exposes the service-specific handles for that robot.

from olo import Client
from olo.core import TopicInfo

with Client() as client:
    # Resolve the default robot namespace when unambiguous.
    robot = client.robot()
    print(robot.namespace or "<global>")

    joint_states = TopicInfo("joint_states", "sensor_msgs/msg/JointState")
    print(robot.core.get_latest(joint_states))

    # Handles always exist, but operations require runtime dependencies.
    # Kinematics pose requires MoveIt; navigation pose requires robot TF.
    print(robot.kinematics.pose())
    print(robot.navigation.current_pose())
import { connect } from "olo/web";
import type { TopicInfo } from "olo/core";

const client = connect();

// Resolve the default robot namespace when unambiguous.
const robot = await client.robot();
console.log(robot.namespace || "<global>");

const jointStates: TopicInfo = {
  name: "joint_states",
  msgType: "sensor_msgs/msg/JointState",
};
console.log(await robot.core.getLatest(jointStates));

// Handles always exist, but operations require runtime dependencies.
// Kinematics pose requires MoveIt; navigation pose requires robot TF.
console.log(await robot.kinematics.pose());
console.log(await robot.navigation.currentPose());

Capability handles

robot.frames, robot.kinematics, robot.locomotion, and robot.navigation are always present on the robot handle. Creating a robot does not contact MoveIt, Nav2, or the locomotion controller; each operation validates its own runtime dependencies. Locomotion validates its conventional odometry and command topics when a command starts. Model-dependent kinematics accessors fetch the robot model lazily on first use (and retry after a failed fetch). Call robot.kinematics.model / await robot.kinematics.getModel() or query robot.navigation.state() / await robot.navigation.state() and inspect state.ready at startup if you want a fail-fast readiness check before commanding motion. Use client.spatial, client.kinematics, client.locomotion, or client.navigation when you need the low-level service wrappers with an explicit namespace per call.

Default namespace resolution

When constructing a namespace-scoped handle without passing an argument, the SDK attempts to resolve the default namespace. If the namespace is ambiguous, the SDK raises an error rather than guessing. This keeps the common single-robot path short while making multi-robot code choose explicitly.

The same rules apply to client.platform.navigation.start / stop / deployment when namespace is omitted (via client.platform, which shares the appliance channel’s resolver).

Discovery result

Default behavior

Exactly one namespace

Uses that namespace

No explicit namespaces, but the appliance reports a global robot

Uses the global namespace, represented by ""

No namespace and no global robot

Raises olo.OloNotFound

More than one namespace

Raises olo.OloAmbiguousNamespace

Explicit namespace selection

Pass a namespace when the program targets a specific robot.

with Client() as client:
    left = client.robot("left_arm")
    right = client.robot("right_arm")

    print(left.namespace)
    print(right.namespace)
import { connect } from "olo/web";

const client = connect();
const left = await client.robot("left_arm");
const right = await client.robot("right_arm");

console.log(left.namespace);
console.log(right.namespace);

The empty string "" and "global" both mean the global namespace. Use them only when the appliance intentionally exposes an un-namespaced robot.

Service-specific handles

Use client.<service>.handle(...) when a program needs only one service surface or needs a low-level, explicit-namespace interface for fleet and diagnostic use. Each handle follows the same namespace resolution rules as client.robot(...).

with Client() as client:
    core = client.core.handle("fr3")
    frames = client.spatial.handle("fr3")
    arm = client.kinematics.handle("fr3")
import { connect } from "olo/web";

const client = connect();
const core = await client.core.handle("fr3");
const frames = await client.spatial.handle("fr3");
const arm = await client.kinematics.handle("fr3");

Core topic and node names

olo.core.CoreHandle topic operations take a olo.core.TopicInfo and resolve names using ROS conventions:

  • No leading /: relative to the robot namespace, for example status on a fr3 handle becomes /fr3/status.

  • Leading /: absolute and used as-is, for example /fleet/status stays /fleet/status.

Use olo.core.CoreHandle.topic() when you need the resolved absolute form for logging or comparison. Node names are prefixed without a leading slash, matching ROS parameter service names such as fr3/controller_manager.

with Client() as client:
    robot = client.robot("fr3")
    status = TopicInfo("status", "std_msgs/msg/String")
    fleet_status = TopicInfo("/fleet/status", "std_msgs/msg/String")

    assert robot.core.topic(status.name, status.msg_type).name == "/fr3/status"
    assert robot.core.topic(fleet_status.name, fleet_status.msg_type).name == "/fleet/status"

    robot.core.publish(status, {"data": "local"})
    robot.core.publish(fleet_status, {"data": "fleet-wide"})
    params = robot.core.get_params("controller_manager", ["update_rate"])
import { connect } from "olo/web";
import type { TopicInfo } from "olo/core";

const client = connect();
const robot = await client.robot("fr3");
const status: TopicInfo = { name: "status", msgType: "std_msgs/msg/String" };
const fleetStatus: TopicInfo = { name: "/fleet/status", msgType: "std_msgs/msg/String" };

console.assert(robot.core.topic(status.name, status.msgType).name === "/fr3/status");
console.assert(robot.core.topic(fleetStatus.name, fleetStatus.msgType).name === "/fleet/status");

await robot.core.publish(status, { data: "local" });
await robot.core.publish(fleetStatus, { data: "fleet-wide" });
const params = await robot.core.getParams("controller_manager", ["update_rate"]);

Robot TF frames

olo.spatial.RobotFrames provides namespace-scoped TF access. Bare frame names such as base_link are resolved server-side to {namespace}/base_link; global frames such as world, map, and odom are never prefixed. client.spatial.handle(...) and robot.frames always return a scoped subtree, not the full TF tree. Use olo.spatial.Spatial.tree() for the complete tree across all robots.

with Client() as client:
    robot = client.robot("fr3")

    base = robot.frames.base_frame()
    world_to_base = robot.frames.lookup("world", "base_link")
    print(robot.frames.tree())

    print(client.spatial.tree())  # Full tree across all robots.
import { connect } from "olo/web";

const client = connect();
const robot = await client.robot("fr3");

const base = await robot.frames.baseFrame();
const worldTBase = await robot.frames.lookup("world", "base_link");
console.log(await robot.frames.tree());

console.log(await client.spatial.tree()); // Full tree across all robots.

Use olo.spatial.Spatial.lookup() directly when you need exact frame ids without namespace resolution.

Low-level calls

Low-level service methods keep their wire-level defaults and do not resolve a default robot namespace. Use them when writing fleet tools that need to pass namespaces around directly or preserve exact wire-level behavior.

with Client() as client:
    topics = client.core.list_topics()
    model = client.kinematics.get_model(robot_namespace="fr3")
import { connect } from "olo/web";

const client = connect();
const topics = await client.core.listTopics();
const model = await client.kinematics.getModel("fr3");

Listing namespaces

Use olo.core.Core.list_robot_namespaces() when a program wants to present a choice to a user or make its own policy decision. The returned olo.core.RobotNamespaces also exposes resolve_default() for code that wants the same resolve-or-raise policy used by the robot handle and service handle factories.

with Client() as client:
    available = client.core.list_robot_namespaces()
    for namespace in available.namespaces:
        print(namespace)
    if available.has_global:
        print("<global>")
import { connect } from "olo/web";

const client = connect();
const available = await client.core.listRobotNamespaces();
for (const namespace of available.namespaces) {
  console.log(namespace);
}
if (available.hasGlobal) {
  console.log("<global>");
}