Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RoArm-M2 Control Setup

Personal setup notes and code for a Waveshare RoArm-M2-S: a repaired gripper servo, a split MoveIt/driver architecture across a Raspberry Pi and a Windows/WSL machine, and a browser dashboard that combines a live camera feed with YOLO object detection and click-to-move control.

Hardware background

The gripper's bus servo burnt out and was replaced with a new ST3215 unit. New servos ship from the factory as ID 1, but this arm's servo bus already has a device on that ID (BASE_SERVO_ID = 11 conflicts happen if the new part isn't renumbered), so the replacement had to be reassigned to its expected ID before it would work:

  • Servo ID map used by this firmware: BASE=11, SHOULDER_DRIVING=12, SHOULDER_DRIVEN=13, ELBOW=14, GRIPPER=15.
  • Fix applied over the serial JSON protocol (115200 baud), with the new servo alone on the bus:
    {"T":501,"raw":11,"new":15}      # reassign ID 11 -> 15 (gripper)
    {"T":502,"id":15}                 # set middle/reference position
    
  • Command reference: CMD_SET_SERVO_ID = 501, CMD_SET_MIDDLE = 502, both defined in the firmware's json_cmd.h (see waveshareteam's RoArm-M2 wiki / roarm_ws_em0 source for the full command table).

Architecture

Two machines, split the same way as this user's other Waveshare robot project (GUI/planning on the PC, driver on the Pi):

┌─────────────────────────┐        Tailscale (WireGuard)        ┌──────────────────────────┐
│  Windows + WSL2 Ubuntu   │ <───────────────────────────────>  │  Raspberry Pi 5           │
│  22.04, ROS2 Humble      │      ROS_DOMAIN_ID=24               │  Ubuntu Server 24.04      │
│                          │      RMW=rmw_cyclonedds_cpp         │                           │
│  - MoveIt2 + RViz2       │                                      │  - Docker container       │
│    (roarm_moveit)        │                                      │    (Ubuntu 22.04 +       │
│  - IKFast solver         │                                      │     ROS2 Humble base)    │
│  - GUI shows via WSLg    │                                      │  - roarm_driver node     │
│                          │                                      │    -> /dev/ttyUSB0       │
│                          │                                      │  - USB camera /dev/video0 │
└─────────────────────────┘                                      └──────────────────────────┘

Why Tailscale instead of plain LAN

Both machines are on the same physical LAN (WSL2 in mirrored networking mode shares the Windows host's real IP/subnet), and ICMP ping worked fine between them — but raw UDP delivery from the Pi to WSL was silently dropped even after opening Windows Firewall rules for the specific remote IP (tried both -Profile Private and -Profile Any, scoped to the Pi's address). Root cause was never conclusively identified (candidates: WSL2 mirrored-mode inbound-UDP quirk, router-level client isolation, or a firewall layer not visible via New-NetFirewallRule). Tailscale reliably works around it by tunneling over WireGuard, and tailscale ping confirmed a working path immediately. If you retry the plain-LAN route later, wsl-gui/cyclonedds.xml and pi-driver/cyclonedds.xml show the working CycloneDDS config shape (explicit <Peers> + AllowMulticast=spdp, not false — disabling multicast entirely also broke same-host node discovery inside the Pi's own container).

Repo layout

  • pi-driver/Dockerfile — lightweight image for the Pi: Ubuntu 22.04 + ros-humble-ros-base (no desktop/GUI packages) + roarm_driver/roarm_description/roarm_msgs built from waveshareteam/roarm_ws (ros2-humble branch).
  • pi-driver/cyclonedds.xml — CycloneDDS config deployed inside the Pi's driver container.
  • wsl-gui/cyclonedds.xml — matching config used on the WSL side.
  • dashboard/ — Flask app: live MJPEG stream with YOLOv8 detections, click an object in the browser to send the arm there.

Setting up the Pi driver

# On the Pi
docker build --platform linux/arm64 -t roarm-driver:latest pi-driver/

docker run -d --name roarm-driver --network host --privileged \
  --device=/dev/ttyUSB0 --restart unless-stopped roarm-driver:latest tail -f /dev/null

docker cp pi-driver/cyclonedds.xml roarm-driver:/root/cyclonedds.xml

docker exec -d roarm-driver bash -c '
  export CYCLONEDDS_URI=file:///root/cyclonedds.xml
  source /opt/ros/humble/setup.bash
  source /root/roarm_ws/install/setup.bash
  exec ros2 run roarm_driver roarm_driver --ros-args -p serial_port:=/dev/ttyUSB0'

Gotcha: killing the driver with pkill -f "ros2 run roarm_driver" only kills the ros2 run launcher wrapper, not the actual spawned node process — it leaks a zombie that still holds /dev/ttyUSB0 open. Use pkill -9 -f roarm_driver (matches the real binary path too) before restarting it, and check ps aux | grep roarm_driver for duplicates if the arm ever seems to get commands from an unexpected/stale source.

Setting up the WSL MoveIt/RViz side

Needs waveshareteam/roarm_ws (ros2-humble branch) cloned separately and built with MoveIt + MoveIt Task Constructor + the IKFast plugin. On a resource-constrained machine, build with MAKEFLAGS="-j2" colcon build --parallel-workers 1 per package — moveit_task_constructor_core compiling with full default parallelism (-j=core count) OOM-killed cc1plus on a machine with ~6GB free RAM. Also remember to source install/setup.bash from the local workspace itself (not just /opt/ros/humble/setup.bash) between separate colcon build invocations — otherwise CMake can't find custom packages like roarm_msgs that have no system-wide fallback (unlike the MoveIt packages, which silently resolve against /opt/ros/humble if the local overlay isn't on CMAKE_PREFIX_PATH, masking the missing-source-step bug).

