A cross-platform Python framework for UART/serial port monitoring, command sending, and data receiving. Built on pyserial.
Includes a CLI tool for quick serial port diagnostics, plug/unplug monitoring (JSONL output), text/hex command communication, and TOML-based device profile management.
% pip install uart-helper
Verify your setup:
% uart-helper --check
% uart-helper
Found 2 port(s):
/dev/ttyUSB0 aaaa:0001 "USB Serial" serial=0001 [Temp Sensor] (role=sensor)
/dev/ttyS0
% uart-helper --json
{"status": true, "action": "scan", "meta": {...}, "data": [{"device": "/dev/ttyUSB0", "vid": "aaaa", "pid": "0001", ...}]}
% uart-helper --vid aaaa # single vendor ID
% uart-helper --vid aaaa --vid bbbb # multiple vendor IDs
% uart-helper --vid aaaa --pid 0001 # vendor + product ID
% uart-helper --name "USB*" # description glob pattern
% uart-helper --device "/dev/ttyUSB*" # device path glob pattern
Multiple --vid, --pid, --name, and --device values are cross-producted into match rules. For example, --vid aaaa --vid bbbb --pid 0001 creates two rules: aaaa:0001 and bbbb:0001.
% uart-helper --listen
Outputs JSONL (one JSON object per line) — designed for piping into AI agents or automation scripts:
{"status": true, "action": "init", "meta": {...}, "data": [{"device": "/dev/ttyUSB0", "vid": "aaaa", "pid": "0001", ...}]}
{"status": true, "action": "plug", "meta": {...}, "data": [{"device": "/dev/ttyUSB1", "vid": "bbbb", "pid": "0002", ...}]}
{"status": true, "action": "unplug", "meta": {...}, "data": [{"device": "/dev/ttyUSB1", "vid": "bbbb", "pid": "0002", ...}]}
{"status": true, "action": "stop", "meta": {...}, "data": []}Events: init (startup port list), plug, unplug, error, stop (Ctrl+C).
Combine with filters:
% uart-helper --listen --vid aaaa --interval 1000
% uart-helper --listen --profile sample
% uart-helper send --port /dev/ttyUSB0 --crlf "AT"
Sent 4 bytes to /dev/ttyUSB0
Received 4 bytes:
TEXT: OK
HEX: 4f 4b 0d 0a
% uart-helper send --port /dev/ttyUSB0 --hex "FF 01 02 03"
Sent 4 bytes to /dev/ttyUSB0
Received 2 bytes:
HEX: 06 00
Hex input supports multiple formats (case-insensitive):
% uart-helper send --port /dev/ttyUSB0 --hex "FF 01 02" # space-separated
% uart-helper send --port /dev/ttyUSB0 --hex "ff0102" # continuous (no spaces)
% uart-helper send --port /dev/ttyUSB0 --hex "Ff:01:aB" # colon-separated, mixed case
% uart-helper send --port /dev/ttyUSB0 --hex "0xFF 0x01 0x02" # 0x-prefixed
Specify UART parameters:
% uart-helper send --port /dev/ttyUSB0 --baudrate 9600 --crlf "AT"
% uart-helper send --port /dev/ttyUSB0 --baudrate 9600 --parity E --stopbits 2 --crlf "AT"
% uart-helper send --port /dev/ttyUSB0 --crlf "AT" --json
{"status": true, "action": "send", "meta": {...}, "data": {"port": "/dev/ttyUSB0", "sent_bytes": 4, "received_bytes": 4, "received_hex": "4f 4b 0d 0a", "received_text": "OK\r\n"}}
% uart-helper monitor --port /dev/ttyUSB0
Monitoring /dev/ttyUSB0 (baud=115200) — Ctrl+C to stop
──────────────────────────────────────────────────
Hello from device...
Screen-style CLI compatibility:
% uart-helper screen -U /dev/tty.usbserial-110 115200
Monitoring /dev/tty.usbserial-110 (baud=115200) — Ctrl+C to stop
──────────────────────────────────────────────────
In screen mode, stdin is key-by-key (no Enter needed) and shortcuts are supported:
Ctrl+A, H toggle HEX mirror output on/off
Ctrl+A, A send a literal Ctrl+A byte (0x01)
HEX mirror can include line number and datetime:
% uart-helper screen -U /dev/tty.usbserial-110 115200 --datetime-format "%Y-%m-%d_%H:%M:%S"
# Example HEX mirror line:
[HEX #000123 2026-03-26_22:45:10] 48 65 6c 6c 6f
Single-file debug logging:
% uart-helper screen -U /dev/tty.usbserial-110 115200 --log-file /tmp/uart-debug.log
# log line format:
# ts=2026-03-26T14:45:10.123456+00:00 bytes=5 hex=48 65 6c 6c 6f text=Hello\r\n
% uart-helper monitor --port /dev/ttyUSB0 --hex
[2026-03-26T12:00:00+00:00] 48 65 6c 6c 6f
% uart-helper monitor --port /dev/ttyUSB0 --output-hex
48 65 6c 6c 6f
% uart-helper monitor --port /dev/ttyUSB0 --stdin
Monitoring /dev/ttyUSB0 (baud=115200) — Ctrl+C to stop
──────────────────────────────────────────────────
# type into terminal and press Enter to send each line to UART
% uart-helper monitor --port /dev/ttyUSB0 --stdin --output-hex
48 65 6c 6c 6f
% uart-helper monitor --port /dev/ttyUSB0 --output-merge
48 65 6c 6c 6f
Hello
% uart-helper monitor --port /dev/ttyUSB0 --stdin --output-file /tmp/output.log --output-file-hex /tmp/output.hex
# stdout keeps printing, while text/hex are also appended to files
% uart-helper monitor --port /dev/ttyUSB0 --output-file-hex-mixed /tmp/output_mixed.log
# each chunk appends:
# line 1: hex
# line 2: text/binary
% uart-helper monitor --port /dev/ttyUSB0 --json
{"status": true, "action": "monitor_start", "meta": {...}, "data": {"port": "/dev/ttyUSB0", "baudrate": 115200}}
{"status": true, "action": "data", "meta": {...}, "data": {"timestamp": "...", "bytes": 5, "hex": "48 65 6c 6c 6f", "text": "Hello"}}
When pyserial is missing, all modes emit a structured error:
{"status": false, "action": "error", "meta": {...}, "error": -1, "errorMessage": "pyserial is not installed. Install with: pip install pyserial"}Profiles are TOML files that define named sets of port match rules and default UART settings. Instead of typing --vid, --pid, and --baudrate every time, save your device definitions once and reference them by name.
./uart-helper.d/— current working directory (project-level)~/.config/uart-helper/— user config (shared across projects)
% mkdir -p ~/.config/uart-helper
% cat > ~/.config/uart-helper/sample.toml << 'EOF'
description = "My UART devices"
[defaults]
baudrate = 115200
bytesize = 8
parity = "N"
stopbits = 1
timeout = 1.0
[[rules]]
vid = "aaaa"
pid = "0001"
label = "Temp Sensor"
[rules.metadata]
role = "sensor"
[[rules]]
vid = "bbbb"
pid = "0002"
label = "Motor Controller"
[rules.metadata]
role = "controller"
EOF
Each [[rules]] entry supports these optional fields:
| Field | Description | Example |
|---|---|---|
vid |
USB Vendor ID (hex string) | "aaaa" |
pid |
USB Product ID (hex string) | "0001" |
label |
Human-readable rule name | "Temp Sensor" |
name |
Port description glob pattern | "USB*" |
serial |
Serial number glob pattern | "SN-*" |
device |
Device path glob pattern | "/dev/ttyUSB*" |
metadata |
Arbitrary key-value pairs | {role = "sensor"} |
The [defaults] section sets UART parameters for the profile:
| Field | Description | Default |
|---|---|---|
baudrate |
Baud rate | 115200 |
bytesize |
Data bits (5, 6, 7, 8) | 8 |
parity |
Parity ("N", "E", "O", "M", "S") |
"N" |
stopbits |
Stop bits (1, 1.5, 2) | 1 |
timeout |
Read timeout in seconds | 1.0 |
% uart-helper --profile sample
% uart-helper --profile sample --listen
% uart-helper --profile sample --json
% uart-helper send --port /dev/ttyUSB0 --profile sample --crlf "AT"
Or specify a TOML file directly:
% uart-helper --config ./my-devices.toml
Profile rules and CLI flags (--vid, --pid, --name, --device) are merged — you can add extra filters on top of a profile.
% uart-helper profiles
Config directories (search order):
./uart-helper.d
/Users/you/.config/uart-helper
Available profiles (1):
sample
My UART devices
2 rule(s), baud=115200 — /Users/you/.config/uart-helper/sample.toml
% uart-helper profiles --json
{"status": true, "action": "profiles", "meta": {...}, "data": {"config_dirs": [...], "profiles": [{"name": "sample", ...}]}}
Place TOML files in uart-helper.d/ in your project root. These take priority over user-level profiles with the same name:
my-project/
uart-helper.d/
my-devices.toml ← project-specific config
src/
main.py
from uart_helper import SerialMonitor, PortMatchRule
rules = [
PortMatchRule(vid=0xAAAA, pid=0x0001, label="Temp Sensor"),
PortMatchRule(vid=0xBBBB, pid=0x0002, label="Motor Controller"),
]
monitor = SerialMonitor(match_rules=rules, poll_interval_ms=500)
# One-time scan
for identity, rule in monitor.scan_once():
print(f"Found: {identity} (rule: {rule.label})")
# Continuous monitoring
monitor.on_port_event = lambda event: print(f"[{event.event_type.value}] {event.port}")
monitor.run_forever() # Ctrl+C to stopfrom uart_helper import UARTDevice, PortIdentity, UARTConfig
identity = PortIdentity(device="/dev/ttyUSB0")
config = UARTConfig(baudrate=115200)
with UARTDevice(identity, config) as dev:
# Send text command (e.g., AT command)
result = dev.send_command("AT\r\n")
print(result.text)
# Send hex bytes
result = dev.send_hex("FF 01 02 03")
if result.ok:
print(f"Received {len(result.data)} bytes: {result.data.hex()}")
# Low-level write + read
dev.write(b"\x01\x02\x03")
result = dev.read(1024, timeout_ms=2000)
print(result.data)
# Read until terminator
result = dev.read_until(terminator=b"\r\n")
print(result.text)from uart_helper import load_profile, load_profile_by_name, list_profiles
# By name (searches config dirs)
profile = load_profile_by_name("sample")
print(f"Default baud: {profile.defaults.baudrate}")
# By path
profile = load_profile("./my-devices.toml")
# List all
for p in list_profiles():
print(f"{p.name}: {p.description} ({p.rule_count} rules)")
# Use profile rules with SerialMonitor
monitor = SerialMonitor(match_rules=profile.rules)from uart_helper import get_meta
meta = get_meta()
# {"uart_helper": "1.0.0", "python": "3.12.3", "platform": "...", "os": "Darwin", "arch": "arm64", "pyserial": "3.5"}Tests are fully mock-based — no real serial hardware needed.
% git clone https://github.com/changyy/py-uart-helper.git
% cd py-uart-helper
% pip install -e ".[dev]"
% python -m pytest tests/ -v
To run with coverage:
% python -m pytest tests/ --cov=uart_helper --cov-report=term-missing
py-uart-helper/
src/uart_helper/
__init__.py Package exports
types.py PortIdentity, PortMatchRule, UARTConfig, TransferResult, PortEvent
device.py SerialDevice abstract base class
uart_device.py UARTDevice — pyserial wrapper with text/hex send/receive
monitor.py SerialMonitor — polling-based attach/detach detection
config.py TOML profile loader with UART defaults
cli.py CLI entry point (uart-helper command)
tests/
examples/
sample.toml Sample device profile
pyproject.toml
README.md
- Python 3.10+
- pyserial >= 3.5
- py-usb-helper — USB device monitoring and bulk/SCSI communication
MIT © Yuan-Yi Chang