PySerial
PySerialDocs

Debugging

Debug PySerial issues with loopback tests, hex dumps, serial monitoring, and timing analysis. Practical techniques, no frameworks.

When serial communication fails, start simple: verify the wire, then the bytes, then the protocol.

Loopback Test

Connect TX to RX on your adapter (pin 2 to pin 3 on DB-9). If this fails, the problem is hardware or drivers, not your code.

import serial

def loopback_test(port, baudrate=9600):
    """Send data and verify it comes back. Requires TX wired to RX."""
    ser = serial.Serial(port, baudrate, timeout=1)
    test_data = b'LOOPBACK_TEST_1234'

    ser.reset_input_buffer()
    ser.write(test_data)
    received = ser.read(len(test_data))
    ser.close()

    if received == test_data:
        print(f"PASS: loopback on {port} at {baudrate} baud")
        return True
    else:
        print(f"FAIL: sent {test_data}, got {received}")
        return False

loopback_test('/dev/ttyUSB0')

Log serial test results automatically

TofuPilot records test results from your PySerial scripts, tracks pass/fail rates, and generates compliance reports. Free to start.

Hex Dump

When you can't tell what's on the wire, print every byte.

def hex_dump(data, width=16):
    """Print data in hex + ASCII format, like xxd."""
    for offset in range(0, len(data), width):
        chunk = data[offset:offset + width]
        hex_part = ' '.join(f'{b:02X}' for b in chunk)
        ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
        print(f"{offset:04X}  {hex_part:<{width * 3}}  {ascii_part}")

# Example
data = b'Hello\r\nWorld\x00\xFF'
hex_dump(data)
# 0000  48 65 6C 6C 6F 0D 0A 57 6F 72 6C 64 00 FF        Hello..World..

Serial Monitor

A minimal monitor that timestamps every line. Useful for watching device output in real time.

import serial
import time

def monitor(port, baudrate=9600):
    """Print timestamped lines from a serial port."""
    ser = serial.Serial(port, baudrate, timeout=0.1)
    print(f"Monitoring {port} at {baudrate} baud (Ctrl+C to stop)")

    try:
        while True:
            line = ser.readline()
            if line:
                ts = time.strftime('%H:%M:%S')
                text = line.decode('utf-8', errors='replace').strip()
                print(f"[{ts}] {text}")
    except KeyboardInterrupt:
        pass
    finally:
        ser.close()

monitor('/dev/ttyUSB0')

Raw Byte Monitor

For binary protocols, show individual bytes as they arrive.

import serial
import time

def byte_monitor(port, baudrate=9600, duration=10):
    """Show raw bytes as hex for a fixed duration."""
    ser = serial.Serial(port, baudrate, timeout=0.1)
    end = time.time() + duration
    total = 0

    print(f"Capturing bytes on {port} for {duration}s")
    try:
        while time.time() < end:
            if ser.in_waiting:
                data = ser.read(ser.in_waiting)
                total += len(data)
                ts = time.strftime('%H:%M:%S')
                print(f"[{ts}] ({len(data):3d}B) {data.hex(' ')}")
            else:
                time.sleep(0.01)
    except KeyboardInterrupt:
        pass
    finally:
        ser.close()
    print(f"Total: {total} bytes")

byte_monitor('/dev/ttyUSB0')

Timing Analysis

Measure response times to catch slow or dropped replies.

import serial
import time

def measure_response_time(ser, command, expected, iterations=10):
    """Send a command repeatedly and measure response latency."""
    times = []
    for i in range(iterations):
        ser.reset_input_buffer()
        start = time.perf_counter()
        ser.write(command)
        ser.flush()
        response = ser.read_until(expected)
        elapsed = (time.perf_counter() - start) * 1000  # ms
        times.append(elapsed)
        time.sleep(0.05)

    avg = sum(times) / len(times)
    print(f"Latency over {iterations} rounds:")
    print(f"  Min: {min(times):.1f} ms")
    print(f"  Max: {max(times):.1f} ms")
    print(f"  Avg: {avg:.1f} ms")
    return times

ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=2)
measure_response_time(ser, b'AT\r\n', b'OK')
ser.close()

Baud Rate Detection

If you don't know the device's baud rate, try common values and look for readable output.

import serial

def detect_baud_rate(port, candidates=None):
    """Try common baud rates and return the one that produces readable text."""
    if candidates is None:
        candidates = [9600, 115200, 19200, 38400, 57600, 4800, 2400, 1200]

    for baud in candidates:
        try:
            ser = serial.Serial(port, baud, timeout=1)
            ser.reset_input_buffer()
            data = ser.read(64)
            ser.close()

            if not data:
                continue

            printable = sum(1 for b in data if 32 <= b < 127 or b in (10, 13))
            ratio = printable / len(data)

            if ratio > 0.8:
                print(f"Likely baud rate: {baud} ({ratio:.0%} printable)")
                return baud
            else:
                print(f"  {baud}: {ratio:.0%} printable (unlikely)")
        except serial.SerialException:
            continue
    return None

detect_baud_rate('/dev/ttyUSB0')

Port Information

List connected serial devices with vendor/product IDs for identification.

import serial.tools.list_ports

def list_serial_ports():
    """Print details for every detected serial port."""
    ports = list(serial.tools.list_ports.comports())
    if not ports:
        print("No serial ports found")
        return

    for p in ports:
        print(f"{p.device}: {p.description}")
        if p.vid is not None:
            print(f"  VID:PID = {p.vid:04X}:{p.pid:04X}")
        if p.serial_number:
            print(f"  Serial: {p.serial_number}")

list_serial_ports()

Command-Response Debugger

Wrap your send/receive in a function that logs both directions. Useful for interactive protocols like AT commands.

import serial
import time

def debug_command(ser, command, timeout=2):
    """Send a command and print both request and response with timing."""
    ser.reset_input_buffer()
    start = time.perf_counter()
    ser.write(command.encode() + b'\r\n')
    ser.flush()

    response = b''
    deadline = time.time() + timeout
    while time.time() < deadline:
        if ser.in_waiting:
            response += ser.read(ser.in_waiting)
        time.sleep(0.01)

    elapsed = (time.perf_counter() - start) * 1000
    print(f"TX: {command}")
    print(f"RX: {response.decode('utf-8', errors='replace').strip()}")
    print(f"    ({elapsed:.1f} ms, {len(response)} bytes)")
    return response

ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=2)
debug_command(ser, 'AT')
debug_command(ser, 'AT+GMR')
ser.close()

Debugging Checklist

Work through these when serial communication misbehaves:

  1. Physical layer: cable connected, correct port, device powered on
  2. Baud rate: must match on both ends exactly
  3. Frame format: data bits, parity, and stop bits must agree (8N1 is the most common)
  4. Timeout: set timeout=1 or higher to avoid returning empty reads prematurely
  5. Buffer state: call reset_input_buffer() before sending a command you expect a reply to
  6. Line endings: some devices want \r\n, others just \n or \r
  7. Encoding: decode with errors='replace' to see garbled bytes instead of crashing
  8. Permissions: on Linux, your user needs to be in the dialout group

On this page

Deploy your PySerial test scripts with TofuPilot

Trusted by Lyft, Cobot, Pacific Defense, and more.

TofuPilot provides dashboards and analytics to track serial device testing at scale.

TofuPilot
Get Started