Diagnostic guide for every common failure mode, with exact error messages and fixes.
Run this first to get a system status snapshot:
python -c "
import sys
print(f'Python {sys.version}')
import cv2; print(f'OpenCV {cv2.__version__}')
import numpy; print(f'NumPy {numpy.__version__}')
import yaml; print('PyYAML OK')
try:
import pyfirmata; print('pyFirmata OK')
except ImportError: print('pyFirmata MISSING')
try:
import speech_recognition as sr; print(f'SpeechRecognition OK')
except ImportError: print('SpeechRecognition MISSING')
try:
import pyttsx3; print('pyttsx3 OK')
except ImportError: print('pyttsx3 MISSING')
try:
import tensorflow as tf; print(f'TensorFlow {tf.__version__}')
except ImportError: print('TensorFlow not installed (optional)')
try:
import cvzone; print('cvzone OK')
except ImportError: print('cvzone MISSING')
"- Arduino / serial issues
- Camera issues
- Hand detection issues
- Servo issues
- Motor issues
- Speech recognition issues
- LSTM / TensorFlow issues
- Python / dependency issues
- Performance issues
- CI / Docker issues
The configured port does not exist or is in use by another process (e.g. the Arduino IDE's serial monitor).
Fix:
- Find the correct port:
- Windows: Device Manager → Ports (COM & LPT) → look for "Arduino Uno"
- Linux:
ls /dev/ttyUSB* /dev/ttyACM* - macOS:
ls /dev/cu.usbmodem*
- Update
portingesture_arm/config/default.yaml, or:GESTURE_ARM_PORT=COM4 python -m gesture_arm.run
- Close the Arduino IDE serial monitor if it is open.
User does not have read/write permission on the serial port.
Fix (Linux/macOS):
# Temporary (until next reboot):
sudo chmod a+rw /dev/ttyUSB0
# Permanent (requires logout):
sudo usermod -aG dialout $USERThe firmware is not StandardFirmata, or pyFirmata's iterator thread did not start.
Fix:
- Re-upload
firmware/server.inofrom the Arduino IDE - Confirm the board is "Arduino Uno" in Tools → Board
- The code calls
pyfirmata.util.Iterator(board).start()— checkhardware/arduino.pyconnect()function includes this
A pin is being defined twice — usually because connect() is called more than once.
Fix: Ensure connect() is called exactly once. Use the board_session() context manager to prevent accidental double-initialization.
Camera read failed — is camera_index correct?
Fix:
# Find available cameras
import cv2
for i in range(5):
cap = cv2.VideoCapture(i)
if cap.isOpened():
print(f"Camera {i}: OK")
cap.release()Update camera_index in config/default.yaml.
You have multiple cameras (e.g. built-in + USB webcam) and the wrong one is selected.
Fix: Change camera_index from 0 to 1 (or higher) in the config.
The camera is set to a resolution the USB bandwidth cannot sustain.
Fix: Reduce resolution in the config:
vision:
width: 640
height: 480Or force the frame rate:
cap.set(cv2.CAP_PROP_FPS, 30)cap.read() returned an empty frame.
Fix: Check that no other application has the camera open (Zoom, Teams, OBS, etc.). Only one process can access a webcam at a time on most operating systems.
- Room is too dark or strongly backlit from behind you
- Hand is too close (<20cm) or too far (>120cm) from camera
- Background color is close to skin tone
Fix:
- Add a light source facing you
- Move your hand to 50–80cm from the camera
- Use a plain, non-skin-toned background
- Lower
detection_confidencein config to0.6
MediaPipe may drop one hand if both overlap significantly or if the frame is crowded.
Fix:
- Keep hands clearly separated in the frame
- Confirm
max_hands: 2inconfig/default.yaml
Your camera may horizontally mirror the image (common with front-facing webcams).
Fix: Add a flip after cap.read() in run.py:
ret, frame = cap.read()
frame = cv2.flip(frame, 1) # horizontal mirror- Servos are drawing power from the Arduino 5V pin (insufficient current)
- Signal wire picking up interference
Fix:
- Power servos from a dedicated 5V supply, not the Arduino's 5V pin
- Shorten or twist the signal wire
- Add a 100µF capacitor between the servo power rail and GND
The min_deg / max_deg bounds in the config are incorrect, or the servo is receiving a value outside the 0–180° range.
Fix:
- Print the angle being commanded: check the
servoX/Y/Zdisplay in the HUD - Narrow the bounds in
config/default.yaml ArmController.write()clamps angles — if the HUD shows a valid angle but the servo is at an extreme, check the physical attachment of the servo horn
The commanded angle is beyond the servo's physical range of motion.
Fix: Narrow min_deg / max_deg in the config until the grinding stops. The grinding means the servo output shaft is hitting its internal mechanical stop while the motor continues to push.
The LSTM buffer is filling with slightly different feature vectors due to hand tracking micro-jitter, causing slow output drift.
Fix:
- Increase
sequence_lengthin the config (e.g. from 15 to 20) for more smoothing - Check that the model is trained — if it is untrained, the output is random
Common causes:
- ENA/ENB jumper caps still on — caps lock speed at full, but only if PWM is not connected. With PWM connected and caps on, the caps are overridden. Actually more likely: the jumpers are off but PWM is not connected.
- Common ground missing — L298N logic GND not connected to Arduino GND
- Motor power supply off or wrong voltage
Fix:
- Remove ENA and ENB jumper caps
- Connect L298N GND to Arduino GND
- Verify motor power supply is 7–9V and its GND is connected to common GND
Motor supply voltage is too low. The L298N has ~2V dropout, so a 5V supply delivers only ~3V to the motors.
Fix: Use a 7–9V power supply for the motor rail.
Motor wiring is reversed (common if you swap OUT1/OUT2 or OUT3/OUT4).
Fix: Swap the two leads of the affected motor at the L298N terminals. Do not change code.
The code's "left" command turns the robot right, or vice versa.
Fix: In config/default.yaml, swap the left and right command motor arguments:
commands:
left: [1, 0, 1.0, 0, 1, 0.5] # ← swap these two lines
right: [0, 1, 0.5, 0, 1, 1.0]No internet connection, or the Google API is temporarily unavailable.
Fix:
- Check internet connectivity
- The system continues in gesture-only mode; speech commands will not work until connectivity is restored
- For offline use, implement
voskas an alternative backend inASRListener._run()
The recognized word does not exactly match a command key.
Fix: Check the terminal output for [Speech] Heard: '...'. If the word is close but not exact (e.g. "forwards" instead of "forward"), add the variant to config/default.yaml:
commands:
forward: [1, 0, 1.0, 0, 1, 1.0]
forwards: [1, 0, 1.0, 0, 1, 1.0] # add synonymOSError: [Errno -9996] Invalid input device (no default)
No microphone is available or the default audio device is wrong.
Fix:
import speech_recognition as sr
for i, mic in enumerate(sr.Microphone.list_microphone_names()):
print(f"{i}: {mic}")Then in gesture_arm/speech/multimodal.py, change sr.Microphone() to sr.Microphone(device_index=N) where N is the index of your microphone.
Running in baseline (frame-by-frame mapping) mode.
TensorFlow is not installed. The system still works in baseline mode.
Fix:
pip install tensorflow
# macOS M1/M2:
pip install tensorflow-macos tensorflow-metalTraining has not been run yet.
Fix:
python scripts/collect.py # collect training data first
python scripts/train.py # then train- Insufficient training data (fewer than ~1000 samples)
- Learning rate too high
Fix:
- Collect more data:
python scripts/collect.py --duration 180 - Lower the learning rate in
config/default.yaml:learning_rate: 0.0005
The loaded model's input shape does not match the current sequence_length or feature_dim config.
Fix: Either retrain the model with the current config, or revert the config to match the model's original settings. The model's expected input shape can be inspected with:
from tensorflow.keras.models import load_model
model = load_model("models/lstm_gesture_model.h5")
print(model.input_shape) # (None, seq_len, feat_dim)pip install cvzoneIf cvzone installs but fails to import with a MediaPipe error:
pip install mediapipe --upgradepip install pipwin
pipwin install pyaudiosudo apt-get install portaudio19-dev python3-dev
pip install pyaudioOpenCV requires libGL on headless Linux.
sudo apt-get install libgl1-mesa-glx
# or for headless use:
pip install opencv-python-headlessTypical causes and fixes:
| Cause | Fix |
|---|---|
| Camera USB bandwidth | Reduce resolution in config: width: 640, height: 480 |
| LSTM inference slow | Reduce sequence_length or lstm_units in config |
| MediaPipe slow | Reduce detection_confidence to 0.6 |
| Too many windows open | Close unused applications |
To measure frame rate:
import time
t0 = time.time()
frame_count = 0
# ... in the loop:
frame_count += 1
if frame_count % 30 == 0:
fps = 30 / (time.time() - t0)
t0 = time.time()
print(f"FPS: {fps:.1f}")This can cause the vision loop to drop below 30fps.
Fix: Reduce model complexity in config/default.yaml:
model:
sequence_length: 10 # was 15
lstm_units: 32 # was 64Then retrain.
ruff check --fix gesture_arm/ scripts/ tests/Review the remaining unfixable warnings and address them manually.
black gesture_arm/ scripts/ tests/
git add -u
git commit -m "style: apply black formatting"The Dockerfile.sim uses portaudio19-dev from apt. If the apt cache is stale:
RUN apt-get update && apt-get install -y portaudio19-devEnsure apt-get update runs before the install in the Dockerfile.
The --no-hardware mode still tries to open a camera. In a headless Docker container, there is no camera device.
Fix: For CI, test with --help instead:
docker run --rm gesture-arm-sim python -m gesture_arm.run --no-hardware --helpFor a full headless demo, a video file mock would be needed instead of cv2.VideoCapture(0).