Mobile base (ROS 2)
The four-wheel differential-drive base under ROS 2 Jazzy — packages, bring-up, topics, and the systemd service.
The mobile base is the four-wheeled platform that carries the two SO-101 arms. It runs on a Jetson Orin Nano under ROS 2 Jazzy (Ubuntu 24.04), driven by two LK-TECH MF5010 BLDC motors over CAN, with a Slamtec RPLIDAR A1M8 for 2D scanning.
The base and the arms are operationally independent — the HMI subscribes to /odom and /scan and publishes /cmd_vel, all topics the ROS hardware stack exposes. You can bring the base up without the HMI, and you can run the HMI on a laptop pointed at an Orin running only the base.
The src/README.md checked into the repo still references ROS 2 Humble + Gazebo Fortress. That's stale — Haller runs on Jazzy on Ubuntu 24.04. This page is the current reference.
Hardware
| Component | Spec |
|---|---|
| Drive motors | 2× LK-TECH MF5010 BLDC (built-in controller, CAN at 1 Mbps) |
| CAN adapter | CANable2 USB → SLCAN, mapped to /dev/haller_can |
| LiDAR | Slamtec RPLIDAR A1M8 (USB CP2102), mapped to /dev/haller_lidar |
| Compute | NVIDIA Jetson Orin Nano (8 GB) |
| Geometry | Wheel radius 0.05 m, wheel separation 0.34 m (see motor_params.yaml) |
| Limits | 1.0 m/s linear, 2.0 rad/s angular (configurable) |
LK-TECH protocol references live in the repo root: MF5010_Specs.pdf and LK-TECH motor control protocol (CAN) V2.35.
Package layout
The colcon workspace under src/ is split into three top-level groups:
src/
├── haller_ros/
│ ├── haller_common/
│ │ ├── haller_controllers/ # ros2_control YAML (sim / fake-hw mode)
│ │ ├── haller_description/ # URDF / Xacro robot model + display.launch.py
│ │ ├── haller_hardware_interface/ # ros2_control hardware interface plugin
│ │ ├── haller_motor_controller/ # direct-CAN motor controller node (real hw)
│ │ ├── haller_msgs/ # custom message definitions
│ │ ├── haller_utils/ # utility nodes (legacy web_teleop lives here)
│ │ └── haller_vision/ # camera, detection, segmentation, traversability
│ ├── haller_robot/
│ │ └── haller_hardware/ # haller_bringup.launch.py — top-level real-hw launch
│ └── haller_simulator/
│ └── haller_gazebo/ # Gazebo simulation
├── haller_navigation/ # Nav2 + slam_toolbox launches and params
├── haller_scanning/ # mapping / scanning pipeline
└── sllidar_ros2/ # submodule — Slamtec LiDAR driverTwo control paths share the same launch file:
- Real hardware (
use_sim:=false, default): thehaller_motor_controllernode reads/cmd_vel, talks SLCAN over USB to both MF5010s, publishes/odom+/joint_states. - Simulation / fake hardware (
use_sim:=true):ros2_control_node+ the standarddiff_drive_controllerandjoint_state_broadcaster— useful for laptop dev without the physical robot.
Prerequisites
- Ubuntu 24.04 LTS.
- ROS 2 Jazzy installed (
/opt/ros/jazzy/). - Gazebo Harmonic (for simulation; skip for real-hardware-only).
- For the Jetson: NVIDIA JetPack 6.
Install the workspace dependencies once:
sudo apt update
sudo apt install -y \
ros-jazzy-ros2-control \
ros-jazzy-ros2-controllers \
ros-jazzy-navigation2 \
ros-jazzy-nav2-bringup \
ros-jazzy-slam-toolbox \
ros-jazzy-robot-localization \
ros-jazzy-xacro \
ros-jazzy-joint-state-publisher-gui
cd ~/haller_ws
rosdep install --from-paths src --ignore-src -r -yClone and build
# Clone with submodules — sllidar_ros2 is a git submodule
git clone --recurse-submodules https://github.com/oscardvs/haller_ws.git
cd haller_ws
colcon build --symlink-install
source install/setup.bashThe --symlink-install flag means Python edits are picked up without a rebuild — relaunch is enough.
Stable device names (udev)
Both the CAN adapter and the LiDAR get fixed /dev/haller_* symlinks via scripts/99-haller-devices.rules. Install once:
sudo cp scripts/99-haller-devices.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm triggerVerify the symlinks resolve:
ls -l /dev/haller_can /dev/haller_lidar
# lrwxrwxrwx 1 root root 7 ... /dev/haller_can -> ttyACM0
# lrwxrwxrwx 1 root root 7 ... /dev/haller_lidar -> ttyUSB0If a device doesn't appear, lsusb for the adapter (CANable2 16d0:117e, CP2102 10c4:ea60) and check dmesg | tail for enumeration messages.
Bring the CAN bus up
The CANable2 adapter exposes a virtual can0 interface that needs to be brought up at the right bitrate before the motor node can talk to anything:
sudo ip link set can0 type can bitrate 1000000
sudo ip link set up can0Sanity-check with candump can0 in one terminal and a manual motor poke in another (see can_test.py in the repo root for a bare LK-TECH ping).
The systemd service handles this automatically on the Jetson — scripts/haller_bringup.sh brings can0 up before launching the stack. Manual ip link is only needed for dev sessions outside the service.
Bring up the real robot
The top-level launch is in haller_hardware:
ros2 launch haller_hardware haller_bringup.launch.pyIt starts (in order):
robot_state_publisher— broadcasts the URDF transforms.motor_controller_node(fromhaller_motor_controller) — opens/dev/haller_can, subscribes to/cmd_vel, publishes/odom.sllidar_node— opens/dev/haller_lidar, publishes/scan.haller_visionpipeline — IMX219 camera + detection + segmentation + traversability (skip withenable_vision:=false). Detailed walkthrough on the Vision pipeline page.
Launch flags
| Flag | Default | Effect |
|---|---|---|
use_sim | false | true switches to ros2_control with fake-hardware actuators (no CAN). |
enable_vision | true | false skips the camera + perception pipeline (faster boot, less GPU load). |
enable_detection | true | Toggles object-detection node within the vision pipeline. |
enable_segmentation | true | Toggles semantic-segmentation node within the vision pipeline. |
enable_web_teleop | false | Deprecated — the legacy single-file web_teleop.py. Use the HMI instead. |
Common variant — debug-fast on the Jetson without the perception pipeline:
ros2 launch haller_hardware haller_bringup.launch.py enable_vision:=falseTopics exposed
This is the bus a downstream consumer (HMI, Nav2, a VLA policy) sees once the launch is up:
| Topic | Direction | Notes |
|---|---|---|
/cmd_vel | sub | geometry_msgs/Twist — linear.x / angular.z. 0.5 s timeout (motors stop if nothing arrives). |
/odom | pub | nav_msgs/Odometry — wheel-encoder fused, published at the 50 Hz control rate. |
/joint_states | pub | sensor_msgs/JointState — wheel joint positions / velocities. |
/scan | pub | sensor_msgs/LaserScan from RPLIDAR. |
/tf, /tf_static | pub | odom, base_link, sensor frames. |
The HMI's base panel publishes /cmd_vel (via WS proxy through FastAPI) and subscribes to /odom + /scan for the dashboard's motion + lidar widgets.
Simulation
# Gazebo with the URDF + simulated LiDAR
ros2 launch haller_gazebo haller_sim.launch.py
# In another terminal, drive it via Nav2
ros2 launch haller_navigation navigation.launch.py use_sim_time:=trueFor dev without Gazebo (no graphics, no physics), use the fake-hardware path:
ros2 launch haller_hardware haller_bringup.launch.py use_sim:=true enable_vision:=falseThis runs the full ROS graph with ros2_control fake actuators — useful for testing HMI ↔ ROS plumbing on a laptop.
Visualization
# RViz preset with robot model, scan, costmaps
ros2 launch haller_description display.launch.pyFor a headless Jetson, run RViz on a remote machine on the same ROS_DOMAIN_ID. Set both ends to the same domain:
export ROS_DOMAIN_ID=42 # match between Jetson and laptopNavigation (Nav2)
# SLAM (build a map of a new space)
ros2 launch haller_navigation slam.launch.py
# Localize + plan in a saved map
ros2 launch haller_navigation navigation.launch.pyParams live in src/haller_navigation/config/ (nav2_params.yaml, slam_toolbox_params.yaml). Maps go to src/haller_navigation/maps/ (gitignored — saved per-environment).
Production: systemd on the Jetson
On the Jetson the stack runs as the haller-robot.service systemd unit, which wraps scripts/haller_bringup.sh. The unit:
- waits for both
/dev/haller_lidarand/dev/haller_canto appear (up to 10 s of polling), - sources ROS + the install tree,
- runs
ros2 launch haller_hardware haller_bringup.launch.py enable_vision:=false, - restarts on failure.
Install once:
sudo cp scripts/haller-robot.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now haller-robot.serviceWatch the logs:
journalctl -u haller-robot.service -fThe HMI service (haller-hmi.service) is independent and connects to whatever topics this stack exposes — see HMI production.
Troubleshooting
ROS-stack symptoms (/scan empty, ros2 topic list missing topics across processes, CAN bus already up, RViz can't see remote topics) are on the Troubleshooting / ROS stack section.
What this page doesn't cover yet
- CAN motor protocol details — frame format, control modes (
0x9C,0xA2, …), encoder math. The LK-TECH PDFs cover this; a derived cheatsheet is TODO. - Tuning Nav2 for Haller's footprint — costmap inflation, controller plugin choice, recovery behaviors. TODO.
- Multi-machine ROS — running Nav2 on a beefier laptop while the Jetson handles only hardware nodes. Pointers, not full guide, TODO.