Skip to content

Latest commit

Β 

History

History
444 lines (316 loc) Β· 16.7 KB

File metadata and controls

444 lines (316 loc) Β· 16.7 KB

Engineer2026 Control

Control stack for the Illini RoboMaster Engineer robot (2026 cycle).

This repository contains ROS 2 Humble packages for a 6-DOF robotic arm: URDF description, ros2_control setup, MoveIt2 motion planning, and teleoperation via AprilTag cube or keyboard. It targets an OrangePi SBC communicating with an MCU (ESP32/STM32) over UART.

The arm is teleoperated using an AprilTag cube held by the operator. A standalone Python client (arm_vision/) handles all camera work β€” calibration, AprilTag detection, and pose estimation β€” and streams the target end-effector pose to the ROS stack over a plain TCP socket. No ROS is required on the client machine.


System Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  arm_vision/  (standalone Python β€” no ROS)                       β”‚
β”‚                                                                  β”‚
β”‚  webcam β†’ AprilTag detection β†’ cube pose β†’ workspace mapping     β”‚
β”‚                                         ↓                        β”‚
β”‚                            6D EE target pose (JSON over TCP)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚ TCP socket (default port 9999)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  ROS 2 Humble  (runs on OrangePi)                                β”‚
β”‚                                                                  β”‚
β”‚  socket_teleop_node   receives target pose, P-controller         β”‚
β”‚  keyboard_teleop_node ──► /servo_node/delta_twist_cmds           β”‚
β”‚                           /servo_node/delta_joint_cmds           β”‚
β”‚  servo_node (MoveIt2) ──► /arm_controller/joint_trajectory       β”‚
β”‚  move_group                                                      β”‚
β”‚  ros2_control         ──► mock_components  OR  uart_bridge_node β†’ MCU   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Package Structure

Path Description
robotic_arm_v4_urdf/ Robot URDF, ros2_control config, MoveIt2 config, core launch files
arm_teleop/ socket_teleop_node (TCP receiver + P-controller) and keyboard_teleop_node
arm_hardware/ UART bridge node, homing node β€” communicate with the MCU over UART
arm_bringup/ Top-level launch that ties all ROS packages together
arm_vision/ Standalone Python client: camera cal, cube cal, detection, socket sender
scripts/ Shell helpers for setup, build, and launch

Prerequisites

OS: Ubuntu 22.04 (Jammy) β€” ROS 2 Humble must already be installed.

Install all ROS and Python dependencies:

bash scripts/setup_ros2_humble_deps.sh

Install standalone client dependencies (no ROS needed):

cd arm_vision
pip install -r requirements.txt

Build (ROS)

source /opt/ros/humble/setup.bash
colcon build --base-paths robotic_arm_v4_urdf arm_teleop arm_hardware arm_bringup
source install/setup.bash

Or use the helper script (builds then launches):

bash scripts/run_ros2_humble_moveit_control.sh

Running

1 β€” Start the ROS stack

Simulation only β€” Plan+Execute via RViz (no hardware, no vision):

source install/setup.bash
ros2 launch arm_bringup arm_bringup.launch.py use_teleop:=false

Real robot β€” Plan+Execute via RViz (no vision pipeline):

ros2 launch arm_bringup arm_bringup.launch.py use_real_robot:=true use_teleop:=false uart_port:=/dev/yourdevice

Real robot + arm_vision socket teleop (full system):

ros2 launch arm_bringup arm_bringup.launch.py use_real_robot:=true uart_port:=/dev/yourdevice

Headless (no RViz):

ros2 launch arm_bringup arm_bringup.launch.py use_moveit_rviz:=false use_teleop:=false

2 β€” Start the arm_vision client (separate machine or terminal)

cd arm_vision
python main.py run --host <ros-host-ip> --show

The client opens the webcam, detects the AprilTag cube, maps its pose to a target EE position, and streams it to the ROS socket_teleop_node at 30 Hz.

3 β€” Homing (real robot only)

Homing moves each joint to 0 rad in a collision-aware order. The group sequence is chosen dynamically based on Joint2's current angle at startup:

J2 angle Sequence Rationale
J2 β‰₯ 0Β° (forward) [J2] β†’ [J3,J4,J5,J6] β†’ [J1] Retract shoulder first to clear space
J2 < 0Β° (back) [J3] β†’ [J2] β†’ [J4,J5,J6] β†’ [J1] Fold elbow first to avoid body collision

Run automatically at startup by adding run_homing:=true to the bringup:

ros2 launch arm_bringup arm_bringup.launch.py use_real_robot:=true uart_port:=/dev/yourdevice run_homing:=true

Or run it as a standalone node while the stack is already running:

source install/setup.bash
ros2 run arm_hardware homing_node

Key parameters (override with --ros-args -p name:=value):

Parameter Default Description
joint_speed_rad_s 0.3 Max speed used to compute each step duration
min_duration_s 2.0 Minimum time allocated per joint step
settle_time_s 0.5 Extra settle wait after each step

4 β€” Joint Monitor (observe live joint angles)

The joint monitor prints joint angles to the terminal while the arm moves or a MoveIt plan is computed. It is a lightweight diagnostic tool β€” no effect on motion.

Recommended: dedicated terminal (cleanest output)

bash scripts/run_joint_monitor.sh

Or inline via bringup:

ros2 launch arm_bringup arm_bringup.launch.py print_joints:=true

Example output:

[CURRENT]   Joint1=  +0.00Β°  Joint2= -45.23Β°  Joint3= +12.10Β°  Joint4=  +0.00Β°  Joint5=  +5.50Β°  Joint6=  +0.00Β°
[PLAN GOAL] Joint1=  +0.00Β°  Joint2=  +0.00Β°  Joint3=  +0.00Β°  Joint4=  +0.00Β°  Joint5=  +0.00Β°  Joint6=  +0.00Β°
  • [CURRENT] β€” updates in-place at each /joint_states tick (live arm position).
  • [PLAN GOAL] β€” printed whenever MoveIt computes a plan (drag the interactive marker in RViz and click Plan).

5 β€” View-only mode (real arm angles in RViz, no motion commands)

Shows live encoder feedback from the MCU as a moving robot model in RViz. No motion commands are ever sent to the hardware in this mode.

bash scripts/run_view_only.sh --port /dev/yourdevice
# with raw-frame debug logging:
bash scripts/run_view_only.sh --port /dev/yourdevice --debug

If --port is not given the script prints available /dev/tty* devices and exits.

This starts only: robot_state_publisher + uart_bridge_node (RX only) + RViz(display.rviz).

6 β€” Keyboard teleoperation (optional, separate terminal)

source install/setup.bash
ros2 run arm_teleop keyboard_teleop_node

Or:

bash scripts/run_keyboard_teleop.sh

7 β€” MCU Emulator (test without real hardware)

The emulator creates a virtual serial port pair so you can run and test the full ROS stack β€” homing, Plan+Execute, and vision teleop β€” without the real robot connected. It simulates the MCU's motor PID: positions approach the commanded angles with a configurable time constant.

Terminal 1 β€” start the emulator:

# All joints at zero (default):
python3 scripts/mcu_emulator.py

# Start at non-zero positions to test homing:
python3 scripts/mcu_emulator.py --start 30,-20,15,-10,50,5

# Slower motor response (default tau=0.2s):
python3 scripts/mcu_emulator.py --start 30,-20,15,-10,50,5 --tau 0.5

The emulator prints the virtual serial port path:

╔══════════════════════════════════════════════════════════════╗
β•‘  MCU Emulator Ready                                        β•‘
β•‘  Connect the bridge with:                                   β•‘
β•‘    uart_port:=/dev/pts/7                                   β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

Terminal 2 β€” launch the full ROS stack with that port:

# Plan+Execute from RViz only (no vision):
ros2 launch arm_bringup arm_bringup.launch.py \
  use_real_robot:=true uart_port:=/dev/pts/7 use_teleop:=false

# With homing on startup:
ros2 launch arm_bringup arm_bringup.launch.py \
  use_real_robot:=true uart_port:=/dev/pts/7 use_teleop:=false run_homing:=true

# Full pipeline β€” vision teleop β†’ MoveIt β†’ emulated arm:
ros2 launch arm_bringup arm_bringup.launch.py \
  use_real_robot:=true uart_port:=/dev/pts/7
# then in a third terminal:
python arm_vision/main.py run --host localhost --show

Emulator output legend:

Color Tag Meaning
Cyan [MCU RX TRAJ] Command frame received from bridge during a trajectory
Cyan [MCU RX HOLD] Command frame received during idle-hold
Yellow [MCU TX MOVE] Encoder feedback sent back β€” motors still converging
Yellow [MCU TX HOLD] Encoder feedback sent back β€” motors at target
Red [MCU WATCHDOG] No command received within timeout β€” TX gap detected

Emulator options:

Option Default Description
--start J1,J2,J3,J4,J5,J6 0,0,0,0,0,0 Initial motor positions in degrees
--rate Hz 20 Feedback send rate (should match send_rate_hz)
--tau s 0.2 Motor time constant β€” how fast position approaches target
--watchdog s 1.0 Warn if no command received for this long
--quiet off Only print changes > 0.5Β° and warnings

8 β€” Pipeline test (mock arm_vision sender)

Sends fake target poses over TCP without a camera, to test the full post-vision pipeline:

# Interactive mode β€” type poses manually:
python scripts/test_pose_sender.py

# Demo mode β€” loops through a preset waypoint sequence:
python scripts/test_pose_sender.py --demo

# Hold a fixed pose:
python scripts/test_pose_sender.py --hold 0.3 0.0 0.2

Calibration Workflow

All calibration is done from the arm_vision/ directory, with no ROS running.

Step 1 β€” Camera calibration

Print a checkerboard (default 9Γ—6 inner corners, 25 mm squares). Show it from multiple angles while pressing SPACE to capture frames. Press Q when done (β‰₯10 captures recommended).

cd arm_vision
python main.py calibrate-camera

Output: arm_vision/data/camera_calibration.yaml

Step 2 β€” Cube calibration (5 faces)

For each of the 5 AprilTag faces on the cube:

  1. Point that face toward the camera.
  2. Press SPACE to capture.
  3. Enter the face label when prompted (+X, -X, +Y, -Y, +Z).
python main.py calibrate-cube --side-length 0.06

Output: arm_vision/data/cube_config.yaml

Step 3 β€” Workspace mapping

Edit arm_vision/config/workspace.yaml to match your physical setup:

Field Description
cam_origin Neutral cube position in camera frame (m) β€” where the arm should sit at rest
ee_origin EE position in base_link at neutral β€” read from /tf at home pose
scale Amplification: how much robot moves per unit of cube movement
cam_to_robot 3Γ—3 rotation matrix mapping camera axes β†’ robot base_link axes

Launch Arguments (arm_bringup.launch.py)

Argument Default Description
use_real_robot false true to start the UART bridge to the MCU
uart_port (required when use_real_robot:=true) Serial device for the MCU connection, e.g. /dev/ttyUSB0
baud_rate 115200 UART baud rate
use_teleop true false to skip socket_teleop and use MoveIt Plan+Execute directly
socket_host 0.0.0.0 TCP bind address for the pose socket
socket_port 9999 TCP bind port for the pose socket
use_moveit_rviz true Show MoveIt2 RViz panel
run_homing false Run sequential homing on startup (real robot only)
print_joints false Print live joint angles + MoveIt plan goals to terminal

arm_vision CLI Reference

python main.py calibrate-camera  [--device N] [--cols 9] [--rows 6]
                                  [--square-size 0.025] [--output FILE]

python main.py calibrate-cube    [--device N] [--tag-family tag36h11]
                                  [--side-length 0.06] [--camera-cal FILE]
                                  [--output FILE]

python main.py run               [--device N] [--host IP] [--port 9999]
                                  [--camera-cal FILE] [--cube-config FILE]
                                  [--workspace-config FILE] [--show]

UART Protocol & Joint Mapping

Physical layer

  • Device: configured via uart_port:=/dev/yourdevice at launch time (or set port in hardware_params.yaml)
  • Baud rate: 115200, 8N1

To find the correct device:

ls /dev/ttyUSB* /dev/ttyACM* /dev/ttyCH341* 2>/dev/null
# plug in the USB-UART cable, then run again and note what appeared

Frame format

Fixed 16-byte binary frame, same structure in both directions:

Offset  Bytes  Field
──────  ─────  ──────────────────────────────────────────────
0       1      SOF = 0xA5  (sync sentinel)
1       1      LEN = 0x0C  (payload byte count = 12)
2–13    12     6 Γ— int16_t, little-endian, centidegrees (1/100 Β°)
               order: J1 J2 J3 J4 J5 J6
14–15   2      CRC16-MODBUS (poly 0xA001, init 0xFFFF) over bytes [0..13]
               little-endian (lo byte first)
──────  ─────  ──────────────────────────────────────────────
Total   16 bytes

Values are in motor degrees (after sign-flip and gear-ratio). Resolution is 0.01Β°, range Β±327.67Β°. Frames failing SOF, LEN, or CRC checks are dropped with a [WARN] log; the bridge re-syncs by scanning for the next 0xA5 byte.

Suggested MCU DMA setup (STM32 HAL):

HAL_UART_Receive_DMA(&huart, rx_buf, 16);  // fires once per complete frame

Joint sign & gear ratio

The MCU reports raw motor angles. uart_bridge_node converts to/from URDF joint angles using two per-joint parameters in arm_hardware/config/hardware_params.yaml:

TX: motor_angle = sign Γ— joint_angle Γ— gear_ratio
RX: joint_angle = sign Γ— motor_angle Γ· gear_ratio
Parameter Default Description
joint_sign_flip [-1, -1, 1, -1, -1, 1] +1 = same direction as URDF, -1 = flip
joint_gear_ratio [2, 1, 1, 1, 1, 1] Motor turns per joint turn (J1 has 2:1 reduction)

Order for both lists: [Joint1, Joint2, Joint3, Joint4, Joint5, Joint6].

To tune these values: run view-only mode, move each joint by hand, and adjust until the RViz model matches the physical arm.

hardware_params.yaml key parameters

Parameter Default Description
send_rate_hz 20.0 TX rate to MCU (Hz) β€” keep ≀ 20 Hz to avoid MCU DMA overrun
command_timeout_s 0.5 Idle-hold refresh interval β€” re-TX last position to keep MCU watchdog alive
override_joint_states false Set true (real robot) to publish MCU encoder feedback as /joint_states
debug_rx false Log every raw RX frame and parsed values to the ROS terminal
debug_tx false Log every TX frame with source tag (TRAJ or HOLD) and frame counter
servo_hold_after_traj_s 2.0 Block servo topic messages for this long after a trajectory ends

Hardware

  • SBC: OrangePi (RK3588, ARM64)
  • MCU: ESP32/STM32 connected via USB-UART dongle (pass device path as uart_port:=...)
  • Camera: USB webcam (any OpenCV-compatible device)
  • Arm: 6-DOF (Joint1 – Joint6)

Serial port access: add your user to the dialout group if the device permission is denied:

sudo usermod -aG dialout $USER   # log out and back in to apply
sudo systemctl stop ModemManager  # prevents ModemManager hijacking ttyACM* devices