Based on contributions by xtrashrr.
The robot lab’s “Robot Hand” (used for the promobot) has one servo per finger, driven by an Arduino UNO. This guide covers two ways to control it: a simple direct-servo version for a handful of fixed poses, and a servo-driver version for finer per-finger control from Python.
What you need
- Arduino UNO
- The Robot Hand (5 servos, one per finger: thumb, index, middle, ring, little)
- For finer control: a PCA9685 16-channel I2C PWM/servo driver (datasheet)
- Arduino IDE with the built-in
Servolibrary (andAdafruit_PWMServoDriverfor the PCA9685 option) - Python 3.x with
pyserial(pip install pyserial) if you want to control the hand from a PC
Steps
1. Wire the servos
Each servo has 3 wires: red (5V), brown (GND), and orange (signal/PWM). Connect all the 5V wires together and all the GND wires together, then connect each servo’s signal wire to its own digital output pin on the Arduino.
2. Option A — direct control with the Servo library
This is the simplest setup: the Arduino reads a single character over serial (0-3) and moves all five servos to a matching preset pose.
// example code used in a rock-paper-scissors game
#include <Servo.h>
const int NUM_SERVOS = 5;
Servo servos[NUM_SERVOS];
int servo_pins[NUM_SERVOS] = {3, 5, 6, 9, 10};
// 0 Thumb
// 1 Index finger
// 2 Middle finger
// 3 Ring finger
// 4 Little finger
char command = '0';
void setup() {
Serial.begin(9600);
// Initial position for the fingers (clenched fist)
int servo_angles[NUM_SERVOS] = {180, 0, 0, 0, 0};
for (int i = 0; i < NUM_SERVOS; i++) {
servos[i].attach(servo_pins[i]);
servos[i].write(servo_angles[i]);
}
}
void loop() {
if (Serial.available() > 0) {
command = Serial.read();
}
// Change the hand pose by sending a single character over serial
switch (command) {
case '0': // Stone (fist)
set_servos(0, 0, 0, 0, 0);
break;
case '1': // Paper (open hand)
set_servos(180, 180, 180, 180, 180);
break;
case '2': // Scissors
set_servos(0, 180, 180, 0, 0);
break;
case '3': // Thumbs up
set_servos(180, 0, 0, 0, 0);
break;
}
delay(100);
}
void set_servos(int angle_s0, int angle_s1, int angle_s2, int angle_s3, int angle_s4) {
int servo_angles[NUM_SERVOS] = {180 - angle_s0, angle_s1, angle_s2, angle_s3, angle_s4};
for (int i = 0; i < NUM_SERVOS; i++) {
servos[i].write(servo_angles[i]);
}
}
The original source used
sizeof(servos)as the loop bound, which returns a byte count, not the number of servos — that would run the loop too many times and index past the end of the arrays. The code above fixes this with an explicitNUM_SERVOSconstant.
Sending values 0–3 over a serial connection (e.g. from the Arduino IDE’s Serial Monitor, or from Python) switches between “stone”, “paper”, “scissors”, and “thumbs up”.
3. Option B — full per-finger control with a PCA9685 servo driver
Direct control only gives you a handful of preset poses. For per-finger control from software, use a PCA9685 I2C PWM driver, which also frees up Arduino pins if you need more than a few PWM channels.
The Arduino reads 5 digits (one per finger, 0-9) terminated by a newline, and maps each digit to an angle (in steps of 20°, so 0-9 covers 0°-180°):
#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();
// PWM channels for each servo
// 0 Thumb
// 1 Index finger
// 2 Middle finger
// 3 Ring finger
// 4 Little finger
void setup() {
Serial.begin(9600);
pwm.begin();
pwm.setPWMFreq(50); // 50 Hz is standard for analog servos
set_servos(0, 0, 0, 0, 0);
}
void loop() {
char last_received = '\0';
String receive_buffer = "";
while (last_received != '\n') {
if (Serial.available()) {
last_received = Serial.read();
if (last_received != '\n') {
receive_buffer += last_received;
}
}
}
if (receive_buffer != "") {
if (receive_buffer.length() == 5) {
bool legal_input = true; // checks the 5-character input is valid before converting it
for (uint8_t i = 0; i < 5; i++) {
if (receive_buffer[i] < '0' || receive_buffer[i] > '9') {
legal_input = false;
}
}
if (legal_input) {
set_servos(
20 * (receive_buffer[0] - '0'),
20 * (receive_buffer[1] - '0'),
20 * (receive_buffer[2] - '0'),
20 * (receive_buffer[3] - '0'),
20 * (receive_buffer[4] - '0')
);
}
} else {
Serial.println(receive_buffer);
}
}
}
// Set servos to the given angles (0-180)
void set_servos(uint8_t angle_s0, uint8_t angle_s1, uint8_t angle_s2, uint8_t angle_s3, uint8_t angle_s4) {
const uint16_t MIN_PULSE = 50; // Minimum pulse length — calibrate for your servos
const uint16_t MAX_PULSE = 200; // Maximum pulse length — calibrate for your servos
uint16_t servo_pulses[] = {
map(180 - angle_s0, 0, 180, MIN_PULSE, MAX_PULSE), // Thumb (reversed)
map(angle_s1, 0, 180, MIN_PULSE, MAX_PULSE), // Index
map(angle_s2, 0, 180, MIN_PULSE, MAX_PULSE), // Middle
map(angle_s3, 0, 180, MIN_PULSE, MAX_PULSE), // Ring
map(angle_s4, 0, 180, MIN_PULSE, MAX_PULSE) // Little
};
for (uint8_t i = 0; i < 5; i++) {
pwm.setPWM(i, 4095 - servo_pulses[i], servo_pulses[i]);
}
}
Check:
MIN_PULSE/MAX_PULSEand the 50Hz frequency are the values used in the original project — recalibrate them for your own servos before relying on them.
4. Control it from Python
With the PCA9685 sketch uploaded, control the hand from a PC by sending 5 digits (0-9 per finger) followed by a newline:
# arduino_class.py
import os
import sys
import time
import serial
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class RobotHand(metaclass=Singleton):
def __init__(self, port: str = "COM3", test_mode: bool = False) -> None:
if not isinstance(port, str):
raise TypeError("port must be a string")
if not isinstance(test_mode, bool):
raise TypeError("test_mode must be a bool")
self.test_mode = test_mode
if not self.test_mode:
try:
self.serial = serial.Serial()
self.serial.port = port
self.serial.baudrate = 9600
self.serial.bytesize = 8
self.serial.parity = "N"
self.serial.stopbits = 1
self.serial.timeout = None
self.serial.open()
except Exception:
if sys.platform == "win32":
print("Wrong port on Windows - opening Device Manager for you.")
os.system('devmgmt.msc')
raise ConnectionError(f"Could not open the connection on serial port {port}")
time.sleep(0.1)
self.__send("00000")
def fingers(self, thumb: int, index: int, middle: int, ring: int, pinkie: int):
for name, value in [("thumb", thumb), ("index", index), ("middle", middle),
("ring", ring), ("pinkie", pinkie)]:
if not isinstance(value, int):
raise TypeError(f"{name} must be an int")
if value < 0 or value > 9:
raise ValueError(f"{name} value must be 0-9")
self.__send(f"{thumb}{index}{middle}{ring}{pinkie}")
def __send(self, data: str):
data = data + "\n"
try:
self.serial.write(data.encode('utf-8'))
except Exception:
raise ConnectionError("Serial device disconnected")
time.sleep(0.1)
received = self.serial.read_all().decode()
if received != "":
print(received)
return received
Save this as arduino_class.py, then use it from your own script:
from arduino_class import RobotHand
hand = RobotHand("COM9") # change the port to match your setup
hand.fingers(9, 9, 0, 0, 9) # example pose — adjust as needed
Check: the original source had broken indentation, missing imports, and used
//(not valid in Python) for comments. The version above fixes these while keeping the same design (a singleton wrapper around the serial port with input validation per finger).
Troubleshooting
ConnectionErroron startup: the COM port is wrong or already in use by another program (e.g. the Arduino IDE Serial Monitor). Close other programs using the port and double-check the port name.- Servos jitter or move to the wrong angle: recalibrate
MIN_PULSE/MAX_PULSEfor your specific servos, they vary between models.
Related
- How to control an Arduino from Python over a serial connection — controlling an Arduino from Python over serial
Rewritten and consolidated (Sept 2026) from the original student how-to’s: How to control the “Robot Hand”.

