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 statusfailedand 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
holonomicand alinearYaxis) drives toward a lookahead point and may strafe. A heading-constrained base getsvy = 0and uses pursuit curvature2 * 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.maxLinearMpsandmaxAngularRadS, 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, orunavailablewhen 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.