NASA Florida Institute of Technology

INTERNATIONAL MISSION: AUTONOMOUS NAVIGATION

Husky A200 · Florida Institute of Technology
Benjamin LouetCreator
Prof. Hector GutierrezSupervisor

Link Status

Not connected

rosbridge over the SSH tunnel opened by scripts/start-husky. Not set up on the robot yet -- see docs/technical.tex.

Battery

-- V (no live data)

Vehicle

Clearpath Husky A200
990 × 670 × 390 mm · 50 kg · max 1.0 m/s

Safety (from /status)

e-stop: --
lockout: --
comm timeout: --

Mirrors the physical status panel LEDs on the back of the chassis.

Sensor Diagnostics

Status dots are placeholders (grey = unknown). A browser can't query USB devices on a remote robot directly -- this needs a small diagnostics-publishing node on the robot side to become live.

Notes

This cockpit is scaffolding: the 3D view, camera feed and telemetry panels are wired to real ROS topic names (see js/config.js) but nothing is live until rosbridge_server and web_video_server are installed and running on the robot, and scripts/start-husky has an active tunnel open. Teleop publishes on the confirmed real topic, /husky_velocity_controller/cmd_vel -- not the generic /cmd_vel, which does not exist on this unit.

3D View — base_link frame

Loading the real Husky A200 CAD model… (a one-time ~13 MB fetch + in-browser parse; the placeholder box below is shown until this finishes)

X = red, Y = green, Z = blue. Drag to orbit, scroll to zoom.

Camera Feed

Teleoperation

W A S D
v = 0.00 m/s ω = 0.00 rad/s
SPEED LIMIT 100%

Publishes geometry_msgs/Twist on /husky_velocity_controller/cmd_vel at 10 Hz whenever connected. Base cap ±0.5 m/s / ±0.5 rad/s (js/config.js), further scaled by the speed limit slider above -- this cap and the E-STOP below apply no matter which control mode is active. E-STOP immediately publishes zero velocity and latches: input is ignored until you press it again to release. This is in addition to the physical joystick and the robot's own hardware e-stop, not a replacement for either.

PS4 Controller mode reads a controller connected to this computer (USB or Bluetooth pairing done in the OS, not on the robot) via the browser's Gamepad API -- left stick drives, same as the on-screen joystick. Press any button on it once connected for the browser to notice it.

Map & Trajectories

MINS estimate Wheel odometry Base EKF GNSS fixes (scatter, not a path) Lidar obstacles (live) Camera obstacles (depth) Camera landmarks

Top-down view in the estimator's own frame, 1 m grid, auto-scaled to the data. The arrow is the current pose (heading = yaw). The base EKF is drawn but does not set the scale — on 2026-09-25 it ended a 20 m loop 19.49 m from its start, and letting a trace that wrong choose the extent squeezed MINS and the wheels into a few unreadable pixels. A trace running off the edge of the frame is itself the diagnosis. There is no continuous ground truth here — no motion capture and no RTK GPS — but a closed loop supplies two exact ones for free: the robot returns to its start, so net displacement is zero, and it completes a whole number of turns, so net rotation is a multiple of 360°. Neither can be flattered by an alignment, and they are what the trajectory comparison is scored on. A planned-path layer is not drawn because no planner runs on this robot yet.

The GNSS track is the only absolute reference on this map, and it is anchored to the others in position but not in heading. Satellite positions arrive as latitude and longitude; everything else here is metres from wherever the estimator happened to start. The GNSS trace is therefore pinned to the robot's estimated pose at the instant of its first fix, so the two share an origin in space and time. The heading offset is learned by driving. GNSS is north-up while the estimators use whatever heading the robot had at startup; over a leg longer than 3 m — far enough to beat a standalone receiver's scatter — the direction travelled in each frame differs by exactly that angle, and the overlay rotates itself to match. Drive a few metres in a straight line and the GNSS will line up with the other tracks; before that it points north and will not. Only a solved fix is plotted; a receiver reporting quality 0 is not drawing a position it has computed.

