Complete step-by-step instructions for assembling the hardware, installing the software, and running the system for the first time.
- Prerequisites
- Hardware assembly
- Arduino firmware
- Python environment
- Configuration
- Verify the installation
- First run
- Troubleshooting
| Item | Qty | Notes |
|---|---|---|
| Arduino Uno R3 | 1 | Or compatible (Nano works on pins 3–13) |
| SG90 or MG996R servo motors | 3 | SG90 for light loads; MG996R for heavier arm |
| L298N dual H-bridge module | 1 | The common red breakout board |
| DC gear motors (3–6V) | 2 | For the mobile base wheels |
| USB webcam | 1 | Any 720p+ camera; built-in laptop cam works |
| USB cable (Type-A to Type-B) | 1 | Arduino to host PC |
| Jumper wires | ~30 | Male-to-male and male-to-female |
| 5V power supply or USB power bank | 1 | For servo power rail |
| 7–12V power supply | 1 | For L298N motor power (separate from logic) |
| Breadboard | 1 | Optional but recommended |
| Software | Version | Purpose |
|---|---|---|
| Python | 3.9 or higher | Runtime |
| Arduino IDE | 2.x | Firmware upload |
| Git | Any | Clone repo |
| OS | Status | Notes |
|---|---|---|
| Windows 10/11 | ✅ Fully supported | COM port format: COM6 |
| Ubuntu 20.04 / 22.04 | ✅ Fully supported | Port format: /dev/ttyUSB0 |
| macOS 12+ | ✅ Fully supported | Port format: /dev/cu.usbmodem* |
Wire each servo's three leads to the Arduino and power rail:
Servo X (horizontal pan)
├── Signal (orange/yellow) → Arduino D3
├── VCC (red) → 5V power rail
└── GND (brown/black) → Common GND
Servo Y (vertical tilt)
├── Signal → Arduino D5
├── VCC → 5V power rail
└── GND → Common GND
Servo Z (grip)
├── Signal → Arduino D6
├── VCC → 5V power rail
└── GND → Common GND
⚠️ Do not power servos from the Arduino 5V pin. Three servos draw 600–900mA under load, which will reset or damage the Arduino. Use a dedicated 5V supply or USB power bank. Connect GND of the power supply to Arduino GND.
L298N module Arduino / motors
─────────────────────────────────────
IN1 → D7
IN2 → D8
ENA (PWM) → D9 ← remove the jumper cap on ENA
IN3 → D13
IN4 → D12
ENB (PWM) → D10 ← remove the jumper cap on ENB
OUT1 + OUT2 → Left motor (red/black leads)
OUT3 + OUT4 → Right motor (red/black leads)
12V → 7–12V motor power supply +
GND → Motor power supply − AND Arduino GND
5V (logic out) → (optional) can power Arduino if no USB
⚠️ Keep motor power supply GND connected to Arduino GND. Without a common ground, the L298N signal lines will not work.
HOST PC ──USB──► Arduino Uno
│
D3 ────► Servo X signal
D5 ────► Servo Y signal
D6 ────► Servo Z signal
D7 ────► L298N IN1
D8 ────► L298N IN2
D9 ────► L298N ENA (PWM)
D10 ────► L298N ENB (PWM)
D12 ────► L298N IN4
D13 ────► L298N IN3
GND ────► Common GND rail
│
5V supply ──────────► Servo VCC rail
GND ────────────────► Common GND rail
7–12V supply ───────► L298N 12V
GND ────────────────► Common GND rail
L298N OUT1/OUT2 ────► Left DC motor
L298N OUT3/OUT4 ────► Right DC motor
Mount the webcam directly above or below the screen, facing you. The system splits the frame vertically — your right hand should be able to reach the left half of the frame, and your left hand the right half. A camera-to-operator distance of 60–90 cm works well.
Download from arduino.cc/en/software and install.
The firmware uses StandardFirmata, which ships with the Arduino IDE:
Arduino IDE → File → Examples → Firmata → StandardFirmata
Alternatively, use the custom firmware in this repo (identical to StandardFirmata but with confirmed servo support):
firmware/server.ino
- Connect Arduino Uno via USB
- Open
firmware/server.inoin the Arduino IDE - Select board: Tools → Board → Arduino AVR Boards → Arduino Uno
- Select port: Tools → Port → COM6 (Windows) or /dev/ttyUSB0 (Linux/macOS)
- Click Upload (→ button)
- Wait for "Done uploading"
Windows:
Device Manager → Ports (COM & LPT) → Arduino Uno (COMx)
Linux:
ls /dev/ttyUSB* /dev/ttyACM*
# Expected: /dev/ttyUSB0 or /dev/ttyACM0
# Grant permission if needed:
sudo chmod a+rw /dev/ttyUSB0
# Or add yourself to the dialout group (permanent):
sudo usermod -aG dialout $USERmacOS:
ls /dev/cu.usbmodem* /dev/cu.usb*git clone https://github.com/minhtetmyet/gesture-arm.git
cd gesture-arm# Create
python -m venv .venv
# Activate
# Windows:
.venv\Scripts\activate
# Linux / macOS:
source .venv/bin/activateMinimum install (gesture control only, no LSTM training):
pip install -e .Full install (includes TensorFlow for LSTM training):
pip install -e ".[ml]"Development install (adds pytest, notebooks, linting tools):
pip install -e ".[ml,dev]"PyAudio requires PortAudio. Install the system dependency first:
Windows:
pip install pipwin
pipwin install pyaudioUbuntu / Debian:
sudo apt-get install portaudio19-dev python3-pyaudio
pip install pyaudiomacOS:
brew install portaudio
pip install pyaudiopython -c "
import cv2, numpy, pyfirmata, speech_recognition, pyttsx3, yaml
print('Core dependencies OK')
try:
import tensorflow as tf
print(f'TensorFlow {tf.__version__} OK')
except ImportError:
print('TensorFlow not installed (optional for LSTM training)')
"Expected output:
Core dependencies OK
TensorFlow 2.x.x OK
All settings are in gesture_arm/config/default.yaml. Open it and update the serial port:
hardware:
port: "COM6" # ← change this to your Arduino port
baudrate: 57600Common port values:
| OS | Typical port |
|---|---|
| Windows | "COM3", "COM4", "COM6" |
| Linux | "/dev/ttyUSB0", "/dev/ttyACM0" |
| macOS | "/dev/cu.usbmodem14101" |
You can also override the port at runtime without editing the file:
# Windows PowerShell:
$env:GESTURE_ARM_PORT = "COM4"
python -m gesture_arm.run
# Linux / macOS:
GESTURE_ARM_PORT=/dev/ttyUSB0 python -m gesture_arm.runIf the system opens the wrong camera, change camera_index in the config:
vision:
camera_index: 0 # 0 = default/first camera, 1 = second camera, etc.To find your camera index:
import cv2
for i in range(5):
cap = cv2.VideoCapture(i)
if cap.isOpened():
print(f"Camera {i}: available")
cap.release()Run the hardware-free smoke test before connecting the Arduino:
python -m gesture_arm.run --no-hardwareThis opens the camera feed with hand tracking overlays but does not attempt to connect to the Arduino. You should see:
- Live camera window titled "Gesture Arm Control"
- Blue rectangle on the left half (right-hand zone)
- Green rectangle on the right half (left-hand zone)
- Hand landmarks drawn when hands are visible
Press Q to quit.
pytest tests/ -vAll tests should pass without hardware or TensorFlow connected.
Plug in the Arduino USB cable. Confirm the port in config/default.yaml.
python scripts/collect.py --duration 90Move your left hand slowly and deliberately through the full range of motion in the right half of the camera frame:
- Left to right (full X range)
- Top to bottom (full Y range)
- Pinch open and closed repeatedly (full Z range)
The script displays a progress bar and sample counter. 90 seconds yields approximately 2,700 samples at 30 fps — sufficient for training. Press Q to save and exit early.
Data is saved to data/training_data.csv.
python scripts/train.pyTraining takes approximately 2–4 minutes on CPU. You will see the Keras training progress with loss and validation MAE per epoch. Early stopping will halt training if validation loss stops improving.
The trained model is saved to models/lstm_gesture_model.h5.
python -m gesture_arm.runRight hand controls (left camera zone):
| Gesture | Action |
|---|---|
| Hand in upper zone | Forward |
| Hand in lower zone | Reverse |
| Hand in left zone | Turn left |
| Hand in right zone | Turn right |
| Closed fist | Stop (emergency override) |
Left hand controls (right camera zone):
| Motion | Servo | Range |
|---|---|---|
| Move left/right | Servo X (pan) | 60° – 180° |
| Move up/down | Servo Y (tilt) | 40° – 140° |
| Pinch open/close | Servo Z (grip) | 100° – 150° |
Voice commands (speak clearly):
| Word | Action |
|---|---|
| "forward" / "go" | Move forward |
| "reverse" / "back" | Move backward |
| "left" | Turn left |
| "right" | Turn right |
| "stop" | Stop motors |
Press Q to exit. A metrics summary is printed to the terminal.
serial.SerialException: could not open port 'COM6'
- Confirm the port in Device Manager /
ls /dev/ttyUSB* - Update
portinconfig/default.yaml - Try unplugging and replugging the USB cable
- On Linux, run
sudo chmod a+rw /dev/ttyUSB0
Camera read failed — is camera_index correct?
- Run the camera detection script in Section 5 to find the correct index
- Update
camera_indexinconfig/default.yaml - On Linux, ensure your user is in the
videogroup:sudo usermod -aG video $USER
- Ensure lighting is adequate — MediaPipe struggles in dark or strongly backlit conditions
- Keep your hand within 30–80 cm of the camera
- Avoid backgrounds with skin-like colors
- Try increasing
detection_confidenceto 0.9 or decreasing it to 0.7 in the config
- Confirm servos are powered from a dedicated 5V supply, not the Arduino 5V pin
- Check that the signal wires are connected to the correct pins (D3, D5, D6)
- Verify GND of the servo power supply is connected to Arduino GND
ASR service error: recognition connection failed
- The default speech engine (Google) requires an internet connection
- Check firewall / proxy settings
- For offline use, install
vosk:pip install vosk— a future update will add offline ASR support
Use pipwin instead of pip directly:
pip install pipwin
pipwin install pyaudioTensorFlow 2.x requires Python 3.9–3.11 and a 64-bit OS. Common fix:
pip install tensorflow --upgradeIf you are on an M1/M2 Mac:
pip install tensorflow-macos tensorflow-metalNo model found at models/lstm_gesture_model.h5 — building untrained model.
You need to collect data and train before running the full LSTM mode. The system will still work in baseline mode. Run:
python scripts/collect.py
python scripts/train.pyIK mode maps your hand position to a Cartesian end-effector target instead of directly to servo angles. This requires two additional steps.
With the arm powered off, measure physically with a ruler:
l1 = distance from the servo-Y pivot (shoulder) to the end-effector mount point
l2 = distance from the mount point to the TCP (fingertip of the gripper)
Typical values for a standard 3D-printed SG90 arm: l1 ≈ 10 cm, l2 ≈ 8 cm.
# gesture_arm/config/default.yaml
kinematics:
enabled: false # keep false; use --ik flag at runtime instead
link1_cm: 10.0 # ← your measured l1
link2_cm: 8.0 # ← your measured l2
servo_x_neutral_deg: 120.0 # servo X angle when arm faces forward
servo_y_zero_deg: 40.0 # servo Y angle when arm is horizontalpython -m gesture_arm.run --ikOr enable permanently in the config (kinematics.enabled: true) and run normally.
Check that the arm reaches the expected positions. If the arm overshoots or undershoots, your link lengths may be slightly off. Run the FK check in a Python console:
from gesture_arm.kinematics import GeometricIKSolver
solver = GeometricIKSolver(link1_cm=10.0, link2_cm=8.0)
# Check: what TCP position does servo X=120, Y=90 produce?
px, py, pz = solver.forward(120, 90)
print(f"TCP at: ({px:.1f}, {py:.1f}, {pz:.1f}) cm")
# Check: can the arm reach (10, 0, 5)?
r = solver.solve(10, 0, 5)
print(r.message)
print(f"Servo X={r.angles[0]:.1f}° Y={r.angles[1]:.1f}°")Adjust link1_cm and link2_cm until forward() output matches what
you observe physically.
If IK is enabled but the target is outside the workspace, the system
falls through to LSTM/baseline automatically. The HUD method badge
shows the current path: [ik], [lstm], or [baseline].
The metrics CSV method column records which path produced each frame's
servo command, enabling comparison in the benchmark notebook.