How to control an Arduino from Python over a serial connection

Based on contributions by ViktorWenemoser1.

This shows how to send text commands from a Python script on your PC to an Arduino over USB serial, so a computer can trigger actions like moving a servo or lighting LEDs. It’s a common foundation for automation projects where the Arduino handles real-time hardware and Python handles logic, a GUI, or a vision pipeline.

What you need

  • An Arduino board and a USB cable
  • Arduino IDE
  • Python 3.x
  • The pyserial library: pip install pyserial
  • LEDs, a servo, or other peripherals to control (depending on your project)

Steps

1. Design a simple command protocol

Pick a simple text format both sides agree on. This example uses "{command} {param}\n", e.g. servo_on \n or set_all_leds Red\n. The Arduino replies with done\n when it has finished executing a command, so Python knows when it’s safe to send the next one.

2. Write the Arduino sketch

On the Arduino side, implement the functions your commands map to, and a loop() that reads incoming serial text, matches it against your command list, calls the matching function, and finally prints done. The example below shows the LED-control part of such a sketch (using the FastLED library, addressable LED strips on pins 9/10/11):

#include "Led_controller.h"

CRGB leds[NUM_STRIPS][MAX_NUM_LEDS];  // Array to hold LED data for each strip

int dataPins[NUM_STRIPS] = {9, 10, 11};  // Adjust the data pins according to your setup
int numLeds[NUM_STRIPS] = {30, 30, 30};  // Example: 30 LEDs on each strip

/* Initialization (default settings) */
void initialize_leds() {
    for (int i = 0; i < NUM_STRIPS; i++) {
        // Initialize each LED strip separately
        if (i == 0) {
            FastLED.addLeds<WS2812B, DATA_PIN_1, RGB>(leds[i], numLeds[i]);
        } else if (i == 1) {
            FastLED.addLeds<WS2812B, DATA_PIN_2, RGB>(leds[i], numLeds[i]);
        } else if (i == 2) {
            FastLED.addLeds<WS2812B, DATA_PIN_3, RGB>(leds[i], numLeds[i]);
        }
    }

    set_all_leds(CRGB::Black);  // Initialize LEDs as off
    FastLED.setBrightness(50);  // Set initial brightness
    FastLED.show();
    delay(500);
}

/* All LED control */
void set_all_leds(CRGB color) {
    for (int i = 0; i < NUM_STRIPS; i++) {
        for (int j = 0; j < numLeds[i]; j++) {
            leds[i][j] = color;
        }
    }
    FastLED.show();
}

/* LED group control */
void set_led_range(int stripIndex, int startLed, int endLed, CRGB color) {
    if (stripIndex < 0 || stripIndex >= NUM_STRIPS) {
        Serial.println("Invalid strip index!");
        return;
    }
    if (startLed < 0 || endLed >= numLeds[stripIndex] || startLed > endLed) {
        Serial.println("Invalid LED range!");
        return;
    }

    for (int i = startLed; i <= endLed; i++) {
        leds[stripIndex][i] = color;
    }
    FastLED.show();
}

/* Load bar control */
void load_bar_range(CRGB color, unsigned long duration, int stripIndex, int startIndex, int endIndex) {
    if (stripIndex < 0 || stripIndex >= NUM_STRIPS) {
        Serial.println("Invalid strip index!");
        return;
    }

    int totalLeds = endIndex - startIndex + 1;
    unsigned long interval = duration / totalLeds;

    for (int i = startIndex; i <= endIndex && i < numLeds[stripIndex]; i++) {
        leds[stripIndex][i] = color;
        FastLED.show();
        delay(interval);
    }

    for (int i = startIndex; i <= endIndex && i < numLeds[stripIndex]; i++) {
        leds[stripIndex][i] = CRGB::Black;
    }
    FastLED.show();
}

:warning: Check: this is the LED-control part of the original sketch only. It does not include the void loop() command dispatcher that reads incoming serial text (e.g. initialize_servo, servo_on, set_all_leds ...) and calls the matching function — that part was not included in the source. You need to write your own loop() that reads a line with Serial.readStringUntil('\n'), splits it into a command word and parameters, calls the matching function (also add your own servo-control functions if you use a servo), and ends by printing Serial.println("done").

3. Install pyserial and find your COM port

pip install pyserial

On Windows, find the COM port in Device Manager. On macOS/Linux, list ports with ls /dev/tty.*.

4. Write the Python script

import serial
import time

# Connect to the Arduino (adjust the port)
arduino = serial.Serial(port='COM4', baudrate=9600, timeout=.1)  # e.g. 'COM4' on Windows, '/dev/ttyUSB0' on Linux
time.sleep(2)  # Wait for the Arduino to reset after opening the port
arduino.reset_input_buffer()  # Clear the serial buffer


def send_command(command, param=""):
    full_command = f"{command} {param}\n"
    arduino.write(full_command.encode())
    print(f"Sent: {full_command.strip()}")
    time.sleep(0.1)  # Give the Arduino time to start responding

    while True:
        response = arduino.readline().decode().strip()
        if response == "done":
            print("Arduino: done")
            break
        elif response:
            print(f"Arduino: {response}")


def execute_sequence():
    """Execute a sequence of commands."""
    commands = [
        ("initialize_servo", ""),   # Initialize the servo
        ("servo_on", ""),           # Move servo to 90 degrees
        ("initialize_leds", ""),    # Initialize LEDs
        ("set_all_leds", "Red"),    # Set all LEDs to red
        ("set_strip_leds", "0 Blue"),         # Set LEDs of strip 0 to blue
        ("set_led_range", "0 0 9 Red"),       # Set LEDs 0-9 on strip 0 to red
        ("load_bar_range", "Yellow 5000 0 10 20"),  # 5-second yellow load bar on LEDs 10-20 of strip 0
        ("servo_off", ""),          # Move the servo back to 0 degrees
    ]
    for command, param in commands:
        send_command(command, param)


def execute_sequence2():
    """Execute a second sequence of commands."""
    commands = [
        ("servo_on", ""),
        ("servo_off", ""),
        ("initialize_leds", ""),
        ("load_bar_range", "Green 5000 0 0 30"),  # 5-second green load bar on LEDs 0-30 of strip 0
        ("set_all_leds", "Black"),
    ]
    for command, param in commands:
        send_command(command, param)


if __name__ == "__main__":
    print("Starting sequence 1...")
    execute_sequence()
    time.sleep(5)
    print("Starting sequence 2...")
    execute_sequence2()
    print("Program finished.")

Supported commands used above: initialize_servo, servo_on, servo_off, initialize_leds, set_all_leds {color}, set_strip_leds {stripIndex} {color}, set_led_range {stripIndex} {startIndex} {endIndex} {color}, load_bar_range {color} {duration} {stripIndex} {startIndex} {endIndex}.

5. Run it

Save the script (e.g. arduino_communication.py) and run:

python arduino_communication.py

Troubleshooting

  • Nothing happens / script hangs: the COM port or baud rate on the Python side doesn’t match the Arduino sketch. Both must use the same value (9600 in this example).
  • Script blocks forever waiting for done: make sure every branch of your Arduino loop() dispatcher ends by printing done, otherwise Python’s send_command() will wait indefinitely.
  • Use the Arduino IDE’s Serial Monitor to see exactly what bytes are being sent and received while debugging.
  • Adjust the time.sleep() delays in the Python script to match how long your hardware actually needs to move/react.

Related


Rewritten and consolidated (Sept 2026) from the original student how-to’s: How to Establish Communication Between Python and Arduino.