Open-source embedded telemetry, debugging, and protocol analysis — the Wireshark of microcontrollers.
UARTScope connects to a serial port, parses the telemetry your board emits
(KEY:VALUE lines and JSON), charts it live, alerts you on thresholds, records
replayable sessions, and decodes raw bus traffic (UART text, Modbus RTU, I2C,
SPI, CAN, CAN with a DBC file). The desktop app is pure Python (NiceGUI) — no
Node.js, no build step, no browser install.
| Area | Detail |
|---|---|
| Serial monitoring | Connect, disconnect, and send on any serial port; terminal view of the raw stream |
| Live telemetry | Automatic KEY:VALUE and JSON parsing into per-metric charts grouped by unit |
| Alerts | Rule-based conditions (>, <, >=, <=, ==, range, rate-of-change) with per-rule cooldown and an alert history with acknowledgment |
| Sessions | Record, replay, diff against a golden baseline, and export JSON/CSV; share as .uartscope bundles from the desktop app or GET /api/export/session/{id}/bundle |
| Protocol decoders | Six built-ins — UART Text, Modbus RTU, I2C, SPI, CAN Bus, CAN DBC — all with encode as well as decode |
| Firmware flashing | Compile and flash ESP32 sketches from the desktop app |
| Plugin marketplace | Install real third-party decoders from a registry manifest; registry/registry.json ships LIN, J1939 and DALI. Refused before execution by an AST capability policy, digest-pinned, with persistence and uninstall implemented |
| MQTT | Multi-broker profiles, pub/sub, message history (backend API is live; the MQTT screen is still on the v1 UI — see below) |
| REST + WebSocket API | FastAPI backend on :8080 with ~50 verified routes; the OpenAPI schema at /docs lists them all |
- Installing a plugin runs third-party Python inside the app process and is
not sandboxed — a registered plugin's
can_decodeis called against live traffic, and no OS boundary can sit in that path without adding an IPC hop to every frame. What the marketplace does enforce: installs are off by default; an AST capability policy refuses any plugin that imports a filesystem, network or process module, or reaches for interpreter internals, before it is compiled; andregistry/registry.jsonpins a SHA-256 per module, which is recorded on install and re-checked at startup, so a file swapped on disk after install is dropped instead of loaded. That is a seatbelt, not a sandbox: a plugin that loops forever still hangs the decode path. Read the source first. The screen says this on the page. - The registry ships as a local manifest, not a hosted service. Set
UARTSCOPE_PLUGIN_REGISTRYto anhttp(s)URL to point at a remote one; the manifest format is the same either way. - Every screen has been rebuilt to the v2 design treatment, so no screen carries a "Not yet v2" marker. The Performance and MQTT screenshots are taken against seeded demo data — a fixed seed, so the numbers in the image are reproducible but are not measurements of your hardware.
- There is no baudrate auto-detection. You set the baudrate; the app uses it.
- Sessions are not created automatically when you start streaming. You create a session when you want one recorded.
git clone https://github.com/DawnofGenX/uartscope.git
cd uartscope
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r backend/requirements.lock
python desktop_app.pyOpen http://localhost:3000. That is the whole app: desktop_app.py imports
the backend modules in-process and serves the UI itself. You do not need to
run uvicorn separately for local use.
backend/requirements.lock (runtime) and backend/requirements-dev.lock (adds
pytest, pytest-asyncio, pytest-httpx) pin every dependency at an exact version, generated
from backend/pyproject.toml. CI, both Dockerfiles and the command above all install from a
lock, so the environment you get is the environment that was tested.
Before this, there were two hand-maintained manifests that disagreed: esptool (firmware
flashing) was declared in pyproject.toml but missing from requirements.txt, so the Docker
images — which installed requirements.txt — shipped without it, and a fresh clone resolved
every dependency to "whatever is latest today".
Regenerate after changing dependencies in backend/pyproject.toml:
cd backend
uv pip compile pyproject.toml --universal -o requirements.lock
uv pip compile pyproject.toml --extra dev --universal -o requirements-dev.lock--universal matters: without it the lock is a Linux resolution, and it carried
uvloop, which refuses to build on Windows. pip install -r requirements.lock therefore failed
outright on Windows — and because that step ran on one line with ;, it looked green while
installing nothing, so the Windows build produced a binary with no dependencies in it. A universal
lock emits environment markers (uvloop ... ; sys_platform != 'win32', colorama ... ; sys_platform == 'win32') and installs on Linux, macOS and Windows from the same file.
No board handy? Simulate one in a second terminal:
python scripts/simulate_device.py /dev/ttyUSB0 115200 --virtualVirtual serial port: /dev/pts/5
Connect UARTScope to: /dev/pts/5
Add the printed /dev/pts/… port as a device in the UI, press Start, and
watch live charts. The steps above are the whole path; the removed tutorial
walked through the same thing in more detail.
cd backend
python -m uvicorn app.main:app --port 8080
curl http://127.0.0.1:8080/api/health{"status":"healthy","version":"2.2.2","devices":{"total":0,"connected":0,"streaming":0,"errors":0,"total_bytes_received":0,"total_packets":0},"active_sessions":0,"websocket_clients":0}Each release attaches uartscope.exe (built on windows-latest from
uartscope.spec). Run it — no Python, no Node, no install:
uartscope.exe
It is one process serving both surfaces: the desktop UI on http://localhost:3000 and the REST/WebSocket API on http://localhost:8080.
If :8080 is already taken — it is llama.cpp's default, among others — the app says so in its
console and starts the UI anyway; the UI does not need the API. Point the API somewhere else with
UARTSCOPE_API_PORT=18080.
To build it yourself:
pip install -r backend/requirements.lock pyinstaller==6.22.3
pyinstaller --clean uartscope.specBuild from the spec, not from a .py file: pyinstaller launch.py ignores the
spec and produces a bundle with no UI, no backend and no registry. That is not
hypothetical — it is what every release up to and including v2.2.1 shipped, and
the binary re-launched itself instead of serving. CI now starts the built .exe
and requires both ports to answer before the asset is attached to a release.
docker compose up -dDocker is the one case where the HTTP backend matters: the desktop container is
given API_URL=http://backend:8080 and the browser talks to the FastAPI
service on :8080, while the UI is served on :3000.
- FAQ — finding your serial port on Linux, macOS and Windows, 8E1 sensors, which framing a protocol needs, why a plugin install was refused, and the bugs this project has already fixed once
The long-form guides have been removed. What remains here:
- Quick start (above) — the five-minute setup, including the no-hardware path
- What to know before you trust the screenshots (above) — what the screens do and do not show
- Development (below) — running the tests and the static gates
The full guides are recoverable from the v2.0.1 tag if you want them: https://github.com/DawnofGenX/uartscope/tree/v2.0.1/docs
Python 3.11–3.13 (CI matrix). From the repo root, with a venv active:
.venv/bin/python -m pytest backend/tests/ -q
python check_handler_order.py
python check_dict_keys.py
python smoke_pages.py --boot --wait 60 # renders every screen headlessThe suite covers the serial reader against a real character device (a pty), so the hardware path is tested without a board attached. CI additionally fails the build if those tests stop running. See the FAQ.
Lint is ruff check app/ --line-length=120 --select E9,F63,F7,F82 from
backend/, enforced in CI.
MIT. See LICENSE.
More writing on this stack: https://dev.to/dawnofgenx

