Sim navigation

The /sim/[robotId] navigation panel reads a NavigationSource. This page attaches the in-browser source only. The rosbridge Nav2 adapter below is a typed shape for a later source. It is not constructed, it does not open a socket, and the panel does not show a Nav2 connection.

Source of truth for the types: lib/sim/nav/.

NavigationSource

type NavMode = "slam" | "localization";
type LocalizationState = "initialising" | "tracking" | "lost";
type NavTaskKind = "goal" | "route" | "explore";
type NavStatus = "idle" | "navigating" | "succeeded" | "failed" | "cancelled";

interface NavPose {
  x: number;
  y: number;
  yaw: number;
}

interface NavTask {
  kind: NavTaskKind;
  /** 1-based waypoint index. */
  index: number;
  total: number;
  /** Metres still to travel. */
  remainingM: number;
}

interface NavigationSource {
  readonly kind: "in-browser" | "rosbridge";
  readonly configName: string;
  readonly mode: NavMode;
  readonly localization: LocalizationState;
  readonly task: NavTask | null;
  readonly status: NavStatus;
  getSnapshot(): NavigationSnapshot;
  subscribe(listener: (snapshot: NavigationSnapshot) => void): () => void;
  goTo(pose: NavPose): void;
  followRoute(poses: readonly NavPose[]): void;
  cancel(): void;
}

getSnapshot() returns those fields plus the strings the panel prints (configName, localizationLabel, taskLabel).

The panel shows configName. For the in-browser source that string is In-browser planner (simulated). A rosbridge source would show its own configName instead. None is attached in this UI.

In-browser planner

The in-browser source plans in the plane and follows the polyline with pure pursuit.

  • Grid. When an occupancy grid is passed in, an 8-connected A* searches it. Diagonal steps are refused when either adjacent orthogonal cell is blocked, so the path does not cut a corner. Cells with occupancy >= 50, unknown cells (-1), and non-finite cells are not traversable. If no path exists the plan returns { ok: false, reason } and does not throw. The source then sets status failed and sends a zero velocity.
  • No grid. Mapping is not attached. The plan is the straight segment from the current pose to the goal. The panel says so: "Straight line. No occupancy grid is attached."
  • Follower. A holonomic base (descriptor holonomic and a linearY axis) drives toward a lookahead point and may strafe. A heading-constrained base gets vy = 0 and uses pursuit curvature 2 * sin(alpha) / L, rotating in place when the lookahead is behind it. Both servo the goal yaw inside the position tolerance.
  • Limits. Commands are clamped to teleop.velocity.maxLinearMps and maxAngularRadS, and to the descriptor's velocity axes, before they are sent. The backend clamps again.

The in-browser mode is localization. The simulator already knows the base pose, so once a telemetry sample arrives the localization line is tracking (ground truth, simulated). Before that sample it is initialising. This source does not start an explore task; the task kind exists so a later source can report one. goTo records a goal task. followRoute records a route task.

Active tasks are printed as Waypoint 2/3 · 4.1 m (1-based index, remaining distance in metres to one decimal). Explore tasks use the same distance with an Explore prefix.

Velocity path

createInBrowserNavigation sends every command, including the cancel zero, through SimBackend.setBaseVelocity. That path still applies the deadman, e-stop, command watchdog, and velocity clamp. Cancel does not clear e-stop and does not hold the deadman. The base moves only while those existing checks allow it.

Occupancy grid

The planner accepts a minimal nav_msgs/OccupancyGrid and does not import mapping code:

Field Meaning
resolution Metres per cell
origin.x, origin.y World position of the corner of cell (0, 0)
origin.yaw Grid heading in radians. Omitted means 0
width, height Cell counts
data Row-major Int8Array or number[], length width * height. Index = y * width + x. -1 unknown, 0 free, 100 occupied

nav_msgs/MapMetaData.origin is a geometry_msgs/Pose. Only the planar position and yaw are used. Yaw comes from the quaternion the same way as a stamped goal pose (yawFromQuaternion in lib/sim/nav/rosbridge.ts).

