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