10 Essential pySerial Tips and Tricks for Reliable Device ControlReliable serial communication is crucial when working with microcontrollers, sensors, modems, or legacy devices. pySerial is the go‑to Python library for serial I/O — simple to start with but easy to misuse in production systems. This article covers 10 practical tips and tricks that will help you build robust, maintainable, and efficient serial applications using pySerial.
1. Choose the right serial settings and verify them
Serial communication requires matching parameters on both ends. The common settings you must verify and explicitly set are:
- baudrate — e.g., 9600, 115200
- bytesize — typically serial.EIGHTBITS
- parity — serial.PARITY_NONE, PARITY_EVEN, or PARITY_ODD
- stopbits — serial.STOPBITS_ONE or STOPBITS_TWO
- timeout — seconds for read operations; None, 0, or a positive float
Always set these in your Serial constructor:
import serial ser = serial.Serial( port='/dev/ttyUSB0', baudrate=115200, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=1 # 1 second read timeout )
If parameters don’t match the device, frames will be garbled or lost. When in doubt, consult the device datasheet.
2. Use timeouts and non-blocking reads to avoid hangs
Blocking reads without a timeout can freeze your program if the device stops responding. There are three useful modes:
- timeout=None — blocking read until requested bytes received
- timeout=0 — non-blocking read (returns immediately)
- timeout=float — block up to that many seconds
Prefer a small timeout (e.g., 0.1–1.0 s) and loop until you have the full message. Example pattern:
def read_line(ser): buffer = bytearray() while True: chunk = ser.read(1) # respects ser.timeout if not chunk: break # timeout reached buffer.extend(chunk) if chunk == b' ': break return bytes(buffer)
This prevents deadlocks and lets your program remain responsive.
3. Frame your protocol and handle partial reads
Serial is a stream: a single logical message may arrive in multiple read() calls. Design a framing protocol (delimiter, length header, or start/end markers). Common approaches:
- Line-based with newline delimiter (b’ ‘)
- Length-prefixed messages (first N bytes indicate payload length)
- Start/End markers with escape sequences
Example: read newline-terminated JSON messages robustly:
import json def read_json_line(ser): line = ser.readline() # convenience method using timeout if not line: return None return json.loads(line.decode('utf-8').strip())
If messages can contain newlines, use length-prefixing or escape strategies.
4. Normalize encoding and handle binary safely
Decide whether your protocol is text (UTF-8) or binary. For text:
- Always encode/decode explicitly with .encode(‘utf-8’) / .decode(‘utf-8’)
- Handle decoding errors: errors=‘replace’ or ‘ignore’ depending on needs
For binary data, treat payloads as bytes and avoid accidental decoding:
# send binary ser.write(bytes([0x02, 0xFF, 0x00])) # receive fixed-length binary block data = ser.read(16)
Mixing text and binary in the same stream requires careful framing.
5. Use read_until, readline, and inWaiting appropriately
pySerial provides helper methods:
- ser.readline() — reads up to a newline or timeout
- ser.read_until(expected=b’ ‘, size=None) — reads until marker or size
- ser.in_waiting (or inWaiting()) — returns number of bytes available to read
Example: read available data in a non-blocking loop:
from time import sleep while True: n = ser.in_waiting if n: data = ser.read(n) handle(data) sleep(0.01)
Avoid reading one byte at a time in high-speed scenarios — batching reduces syscall overhead.
6. Implement retries, checksums, and acknowledgements
For reliability across noisy links, add integrity checks:
- Use checksums (CRC16, CRC32) or hashes appended to each message
- Implement application-level acknowledgements (ACK/NACK) and retransmit on failure
- Include sequence numbers to detect out-of-order or duplicate frames
Simple checksum example (XOR byte):
def checksum(data: bytes) -> int: c = 0 for b in data: c ^= b return c & 0xFF # append to payload payload = b'HELLO' ser.write(payload + bytes([checksum(payload)]))
A robust protocol reduces silent data corruption and hidden bugs.
7. Use context managers and proper close/shutdown
Always close serial ports cleanly to release OS resources. Use context managers or try/finally:
with serial.Serial('/dev/ttyUSB0', 115200, timeout=1) as ser: ser.write(b'PING ') resp = ser.readline() # automatically closed here
If you can, also flush input/output when reconnecting:
ser.reset_input_buffer() ser.reset_output_buffer()
This avoids processing stale data after reconnects.
8. Make threaded or async designs safe
If multiple threads or async tasks access the same Serial instance, protect reads/writes:
- For threads: use a Lock around ser.read/ser.write
- For asyncio: use a dedicated reader/writer task or use libraries like serial_asyncio
Threaded example:
import threading lock = threading.Lock() def safe_write(ser, data): with lock: ser.write(data)
A single reader thread that puts complete messages onto a queue simplifies concurrency.
9. Log raw traffic during development
When debugging, log raw bytes with timestamps and direction (TX/RX). Hex dumps make problems apparent:
import binascii, time def log_tx(data): print(f"{time.time():.3f} TX: {binascii.hexlify(data)}") def log_rx(data): print(f"{time.time():.3f} RX: {binascii.hexlify(data)}")
Turn this off or reduce verbosity in production to avoid performance and privacy concerns.
10. Test with virtual loopback and tools
Before connecting hardware, or to reproduce bugs, use virtual serial ports and tools:
- socat (Linux/macOS) to create linked pseudo-TTY pairs
- com0com or com2tcp (Windows) for virtual COM ports
- Use serial terminal apps (gtkterm, minicom, PuTTY) to inspect traffic
Example socat command to create a pair on Linux:
- socat -d -d pty,raw,echo=0 pty,raw,echo=0
Then connect your program to one end and a terminal to the other to emulate device behavior.
Quick checklist
- Set and verify baud, parity, stop bits, timeout
- Use timeouts and avoid blocking reads
- Frame messages (delimiter, length, or markers)
- Treat text vs binary explicitly
- Batch reads using in_waiting, read_until, or readline
- Add checksums/ACKs and retries for reliability
- Close ports and flush buffers on reconnect
- Protect Serial access in multi-threaded/async code
- Log raw bytes when debugging
- Test with virtual serial ports before hardware
Reliable serial communication is more about good protocol design, defensive coding, and thorough testing than about specific library calls. pySerial gives you the primitives — combine them with clear message framing, error detection, timeouts, and careful concurrency to build systems that keep working even when links misbehave.
Leave a Reply