A minicom-style, cross-platform serial terminal TUI with robust YMODEM file transfer.
Built for embedded firmware flashing (STM32 & other ymodem bootloaders) and everyday serial debugging.
Main terminal — device ANSI/VT output with scrollback and live TX/RX counters
HEX mode — hex bytes on the left, printable ASCII (grey dots for the rest) in the right-hand pane
Ctrl+A menu popup and the options screen
- Full-screen terminal UI — device ANSI/VT output is rendered correctly, with scrollable history.
- Light / dark themes — the One Half palette; one theme ships both variants, switchable in Options (“Auto” follows the terminal background via OSC 11). Extra themes are plain JSON files.
- Ctrl+A prefix-key + overlay menu — familiar minicom interaction model.
- YMODEM send / receive — CRC-16-CCITT, configurable 128/1024-byte blocks, timeout retransmission, progress display, cancellable.
- ZMODEM transfer — lrzsz-compatible engine (pick it via
Ctrl+AS/R). - Live port / baudrate / parity editing with persistent configuration.
- Session capture (logging), local echo, line-ending conversion, HEX display, and more.
- Overlays (menu / connection / options / confirm dialogs) auto-adapt to small windows (compact full-screen layout).
- When the window is too small to be usable (< 20 columns or < 5 rows), prints a hint and exits cleanly.
--bareheadless serial pass-through: stdin → serial port, serial RX → stdout, for driving by external processes such as AI agents.- Windows (Windows Terminal recommended) and Linux.
python -m venv .venv
# Windows
.venv\Scripts\activate
# Linux
source .venv/bin/activate
pip install -e ".[dev]"Or install from PyPI:
pip install pycompycom # start, then press Ctrl+A Z for the menu / auto connection dialog
pycom --port COM3 --baud 115200
pycom /dev/ttyUSB0 -b 921600
# send a string right after connecting (supports \n \r \t \xHH escapes)
pycom -p COM3 -s "AT\r"
# send a script file line by line (# starts a comment line)
pycom -p COM3 -f boot.txt
# exit automatically after 5 seconds without receiving any byte (-e supports fractional seconds)
pycom -p COM3 -s "AT\r" -e 5
# start with 16-hex receive/send mode (HEX) enabled
pycom -p COM3 --hex
# disable mouse capture (wheel/click is left to the host terminal)
pycom -p COM3 --no-mouse
# compatibility mode for a bare Linux virtual console (init3) / extreme terminals:
# force English, 16 colors, no animations, no mouse
pycom -p /dev/ttyUSB0 --compat
# force the UI language for this run (zh | en); PYCOM_LANG=... also works
pycom --lang en
# headless pure serial pass-through (--bare): hides all UI, requires a port.
# stdin bytes → serial, serial RX → stdout; hand the terminal to an AI agent, etc.:
pycom --bare -p COM3 -b 115200Data bits / stop bits / flow control use the full names
--data-bits/--stop-bits/--flow.-s/-fmean "send string/script after connecting" and require-p/--port.
--bareis a UI-less pure pass-through mode: it only uses the connection parameters (-p/-b/--parity/...) and cannot be combined with-s/-f/-e/--hex.Extreme environments (Linux
init3/ bare virtual console). A Linux virtual console (TERM=linux) only supports 8/16 colors, its font has no CJK glyphs, and it has no mouse reporting — the rich UI then renders garbled. PyCom detects this and automatically enables compatibility mode: English UI, a dedicated high-contrast ANSI theme using only the base 8 colours (the Linux console ignores bright backgrounds, which would otherwise leave fields background-less), animations off and mouse capture off. Force it manually with--compat, or opt out of the automatic detection with--no-compat.--lang en(or thePYCOM_LANG=enenvironment variable) forces the language for a single run without touching the saved preference. You can also simplify rendering yourself via Textual's environment variables:TEXTUAL_COLOR_SYSTEM=standard,TEXTUAL_ANIMATIONS=noneand/orNO_COLOR=1(the latter also strips the device's ANSI colors). In compatibility mode the checkbox/radio markers fall back to ASCII[ ]/[x]and the dropdown/collapsible arrows tov/^/>, since console fonts often lack the glyphs (○/●/▼/▲/▶).
| Keys | Action |
|---|---|
Ctrl+A Z |
Open the main menu (floating popup) |
Ctrl+A P |
Serial parameters (connection) |
Ctrl+A S |
Send file (choose YMODEM/ZMODEM first) |
Ctrl+A R |
Receive file (choose YMODEM/ZMODEM first) |
Ctrl+A L |
Toggle session capture |
Ctrl+A C |
Clear screen |
Ctrl+A H |
Toggle 16-hex receive/send (HEX) |
Ctrl+A O |
Options |
Ctrl+A Y |
Language |
Ctrl+A A |
About |
Ctrl+A X |
Quit |
Esc (in prefix) |
Cancel prefix |
Local echo, auto-wrap, etc. live in the
Ctrl+AOoptions overlay (off by default) rather than occupying prefix shortcuts.HEX mode (
Ctrl+A H, persistent): received bytes are shown as hex text on the left, with a right-hand ASCII pane — printable ASCII as characters, everything else as grey dots. A multi-line hex input area appears at the bottom (only valid characters, auto space-separated per byte, wrapping at 4/8/16/32 bytes per line depending on window width). Keys no longer send directly — pressEnterin the input area (or click the bottom "Send" button) to parse the input as bytes. Toggling via shortcut auto-focuses the input area.Virtual loopback device (debugging): start with
--enable-debug, thenLOOPBACKappears at the end of the port list inCtrl+A P. No real port needed — every byte sent is echoed back (pure loopback), ideal for testing TX/RX and HEX display without hardware.Mouse capture is on by default (needed for scroll-back and in-app selection); start with
--no-mouseto hand wheel/click back to the host terminal.
python tools/check.py # same entry point as CI (see below)It runs ruff check / ruff format --check / mypy / pytest, and then the
packaging checks that mirror the CI build job: the version in pyproject.toml
must match pycom.__version__, and the wheel must build and ship every runtime
resource (app.tcss, the bundled themes) — a wheel that is missing one of them
installs cleanly but the app cannot start. Pass --skip-build to skip that last
step while iterating.
pip install pyinstaller
pyinstaller packaging/pycom.spec # produces the onedir layout dist/pycom/ (size-optimized)Size optimizations (already in packaging/pycom.spec):
- onedir layout: avoids onefile's per-launch self-extraction overhead, easier to inspect/remove unused runtime files on embedded devices
- exclude ssl/network modules: a serial terminal doesn't need SSL — saves ~6MB (libcrypto/libssl)
- exclude unused Textual widgets and stdlib extension modules (
_decimal/_lzma/_bz2/_zstd, etc.) - strip + UPX: effective on Linux and Python 3.12; Windows + Python 3.14 auto-skips UPX due to CFG-enabled binaries
CI (.github/workflows/ci.yml) runs lint/type/tests and builds artifacts on Windows and
Ubuntu (CI installs UPX automatically). Pushing a v* tag (e.g. v0.1.0) automatically
creates a GitHub Release from the build artifacts.
- Python ≥3.9,
srclayout, stdlib-first - Textual — terminal TUI framework (modal overlays / forms / file tree, native overlay menu support)
- pyserial — serial port (including
list_portsenumeration) - pyte — VT terminal emulation for the device RX byte stream (subclassing its
Screento capture scrolled-out content for history; LGPLv3) - In-house YMODEM engine (
xfer/ymodem.py): CRC-16-CCITT, SOH/STX, 128/1024 blocks, configurable timeout/retry, duplicate-block tolerance, CAN-CAN abort, auto-retransmit on bad block, progress callbacks - In-house ZMODEM engine (
xfer/zmodem.py): ZRQINIT/ZRINIT/ZFILE/ZDATA handshake, hex control headers + binary data subpackets with ZCRCW/ZACK, per-file session - Packaging: PyInstaller; testing: pytest (including Textual Pilot headless UI tests), ruff, mypy
src/pycom/
app.py main program (Ctrl+A prefix state machine, serial routing, transfer worker, capture)
config.py config data models + JSON persistence
serialio.py serial layer (background read thread, write lock, port enumeration, hot-plug)
keys.py key → byte mapping (line ending / backspace / arrow VT sequences…)
termdisplay/
vt.py pyte terminal model (decode, scroll history, resize)
view.py TerminalView / StatusBar widgets
xfer/ymodem.py YMODEM bidirectional protocol engine (pure Python, unit-testable without serial)
xfer/zmodem.py ZMODEM transfer engine (pure Python, unit-testable without serial)
screens/ connection, main menu, options, file/dir picker, transfer screens
theme.py theme-file loader (JSON, dark+light variants, user theme dir)
resources/app.tcss stylesheet (consumes theme variables only)
resources/themes/ theme files (one-half.json = One Half, both variants)
tests/unit/ CRC/frame/block0, engine loopback (incl. error injection), keys, terminal model, Pilot UI
packaging/ PyInstaller launcher and specThe config file is JSON (%APPDATA%\pycom\config.json on Windows /
~/.config/pycom/config.json on Linux); edit it via Ctrl+A O and it saves during runtime.
Colours come from theme files: one theme ships both a dark and a light variant. Pick the theme and the mode (Auto = follow the terminal background / dark / light) in Ctrl+A O → Appearance.
To add your own theme, drop a JSON file into the themes/ folder next to the
config file:
{
"name": "my-theme",
"label": "My Theme",
"variants": {
"dark": { "primary": "#61afef", "variables": { "control-bg": "#3a4048" } },
"light": { "primary": "#0184bc", "variables": { "control-bg": "#ffffff" } }
}
}primary is the only required key; unlisted variables keep their defaults
(the full key list lives in the src/pycom/theme.py docstring). For an
8/16-colour terminal use "ansi": true with ANSI base-colour names such as
ansi_red — that is how the built-in compatibility theme is written.
- v1 included: YMODEM bidirectional transfer, ZMODEM transfer, capture log, line-ending/echo/decode/flow config, scrollback, HEX rendering
- v1 not included: XMODEM/Kermit, ASCII send, macro scripts, dialing directory, split-pane multi-session
- Recommended to run under Windows Terminal (full ConPTY/color support)
- Cross-validate with lrzsz on Linux:
sz -Y fileto PyCom receive;rz -Yto PyCom send - ZMODEM: have lrzsz's
rzreceive a file that PyCom sends (Ctrl+AS, pick ZMODEM) - Use socat(pty)/com0com virtual serial ports for end-to-end loopback on Windows/Linux
- STM32 bootloader flashing on hardware: verify at 115200/921600 each with a large file (SHA256 compare)
This project used an AI programming assistant (GitHub Copilot) during development to help write, review, and debug code. All code was manually reviewed and verified by automated tests (pytest / ruff / mypy).