source /opt/ros/humble/setup.bash
source ~/roarm_arm_ws/install/setup.bash
export ROARM_MODEL=roarm_m2
export ROS_DOMAIN_ID=24
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
export CYCLONEDDS_URI=file:///path/to/wsl-gui/cyclonedds.xml

ros2 launch roarm_moveit roarm_moveit.launch.py

Drag the interactive marker in RViz, click Plan, then Plan & Execute to move the real arm.

Dashboard (camera + YOLO + click-to-move)

Runs entirely on the Pi (camera and serial are both local there), independent of the MoveIt/ROS2 side — it talks to the arm directly over the same serial JSON protocol the firmware exposes, using CMD_XYZT_GOAL_CTRL ({"T":104,...}) so the ESP32's own built-in inverse kinematics handles the move, no MoveIt/IKFast needed for this path.

cd dashboard
python3 -m venv venv
./venv/bin/pip install torch --index-url https://download.pytorch.org/whl/cpu
./venv/bin/pip install torchvision --index-url https://download.pytorch.org/whl/cpu
./venv/bin/pip install -r requirements.txt
./venv/bin/python3 app.py    # serves http://<pi-ip>:8091

Depth without a depth camera: the gripper-mounted camera is monocular, so there's no real depth sensing. Instead, app.py back-projects the clicked pixel through the camera's current pose (derived from the arm's own end-effector feedback, {"T":105}) onto an assumed flat table plane (TABLE_Z_MM) — a ray-plane intersection, not a guess. This is the standard approach for eye-in-hand tabletop picking without a depth sensor, but it needs two things measured/tuned for your physical setup before it's accurate:

  • TABLE_Z_MM — table height in the arm's base frame.
  • CAMERA_FORWARD_OFFSET_MM / CAMERA_UP_OFFSET_MM / CAMERA_TILT_OFFSET_DEG — how the camera is actually mounted relative to the gripper TCP.
  • CAMERA_HFOV_DEG — the USB camera's real horizontal field of view (currently a rough guess; a proper checkerboard calibration would replace this and the two offsets above with real numbers).

All of these are constants at the top of dashboard/app.py.

Known issues / TODO

  • Camera/table geometry constants in dashboard/app.py are estimates — need real measurements or a checkerboard calibration pass.
  • Pi has rebooted unexpectedly a couple of times during setup (once mid Docker build) — cause not confirmed (no OOM-kill evidence in dmesg, and the last one showed a clean systemd-poweroff sequence, so possibly a manual power cycle rather than a crash). Worth keeping an eye on journalctl --list-boots.
  • roarm-driver container doesn't auto-restart the ROS node itself on Pi reboot (only the container's placeholder process does, via --restart unless-stopped) — you have to re-run the docker exec -d ... ros2 run roarm_driver step by hand after any Pi reboot.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages