Master manual control and understand motion control systems
Tutorial Overview
π― Learning Objectives
By the end of this tutorial, you will:
β Understand teleoperation architecture
β Master joystick control techniques
β Implement keyboard teleoperation
β Create custom control interfaces
β Understand velocity ramping and safety systems
β Tune motion control parameters
β Debug control issues
β Compare different control methods
β±οΈ Time Required
Reading & Theory: 20 minutes
Joystick Control Practice: 25 minutes
Keyboard Implementation: 30 minutes
Custom Control: 25 minutes
Parameter Tuning: 20 minutes
Total: ~120 minutes
π Prerequisites
β Completed ROS2 Communication & Tools
β Can visualize robot in RViz
β Understand topics and services
β Comfortable with command line
β Basic Python knowledge (for custom control)
π οΈ What You'll Need
β Beetlebot (powered, fully charged)
β Laptop with ROS2 Jazzy
β Wireless controller (Cosmic Byte Nexus)
β USB keyboard
β Open space (3m Γ 3m minimum)
β RViz for visualization
Part 1: Teleoperation Architecture
Understanding the Control Chain
Complete signal flow:
Key Nodes Explained
1. joy_node (ROS2 standard)
Reads USB controller via Linux input system
Publishes raw button/axis data
Topic: /joy
Rate: ~50-100 Hz (when buttons pressed)
2. lyra_teleop_node (Beetlebot-specific)
Maps joystick axes to velocities
Handles deadman switch (LB button)
Publishes velocity commands
Topic: /cmd_vel_joy
3. cmd_vel_mux (multiplexer)
Combines multiple velocity sources:
/cmd_vel_joy (joystick - highest priority)
/cmd_vel_nav (autonomous navigation)
/cmd_vel (keyboard/external)
Joystick always wins (safety!)
Output: /cmd_vel
4. lyra_cmd_vel_gate_node (safety)
Checks if robot is ARMED
Applies velocity limits
Emergency stop logic
Topic: /cmd_vel_safe
5. lyra_node (motor control)
Velocity ramping (smooth acceleration)
PID control for each wheel
Sends commands to STM32
Receives encoder feedback
Velocity Ramping Deep Dive
Critical feature: Beetlebot doesn't jump to commanded velocity instantly!
How it works:
Ramp rate: 5 RPM per control cycle (MAX_RPM_STEP_PER_CYCLE in robot_config.h)
Control frequency: 20 Hz (every 50ms)
Total ramp time: ~1 second (0 β max speed)
Equivalent linear acceleration: ~0.68 m/sΒ²
CRITICAL: Emergency Stop Behavior
LB button (deadman switch) has special behavior:
While LB held: Smooth ramping (as described above)
LB released: β οΈ IMMEDIATE STOP
No ramping! Motors stop instantly
Robot disarms (requires re-pressing LB to move again)
Safety feature: operator must maintain control
Why immediate stop on LB release?
Deadman switch must be instant (safety requirement)
Operator losing control = robot must stop NOW
Cannot wait 1 second for ramp-down
Example scenario:
Part 2: Mastering Joystick Control
Controller Layout Review
Cosmic Byte Nexus controller:
Beetlebot mapping:
LB (Left Bumper): ARM/Deadman switch (MUST hold!)
Left Stick Y-axis: Forward/backward speed
Right Stick X-axis: Turning (left/right)
RB (Right Bumper): Turbo mode (2Γ speed)
Other buttons: Currently unused
Basic Control Techniques
Exercise 6.1: Smooth Acceleration
Task: Feel the velocity ramping
What you learned:
Ramping makes motion smooth and predictable
LB release = emergency stop (no ramp)
Robot feels professional (not jerky like toy robots)
Advanced Driving Patterns
Exercise 6.2: Precision Maneuvering
Task 1: Straight line
Task 2: 90Β° turn
Task 3: Figure-8 pattern
Task 4: Parallel parking
Exercise 6.3: Turbo Mode
RB button doubles max speed (safety feature disabled!)
Task: Compare normal vs turbo
Understanding Joystick Dead Zones
Problem: Joystick resting position isn't exactly centered
Reads ~0.02 instead of 0.00
Would cause slow creep
Solution: Dead zone
Values < threshold treated as zero
Typical: Β±5% dead zone
Check dead zone:
Part 3: Keyboard Teleoperation
Using Built-in Keyboard Teleop
Standard ROS2 tool:
Try driving patterns:
Creating Custom Keyboard Control
[!WARNING] TODO: Exercise Script Not Included in Core Repository The custom_teleop.py script below is an exercise for the user to practice writing ROS2 publishers. It does not come pre-installed in the Beetlebot ROS2 packages. You are encouraged to follow the steps to create it yourself!
More ergonomic layout: WASD + arrow keys
Script:
Run custom teleop:
Drive with WASD keys!
Exercise 6.4: Compare Control Methods
Task: Drive same path with different control methods
Path: 2m Γ 2m square
Method 1: Joystick
Time yourself
Count how many "corrections" needed
Rate smoothness (1-10)
Method 2: Standard keyboard (i/j/k/l)
Time yourself
Count corrections
Rate smoothness
Method 3: Custom keyboard (WASD)
Time yourself
Count corrections
Rate smoothness
Analysis:
Which was fastest?
Which was smoothest?
Which required most corrections?
Which did you prefer?
Typical results
Joystick:
Fastest (analog control)
Smoothest (continuous input)
Fewest corrections
Best for general driving
Keyboard:
Slower (discrete steps)
Less smooth (on/off inputs)
More corrections needed
Good for precise positioning (step-by-step)
500ms safety timer will make it jerky as it auto disarms and rearms
Best practice: Joystick for driving, keyboard for quick testing
Part 4: Understanding /cmd_vel Topic
Twist Message Structure
Topic:/cmd_velType:geometry_msgs/msg/Twist
Message definition:
For Beetlebot (4-wheel skid-steer drive):
Publishing Manual Commands
Test commands via command line:
β οΈ Important: These are single commands! Robot executes then stops. Note: These are controller limits. Robot's Lyra controller also has hardware ramping (5 RPM/cycle, MAX_RPM_STEP_PER_CYCLE in firmware)!
For continuous motion:
Exercise 6.5: Command Line Control Patterns
Task: Drive patterns using only CLI
Pattern 1: Drive 1 meter forward
Pattern 2: Turn 180Β°
Pattern 3: Circle (radius 1m)
Part 5: Safety Systems
ARM/DISARM Mechanism
Purpose: Prevent accidental motion
ARM = Motors enabled
Can respond to /cmd_vel commands
Joystick LB button pressed
Or service call: /lyra/arm
DISARM = Motors disabled
Ignores /cmd_vel commands (for safety)
Joystick LB button released
Or service call: /lyra/disarm
Or timeout: No cmd_vel for 500ms
Check ARM Status
Manual ARM/DISARM
Command Timeout Safety
Built-in safety: If no cmd_vel received for 500ms β DISARM
Test:
Why? Safety! If controller disconnects or node crashes, robot stops.
Emergency Stop
Immediately stop robot:
When to use:
Robot behaving unexpectedly
About to collide
Software error
Any emergency situation
Velocity Limits (Safety Gate)
Safety gate node enforces maximum speeds:
Even if cmd_vel requests faster, these limits are enforced!
Example:
Part 6: Motion Control Parameters
Key Parameters
Location:/lyra_node parameters
Critical parameters:
Tuning Max Speed
Change maximum velocity:
Tuning Turn Rate
Exercise 6.6: Speed Tuning Experiment
Task: Find your preferred speed settings
Test 1: Conservative (beginner-friendly)
Test 2: Moderate (default)
Test 3: Aggressive (advanced)
Record your preferences!
Making Parameter Changes Permanent
Problem: Parameters reset on reboot
Solution: Edit config file
Part 7: Debugging Control Issues
Problem: Robot Doesn't Respond to Commands
Systematic debug process:
If still not working: β‘ Power cycle robot
Problem: Robot Moves When Joystick Centered
Cause: Joystick dead zone too small or joystick drift
Debug:
Problem: Jerky Motion
Possible causes:
Low battery
Network latency
Wheel obstruction
Check wheels spin freely
Remove any debris
Parameter issues
Problem: Robot Drifts to One Side
Causes:
Uneven floor (robot is fine, floor is tilted)
Wheel diameter mismatch (calibration needed)
Motor power imbalance (one motor weaker)
Test on flat surface first!
If problem persists:
Part 8: Advanced Control Techniques
Ackermann-Style Control (Simulated)
[!WARNING] TODO: Feature Not Implemented The ackermann_style_teleop.py script is a planned future feature and is not yet available in the Beetlebot codebase. The following code is provided as a learning exercise but may require further configuration to work correctly.
Beetlebot is a skid-steer drive, but can simulate car-like steering:
Create car-style control node:
Script:
Try car-style control!
Velocity Smoothing Script
Further smooth joystick input (beyond hardware ramping):
Part 9: Performance Metrics
Measure Control Latency
How long from button press to robot motion?
Test setup:
Measure Control Accuracy
How closely does robot follow commands?
Part 10: Knowledge Check
Concept Quiz
What happens when you release the LB button?
What's the purpose of velocity ramping?
Why does joystick override autonomous navigation?
What does /cmd_vel_safe do that /cmd_vel doesn't?
If robot won't move, what's the first thing to check?
Hands-On Challenge
Task: Create autonomous square driver
Requirements:
Python script that drives 1m Γ 1m square
No joystick input (purely programmatic)
ARMs robot at start
Uses /cmd_vel topic
Accurate timing for sides and turns
Stops and disarms at end
Bonus:
Add parameter for square size
Visualize in RViz during execution
Measure final position error
Part 11: What You've Learned
β Congratulations!
You now understand:
Control Architecture:
β Complete teleoperation signal chain
β Joy β Teleop β Mux β Safety Gate β Motor Control
β ARM/DISARM mechanism
β Emergency stop systems
Velocity Ramping:
β Smooth acceleration (5 RPM/cycle, defined as MAX_RPM_STEP_PER_CYCLE)
β LB release = immediate stop (no ramp)
β Benefits for hardware and odometry
Control Methods:
β Joystick (analog, smooth)
β Keyboard (discrete, precise)
β Programmatic (/cmd_vel topic)
β Custom control interfaces
Safety Systems:
β Deadman switch (LB button)
β Command timeout (500ms)
β Velocity limits (safety gate)
β Emergency stop service
Practical Skills:
β Driving techniques (straight, turns, patterns)
β Parameter tuning (speed, turn rate)
β Debugging control issues
β Creating custom teleop nodes
Next Steps
π― You're Now Ready For:
Immediate Next: β Sensor Fusion with EKF - Combine wheel odom + IMU for better accuracy
Navigation: β SLAM Mapping - Build maps while driving
β Autonomous Navigation - Let robot drive itself
Advanced Control:
Path following algorithms
Obstacle avoidance behaviors
Multi-robot coordination
Quick Reference
Essential Control Commands
Joystick Button Reference
Control
Function
Notes
LB
ARM / Deadman
MUST hold to move
Left Stick Y
Forward/Backward
Push up = forward
Right Stick X
Turn Left/Right
Push left = turn left
RB
Turbo Mode
2Γ speed (use carefully!)
Release LB
Emergency Stop
Immediate stop, disarm
Completed Teleoperation Control! π
β Continue to Sensor Fusion with EKF
β Or return to Tutorial Index
Last Updated: January 2026Tutorial 6 of 11 - Intermediate LevelEstimated completion time: 120 minutes
Controller (Cosmic Byte)
β USB RF Dongle
β /joy topic (sensor_msgs/Joy)
Joy Node (joy_node)
β Button/axis values
Teleop Node (lyra_teleop_node)
β /cmd_vel_joy topic (geometry_msgs/Twist)
β Converts joy β velocity
Cmd Vel Mux (mux_node)
β Arbitrates multiple cmd_vel sources
β /cmd_vel topic (geometry_msgs/Twist)
Safety Gate (lyra_cmd_vel_gate_node)
β Checks ARM status, applies limits
β /cmd_vel_safe topic
Lyra Node (lyra_node)
β Velocity ramping (smooth acceleration)
β PID control at 20 Hz
STM32 Motor Controllers
β PWM signals
DRV8874 Motor Drivers
β Current to motors
Motors β Wheels β Motion!
Target speed: 1.0 m/s (from joystick)
Current speed: 0.0 m/s (robot stationary)
Control cycle 1 (t=0.00s): Current = 0.0 m/s
β Ramp up by max_accel (5 RPM/cycle)
β New speed = 0.03 m/s
Control cycle 2 (t=0.05s): Current = 0.03 m/s
β Ramp up by 5 RPM/cycle
β New speed = 0.07 m/s
... (continue ramping)
Control cycle ~20 (t=1.00s): Current reaches target
β Ramp up
β New speed = 1.00 m/s β Target reached!
1. Driving forward at 1.0 m/s
2. Release LB button
3. Robot stops in ~0.1 seconds (not 1 second)
4. Must press LB again to resume control
Setup:
1. Place robot in open area
2. Launch RViz with odometry display
3. Power on robot, let it boot (90 seconds)
Test:
1. Press and hold LB
2. Gently push left stick forward (50%)
3. Observe in RViz:
- Velocity doesn't jump instantly
- Smooth ramp-up over ~0.5 seconds
- Steady velocity achieved
4. Release left stick (keep LB held!)
5. Observe:
- Smooth ramp-down to zero
- Not instant stop
6. Now release LB
7. Observe:
- INSTANT stop (no ramp!)
- Robot disarms
Goal: Drive exactly 2 meters in straight line
Technique:
1. Hold LB + push left stick forward 30% (slow speed)
2. Keep right stick centered (no turn input)
3. Watch odometry in RViz
4. Stop when x = 2.0 meters
Challenge: Can you stay within Β±5cm of straight line?
Tip: Small right stick movements for course corrections
Goal: Turn exactly 90Β° left
Technique:
1. Robot stationary (LB held, sticks centered)
2. Push right stick left ~40%
3. Watch orientation in RViz (or use protractor on floor)
4. Release right stick when yaw = 90Β°
Challenge: Can you achieve 90Β° Β±3Β°?
Tip: Use slow turn speed for precision
Goal: Drive smooth figure-8 (each loop 1m diameter)
Technique:
1. Forward + gentle left turn (left stick forward + right stick left)
2. Maintain constant speed throughout
3. Smooth transition at crossover point
4. Complete 3 figure-8s
Challenge: Keep loops symmetrical
Tip: Constant gentle inputs, no jerking
Goal: Park robot parallel to wall, 20cm away
Setup:
1. Place two obstacles 80cm apart (robot length + margin)
2. Start perpendicular to "parking spot"
Technique:
1. Drive forward past first obstacle
2. Turn sharp left while reversing (left stick back + right stick left)
3. Straighten out parallel to wall
4. Adjust distance with small forward/back movements
Challenge: Park within 20cm Β±2cm from wall
Test 1: Normal speed (no RB)
1. Hold LB, push left stick 100% forward
2. Observe speed in RViz (/cmd_vel topic)
3. Note: Max linear.x = ~1.0 m/s
Test 2: Turbo mode (RB pressed)
1. Hold LB + RB
2. Push left stick 100% forward
3. Observe speed
4. Note: Max linear.x = ~2.0 m/s
β οΈ Warning: Turbo mode increases:
- Crash risk (less reaction time)
- Wheel slip (especially on turns)
- Odometry drift (encoders may skip)
Use turbo only in large open areas!
# Echo joystick values
ros2 topic echo /joy
# With sticks centered, observe axes values
# Should be very close to 0.0 (within Β±0.05)
# If robot creeps with sticks centered β dead zone too small
# If robot doesn't respond to gentle stick push β dead zone too large
# On laptop (robot on same network)
ros2 run teleop_twist_keyboard teleop_twist_keyboard
# Output:
# Reading from the keyboard and publishing to Twist!
# ---------------------------
# Moving around:
# u i o
# j k l
# m , .
#
# u/o : forward + turn
# i : forward
# j/l : turn left/right in place
# m/. : backward + turn
# , : backward
# k : stop
#
# q/z : increase/decrease max speeds by 10%
# w/x : increase/decrease linear speed by 10%
# e/c : increase/decrease angular speed by 10%
1. Straight forward: Press 'i' repeatedly
2. Turn left: Press 'j' once (turns in place)
3. Circle: Press 'u' repeatedly (forward + left turn)
4. Stop: Press 'k' (or spacebar)
ros2 interface show geometry_msgs/msg/Twist
# Output:
# Vector3 linear
# float64 x
# float64 y
# float64 z
# Vector3 angular
# float64 x
# float64 y
# float64 z
# Current max
ros2 param get /lyra_node max_linear_velocity
# Response: 1.0
# Reduce for safer operation (good for beginners)
ros2 param set /lyra_node max_linear_velocity 0.5
# Now robot won't exceed 0.5 m/s, even with full joystick
# Reset to default
ros2 param set /lyra_node max_linear_velocity 1.0
# Current max turn rate
ros2 param get /lyra_node max_angular_velocity
# Response: 2.0 (rad/s)
# Reduce for gentler turns
ros2 param set /lyra_node max_angular_velocity 1.0
# Sharp turns for tight spaces
ros2 param set /lyra_node max_angular_velocity 3.0
ros2 param set /lyra_node max_linear_velocity 0.3
ros2 param set /lyra_node max_angular_velocity 0.8
# Drive around - too slow?
ros2 param set /lyra_node max_linear_velocity 0.8
ros2 param set /lyra_node max_angular_velocity 1.5
# Drive around - good balance?
ros2 param set /lyra_node max_linear_velocity 1.2
ros2 param set /lyra_node max_angular_velocity 2.5
# Drive around - too fast?
# On robot (via SSH)
nano ~/lyra_ws/src/lyra_bringup/config/lyra_params.yaml
# Find and edit:
max_linear_velocity: 0.8 # Your preferred value
max_angular_velocity: 1.5 # Your preferred value
# Save, then rebuild workspace
cd ~/lyra_ws
colcon build
source install/setup.bash
# Power cycle robot - parameters now persistent!
# 1. Is robot ARMED?
ros2 topic echo /lyra/armed --once
# If false β ARM it
# 2. Is joystick connected?
ros2 topic hz /joy
# Should show ~50-100 Hz when button pressed
# 3. Is teleop node running?
ros2 node list | grep teleop
# Should show /lyra_teleop_node
# 4. Are velocity commands being published?
ros2 topic hz /cmd_vel
# Should show >0 Hz when driving
# 5. Are commands reaching motor controller?
ros2 topic hz /cmd_vel_safe
# Should show >0 Hz
# 6. Check for errors
ros2 topic echo /rosout | grep -i error
# Check raw joystick values
ros2 topic echo /joy
# With sticks centered, axes should be ~0.0
# If reading 0.1 or 0.2 β joystick has drift
# Solution 1: Increase dead zone in teleop config
# Solution 2: Recalibrate joystick
# Solution 3: Replace joystick (if hardware issue)
# Check WiFi signal
iwconfig wlan0
# Link Quality should be >40/70
# Reset to defaults
ros2 param set /lyra_node max_linear_velocity 1.0
ros2 param set /lyra_node max_angular_velocity 2.0
# Check wheel speeds during straight drive
ros2 topic echo /wheel_rpm
# All wheels should be similar
# If one much different β mechanical issue or calibration needed
nano ~/ackermann_style_teleop.py
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from sensor_msgs.msg import Joy
class AckermannStyleTeleop(Node):
def __init__(self):
super().__init__('ackermann_style_teleop')
self.subscription = self.create_subscription(Joy, '/joy', self.joy_callback, 10)
self.publisher = self.create_publisher(Twist, '/cmd_vel', 10)
self.max_speed = 1.0
self.max_steering_angle = 0.5 # Virtual steering angle limit
def joy_callback(self, msg):
# Left trigger (axis 2) = throttle (0 to -1 range, inverted)
# Right stick X (axis 3) = steering
# LB (button 4) = deadman
if len(msg.buttons) < 5 or msg.buttons[4] == 0:
# LB not pressed, don't move
return
throttle = -msg.axes[2] if len(msg.axes) > 2 else 0.0 # Invert axis
steering = msg.axes[3] if len(msg.axes) > 3 else 0.0
# Convert throttle to linear speed
linear_speed = throttle * self.max_speed
# Convert steering angle to angular velocity
# angular = v * tan(steering_angle) / wheelbase
# Simplified: angular β steering * speed
angular_speed = steering * linear_speed * 2.0 # Scale factor
twist = Twist()
twist.linear.x = linear_speed
twist.angular.z = angular_speed
self.publisher.publish(twist)
def main(args=None):
rclpy.init(args=args)
node = AckermannStyleTeleop()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()