Panel

Registered as the navigation slot in lib/sim/workbench (left column, between telemetry and the descriptor). It shows:

  • Config
  • Mode
  • Localization status
  • Active task
  • A status pill: idle, navigating, succeeded, failed, cancelled, or unavailable when no in-browser source is attached
  • Stop, which calls cancel()
  • Go to, with numeric x (m), y (m), and yaw (rad). Click-to-goal is not part of this panel

If the sim backend is not the in-browser one, the panel says the in-browser planner is not attached. It does not offer a Nav2 connect button.

Rosbridge Nav2 adapter

A later RosbridgeNavAdapter implements NavigationSource with kind: "rosbridge". It would speak rosbridge action ops to Nav2. This repository does not open that connection.

Actions:

Task Action name Action type
goTo /navigate_to_pose nav2_msgs/action/NavigateToPose
followRoute /follow_waypoints nav2_msgs/action/FollowWaypoints
interface RosPoseStamped {
  header: { frame_id: string; stamp?: { sec: number; nanosec: number } };
  pose: {
    position: { x: number; y: number; z: number };
    orientation: { x: number; y: number; z: number; w: number };
  };
}

interface NavigateToPoseGoal {
  pose: RosPoseStamped;
  behavior_tree: string;
}

interface NavigateToPoseFeedback {
  current_pose: RosPoseStamped;
  navigation_time: { sec: number; nanosec: number };
  estimated_time_remaining: { sec: number; nanosec: number };
  number_of_recoveries: number;
  distance_remaining: number;
}

interface FollowWaypointsGoal {
  poses: RosPoseStamped[];
}

/** 0-based index. This feedback has no distance field. */
interface FollowWaypointsFeedback {
  current_waypoint: number;
}

interface RosbridgeActionGoal {
  op: "send_action_goal";
  id: string;
  action: "/navigate_to_pose" | "/follow_waypoints";
  action_type: "nav2_msgs/action/NavigateToPose" | "nav2_msgs/action/FollowWaypoints";
  args: NavigateToPoseGoal | FollowWaypointsGoal;
  feedback: true;
}

interface RosbridgeActionFeedback {
  op: "action_feedback";
  id: string;
  action: string;
  values: NavigateToPoseFeedback | FollowWaypointsFeedback;
}

interface RosbridgeActionResult {
  op: "action_result";
  id: string;
  action: string;
  /** action_msgs/msg/GoalStatus */
  status: number;
  values: Record<string, unknown>;
  result: boolean;
}

interface RosbridgeCancelGoal {
  op: "cancel_action_goal";
  id: string;
  action: string;
}

interface RosbridgeNavAdapter extends NavigationSource {
  readonly kind: "rosbridge";
  readonly endpoint: string;
  readonly navigateAction: "/navigate_to_pose";
  readonly followAction: "/follow_waypoints";
}

Mapping onto NavigationSource:

Nav2 NavigationSource
NavigateToPose feedback distance_remaining task.remainingM for a goal task (index 1, total 1)
NavigateToPose feedback current_pose planar pose via navPoseFromStamped
FollowWaypoints feedback current_waypoint (0-based) task.index = current_waypoint + 1, clamped to poses.length
FollowWaypoints goal poses.length task.total
Remaining distance on a route Not in the action feedback. The adapter fills remainingM from its own path estimate
Goal status 1 ACCEPTED, 2 EXECUTING navigating
Goal status 3 CANCELING, 5 CANCELED cancelled
Goal status 4 SUCCEEDED succeeded
Goal status 6 ABORTED failed
Other codes idle

goTo would send send_action_goal for NavigateToPose. followRoute would send FollowWaypoints. cancel would send cancel_action_goal. Feedback and result messages update the snapshot. Until that source is actually attached, the panel keeps the in-browser planner label and does not show these actions as connected.