How to control the 5-finger Robot Hand from Arduino and Python

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 Servo library (and Adafruit_PWMServoDriver for 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]);
    }
}

:warning: 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 explicit NUM_SERVOS constant.

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]);
    }
}

:warning: Check: MIN_PULSE/MAX_PULSE and 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

:warning: 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

  • ConnectionError on 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_PULSE for your specific servos, they vary between models.

Related


Rewritten and consolidated (Sept 2026) from the original student how-to’s: How to control the “Robot Hand”.