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