GNSS is drawn as a cloud of points, never as a line, and that is deliberate. A standalone receiver wanders by metres while the robot stands perfectly still. Joining those samples with a line would draw a path the robot never took — which is exactly how a trajectory layer reads. The dashed circle and its scatter figure enclose the recent fixes, so the wander becomes a number instead of an illusion of movement. That number is the case for RTK: metres here, against the about 14 mm a Fixed RTK solution gives. Watching it collapse once the base station broadcasts corrections is how we will know RTK is working.

The two perception layers show what the robot senses right now, and disappear when their sensor goes quiet rather than leaving a stale picture: lidar obstacles are the live /scan return rotated onto the map by the current pose (so they are only as accurate as that pose, and the lidar's own mounting offset is not yet compensated), and camera landmarks are the 3D points MINS is currently tracking, which appear only once MINS is running and initialized. Sparse scatter is expected there — MINS tracks a few dozen landmarks, it does not build a dense reconstruction.

Camera obstacles come from the ZED's depth map, reduced to a compact scan on the robot (scripts/depth_to_obstacles.py) because a raw depth frame is ~920 kB and cannot cross the tunnel. The two obstacle layers see genuinely different things: the lidar sweeps a full 360° in one horizontal plane and is blind above and below it, while the camera sees only a ~100° cone ahead but across a band of heights. Camera obstacles refresh roughly once per second while MINS is running — MINS saturates a core of the Jetson and the ZED's depth output drops with it; it is ~11 Hz with MINS stopped.

MINS State & Filter Health

Position--
Orientation--
Velocity--
Uncertainty (1σ)--
unknown -- MINS is not running
-- (no feature cloud: MINS not running)

Feature counts come from /mins/cam/msckf and /mins/cam/slam. They matter because the pose topic cannot tell you the filter has gone blind: with no visual features MINS keeps propagating on the IMU alone and still publishes a confident pose, drifting silently. A count falling toward zero while the robot is moving is the earliest visible symptom.

Zero while the robot is parked is normal, not a fault. Features are triangulated from the camera's own displacement between frames, so a stationary camera gives no parallax and nothing to triangulate — the count goes to zero and the few landmarks that do survive land at meaningless depths, typically bunched a few tens of centimetres from the robot. The visual layer cannot be judged on a parked robot at all, however good the calibration is. Drive a few metres and the landmarks should populate and settle onto walls and furniture; that is the check worth making.

Input streams

MINS estimate: no data yet
Wheel odometry: no data yet
Base EKF: no data yet

A stream that stops is shown as STALE with the time since its last message, and its trajectory dims on the map, so a sensor dropout is visible instead of looking like a robot standing still. Uncertainty comes from the covariance MINS publishes on /mins/imu/pose; the good/degraded/poor thresholds are a display heuristic, not values validated against measured error on this robot.

GNSS — satellite positioning

No GNSS receiver detected.
Fix: —
Satellites: —

Why this matters, and why it is the one sensor the others cannot replace. MINS fuses only relative measurements — how far the wheels turned, how the image moved, how the IMU accelerated. Every one of them accumulates error without bound, so outdoors, with no loop to close, the estimate drifts forever. GNSS is the only planned sensor that gives an absolute position, and it is what stops that drift.

Plain satellite positioning is not accurate enough for a robot that has to follow a path: a standalone receiver lands within a few metres. The plan is RTK — two receivers, one parked at a surveyed point broadcasting corrections, one on the robot. With those corrections and a Fixed solution, horizontal accuracy reaches about 14 mm. The fix quality above is what to watch: 1 is plain GPS (metres), 5 is RTK Float (decimetres), 4 is RTK Fixed (millimetres). Anything below 4 is not the result we are after.

Status: hardware arriving, nothing fused yet. A u-blox receiver was first seen on this robot on 2026-09-28 and emitted valid NMEA, with no fix — which is exactly right indoors, since a GNSS antenna needs a clear view of the sky and a window is usually not enough. No ROS driver runs yet and MINS has no config_gps.yaml, so nothing here reaches the estimator. Full plan in the GNSS RTK positioning section of docs/technical.tex, in the Docs tab.

Sensors — obstacle detection

Ops agent not running -- cannot start the sensors from here.
RPLidar driver
LaserScan → PointCloud2 converter
Camera obstacle detection (ZED depth)

Starts only the perception sensors: the RPLidar, the converter MINS needs to read it, and the camera depth-to-obstacle node. This is what fills the map with obstacle points, and it runs without the estimator — deliberately, so the map stays usable and demonstrable while MINS is being debugged. None of it runs at boot, since it costs power and CPU outside a session.

Camera obstacles work, as of 2026-09-24, and no longer cost the frame rate. This box used to report that the feature was unavailable: depth computation was measured at 1.294 s per frame, collapsing the stereo streams from 14.9 Hz to 0.80 Hz, and starving the stereo pair is exactly what made MINS diverge — so the driver ran with depth_mode: NONE. That measurement had only ever been taken in one depth mode. With depth_mode: PERFORMANCE and depth_stabilization: 0, the stereo pair and the obstacle scan run at 14.9 Hz together, MINS included. The cost is depth quality, which is the cheapest of the five active modes — fine for obstacles capped at 6 m, and worth revisiting if the depth map is ever wanted for anything more demanding. Lidar obstacles (red) cost nothing and are always on.

MINS estimate

Ops agent not running.

Fuses the ZED stereo pair, its IMU and the wheel encoders. With the robot stationary it holds position to under 2 cm over 95 seconds (measured 2026-09-11) and starts on its own, with no jolt, as long as the wheels are included.

The wheels checkbox is not cosmetic: lifted off the ground the encoders truthfully report “not turning”, which MINS cannot tell apart from “not moving”, so a hand-carry test needs them excluded — but without wheels MINS falls back to an initializer that needs a firm jolt to trigger.

Lidar fusion is off by default on measured grounds, not caution: it stays stable but takes MINS from 152 % to 425 % CPU, which halves its own camera intake from 14.8 to 7.0 Hz — and visual updates are precisely what keep this filter from drifting. The map's red obstacle points do not depend on it.

Architecture — how MINS is wired on this robot

flowchart LR
  subgraph SENSORS["Raw sensors (Husky A200)"]
    IMU["ZED 2i IMU
/zed2i/zed_node/imu/data_raw
~200 Hz"] CAM["ZED 2i stereo (raw gray)
left_raw + right_raw
~15 Hz"] ENC["Wheel encoders
/joint_states
4-wheel skid-steer"] LID["RPLidar A3M1
/scan → /scan/points
(laserscan_to_pointcloud.py)"] end subgraph PRE["Pre-processing"] FEAT["KLT feature tracking
200 pts, FAST 20, histogram eq."] DIFF["Differential-drive model
Wheel2DAng, r=0.1651 m"] CHI2["chi2 gating
slip / outlier rejection"] end subgraph MINS["MINS estimator"] PROP["IMU propagation
Allan-variance noise model"] UPD["MSCKF + SLAM update
sliding window, 20 Hz clones"] CAL["Online calibration
extrinsics, intrinsics, dt"] end subgraph OUT["Pose estimate + TF"] POSE["/mins/imu/pose
pose + 6x6 covariance"] PATH["/mins/imu/path"] ODOM["/mins/imu/odom"] end subgraph NAV["Mapping and navigation"] UI["Cockpit Navigation panel
via rosbridge websocket"] FUT["Path planning / autonomy
NOT implemented yet"] end IMU --> PROP CAM --> FEAT --> UPD ENC --> DIFF --> CHI2 --> UPD LID -.->|stage 3, plane matching, unvalidated| UPD PROP --> UPD UPD <--> CAL UPD --> POSE UPD --> PATH UPD --> ODOM POSE --> UI PATH --> UI ODOM -.-> FUT

Fusion is enabled one sensor at a time on purpose: with several unvalidated extrinsics live at once a divergence cannot be attributed to any one of them. Camera+IMU+wheel (stages 1–2) ran together for the first time on 2026-09-07, and the camera was proven to earn its place on 2026-09-24: replaying both square drives with it on and off gives 1.8° of heading error against 7.95°. The lidar was wired on 2026-09-10 but contributed nothing at all until 2026-09-25. MINS's plane-matching update rejects any neighbourhood whose coordinate matrix is ill-conditioned, and with every point of a single-ring 2D scan published at the same height that matrix is rank-deficient — so every lidar update was silently rejected while the sensor still cost CPU. The converter now publishes each return at five heights, which a wall justifies because a wall is vertical, and the fitted plane normal becomes horizontal. That is what constrains position, and loop closure fell from 0.477 m to 0.093 m on the same recorded drive. No GNSS receiver is fitted yet — MINS itself does support one, and an RTK pair is being prepared for outdoor missions. Full write-up: the MINS and GNSS RTK sections of docs/technical.tex, in the Docs tab.

Control Barrier Functions

NOT IMPLEMENTED — this tab is scaffolding for future work, not a live safety filter

A control barrier function (CBF) defines a safe set — every state x where a scalar function h(x) stays non-negative — and a corresponding filter that intervenes on a commanded input only when the state is about to leave that set. It enforces that h can never decrease faster than a bound tied to its own current margin, typically by solving a small quadratic program that finds the control input closest to what was actually requested (teleop, or a future planner) while still satisfying that constraint. The point is minimal intervention: unlike the hard velocity clamp already in Camera & Teleop (maxLinearSpeed/maxAngularSpeed in js/config.js, which caps every command unconditionally), a CBF filter would leave commands untouched until the robot is genuinely approaching a constraint boundary.

Candidate constraints on this robot

Obstacle clearance
/scan (RPLidar A3M1)
not wired
Geofence boundary
RTK GPS position
hardware not installed
Velocity / acceleration limits
/husky_velocity_controller/cmd_vel
hard clamp exists, not CBF-filtered

None of these are connected to anything yet. The lidar is fused into MINS as of 2026-09-25 (see the Navigation tab), but it is not wired into any safety layer; the RTK GPS receiver has not been purchased (see docs/technical.tex, GNSS RTK positioning); and the existing speed limiter is a simple clamp, not a state-dependent barrier. Listed here as the constraints most relevant to this platform once each becomes available, so the filter can be wired in incrementally, one constraint at a time — the same one-sensor-at-a-time discipline already used for MINS.

Planned layout (once implemented)

Barrier value h(x)--
Filter status--
Command modified?--

These three rows are reserved (their element ids are already wired into nothing) for when a real CBF node publishes something to subscribe to: the current barrier margin, whether the filter is active per constraint, and whether the last command sent to the robot was actually the one requested or one the QP had to modify to stay safe. Deliberately not populated with placeholder numbers — a fake-looking live value here would be worse than an empty one.

Robot agent not running

This panel is driven by scripts/cockpit_agent.py, a small companion node on the robot. Nothing here is live until it is started — the rest of the cockpit works without it.

scp scripts/cockpit_agent.py cerveaux@ubuntu:~/
ssh cerveaux@ubuntu
source /opt/ros/noetic/setup.bash
source ~/catkin_ws/devel/setup.bash
python3 ~/cockpit_agent.py

It publishes /cockpit/diagnostics once a second and listens on /cockpit/command. If it is running and this message persists, the rosbridge connection is the thing to check — see the Link Status card on the Overview tab.

Research log

Loading…

Technical doc

Loading…

Rendered from docs/research.tex and docs/technical.tex by scripts/build_docs.sh. If scripts/watch_docs.sh is running (started automatically by start-husky), each panel refreshes on its own within ~15s of the source file changing -- no manual rebuild or page reload needed.