Metadata-Version: 2.4
Name: uart-helper
Version: 1.0.1
Summary: A Python UART/serial device monitoring and communication framework built on pyserial
Author-email: Yuan-Yi Chang <changyy.csie@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/changyy/py-uart-helper
Project-URL: Repository, https://github.com/changyy/py-uart-helper
Project-URL: Issues, https://github.com/changyy/py-uart-helper/issues
Keywords: uart,serial,pyserial,device,monitor,rs232,tty,com-port
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Hardware
Classifier: Topic :: Communications
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyserial>=3.5
Requires-Dist: tomli>=1.0; python_version < "3.11"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Dynamic: license-file

# uart-helper

[![PyPI](https://img.shields.io/pypi/v/uart-helper.svg)](https://pypi.org/project/uart-helper/)
[![PyPI Downloads](https://static.pepy.tech/badge/uart-helper)](https://pepy.tech/projects/uart-helper)

A cross-platform Python framework for UART/serial port monitoring, command sending, and data receiving. Built on [pyserial](https://github.com/pyserial/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.

## Installation

```
% pip install uart-helper
```

Verify your setup:

```
% uart-helper --check
```

---

## CLI Usage

### List connected serial ports

```
% 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", ...}]}
```

### Filter devices

```
% 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`.

### Monitor plug/unplug events

```
% uart-helper --listen
```

Outputs JSONL (one JSON object per line) — designed for piping into AI agents or automation scripts:

```json
{"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
```

### Send commands

```
% 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"}}
```

### Interactive serial monitor

```
% 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"}}
```

### Error output

When pyserial is missing, all modes emit a structured error:

```json
{"status": false, "action": "error", "meta": {...}, "error": -1, "errorMessage": "pyserial is not installed. Install with: pip install pyserial"}
```

---

## Device Profiles

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.

### Profile search directories (first match wins)

1. `./uart-helper.d/` — current working directory (project-level)
2. `~/.config/uart-helper/` — user config (shared across projects)

### Create a profile

```
% 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` |

### Use a profile

```
% 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.

### List available profiles

```
% 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", ...}]}}
```

### Project-level profiles

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
```

---

## Python API

### Port monitoring

```python
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 stop
```

### UART communication

```python
from 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)
```

### Load profiles programmatically

```python
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)
```

### Runtime metadata

```python
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"}
```

---

## Running Tests

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
```

---

## Project Structure

```
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
```

---

## Requirements

- Python 3.10+
- pyserial >= 3.5

---

## Related

- [py-usb-helper](https://github.com/changyy/py-usb-helper) — USB device monitoring and bulk/SCSI communication

---

## License

MIT © [Yuan-Yi Chang](https://github.com/changyy)
