Python library for Brady label printers (USB, TCP, Bluetooth Classic, BLE)
Project description
pybrady
Python 3.10+ library for communicating with Brady label printers.
Status: pre-alpha. Initial target is the Brady BMP51 over USB. The full framework supports USB, TCP (Wi-Fi / Ethernet), Bluetooth Classic SPP, and BLE across Linux, macOS, and Windows, but only the USB + ESC/BMP path is implemented today.
Supported printers (planned)
| Model | Protocol | Transports | v0.1 |
|---|---|---|---|
| BMP51, BMP53 | ESC/BMP | USB, TCP, Bluetooth Classic | USB only |
| BMP61 | ESC/BMP | USB, TCP | — |
| M610, M710 | ESC/BMP | USB, TCP, BLE | — |
| M611, i5300, S3700, i7500, i4311, C1-30, MJ811 | JSON/PICL | USB, TCP, BLE | — |
| M211, M511, MM100BT | VGL/STX | BLE | — |
Platform support matrix
| Platform | Unit-test suite | Real-hardware printing (BMP51) | brady-export-templates |
BWS/BWT decode + render | Font fallback |
|---|---|---|---|---|---|
| Linux (Fedora / RHEL / Ubuntu / Debian; x86_64 + aarch64) | ✅ CI on every push, 3.10–3.13 | ✅ print+bidi via LinuxUsblpTransport (validated on RHEL 10, lp group, no Zadig/udev-equivalent) or UsbTransport (libusb) |
✅ | ✅ | ✅ (DejaVu / Liberation) |
| Windows 11 (x86_64) | ⚠ Manually verified against v0.1.3; not in CI | ✅ print+bidi via UsbTransport (Zadig → WinUSB); ✅ print-only via WindowsSpoolerTransport (no Zadig; no status queries — see docs/brady_specification.md §2.5) |
✅ | ✅ | ✅ (Arial via Windows Fonts) |
| macOS (Apple Silicon + Intel) | ⚠ No CI, no manual validation yet | ⚠ Untested | ⚠ Untested | ⚠ Untested | ⚠ Untested |
Everything pybrady ships is pure-Python + wheels-with-aarch64-support for its binary deps (Pillow, lz4, dbus-fast), so the ⚠ untested rows should work — we just haven't confirmed them on real hardware. Reports welcome (see Contributing below).
Install
pip install 'pybrady[usb]' # USB support (pyusb)
pip install 'pybrady[ble]' # BLE support (bleak)
pip install 'pybrady[all]' # everything
Development
pybrady uses uv for environment management, with ruff for lint/format and pytest for tests.
uv sync --all-extras # create .venv, install package + all optional deps + dev group
uv run pytest # run tests
uv run ruff check # lint
uv run ruff format # format
uv run mypy src # type-check
uv.lock is committed — uv sync produces a reproducible environment across machines. The floor Python version is pinned in .python-version; uv will download it automatically if your system Python doesn't match.
Quick start — CLI
brady-print --list-models # see supported printers
brady-print --text HELLO --dry-run # validate bytes, print nothing
brady-print --text HELLO --dry-run --save out.prn # save raw bytes for replay
brady-print --text HELLO # actually print to a BMP51
Quick start — Python API
import asyncio
from PIL import Image, ImageDraw, ImageFont
from pybrady import BradyPrinter
from pybrady.transport import UsbTransport
async def main():
img = Image.new("1", (600, 200), 1) # 2" x 0.67" @ 300 DPI, white
draw = ImageDraw.Draw(img)
draw.text((20, 20), "HELLO", fill=0) # 0 = black
async with UsbTransport.find_bmp51() as t:
printer = BradyPrinter(t, model="BMP51")
await printer.print(img)
asyncio.run(main())
USB setup per platform
Linux
Two transports work on Linux. LinuxUsblpTransport is the recommended default — simpler, no driver detach/reattach, no custom udev rules:
| Transport | Permissions setup | Kernel-driver handling | Works with usblp disabled |
|---|---|---|---|
LinuxUsblpTransport (pure stdlib) |
sudo usermod -aG lp $USER — then log out and back in |
Stays a consumer of the usblp kernel driver |
❌ No (no device node exists) |
UsbTransport (libusb via pyusb) |
udev rule (see below) | Detaches usblp on each open, reattaches on close |
✅ Yes |
Usage is identical — pick the transport you want:
from pybrady import BradyPrinter
from pybrady.transport import LinuxUsblpTransport # or UsbTransport
async with LinuxUsblpTransport.find_bmp51() as t:
printer = BradyPrinter(t, model="BMP51")
await printer.print(img)
Setup for LinuxUsblpTransport (recommended)
/dev/usb/lp* is a standard root:lp 0660 character device. Add yourself to the lp group:
sudo usermod -aG lp $USER
Log out and log back in (or start a new shell with newgrp lp) for the group membership to take effect. No udev rule, no pyusb/libusb install, no driver acrobatics. Verify with:
ls /dev/usb/lp* # should be listed
python -c "from pybrady.transport import LinuxUsblpTransport; print(LinuxUsblpTransport.list_devices())"
Setup for UsbTransport (libusb path; only needed if usblp is blacklisted)
libusb opens /dev/bus/usb/..., which isn't group-owned. Install the udev rule so the active console user gets ACL access automatically via systemd-logind:
sudo cp packaging/99-brady.rules /etc/udev/rules.d/
sudo udevadm control --reload
sudo udevadm trigger
Then replug the printer. The rule uses TAG+="uaccess" — works on RHEL / Fedora / Ubuntu / Arch desktops without needing a plugdev or similar group. (On a headless SSH-only host, uaccess doesn't apply since there's no active seat; on those, use LinuxUsblpTransport instead or add a group-based rule.)
Windows
Windows ships two usable transports with different tradeoffs. Pick based on what you need:
| Need | Transport | Driver change required | Coexists with Brady Workstation? | Status query (media type, % remaining, errors) |
|---|---|---|---|---|
| Print + live status queries | UsbTransport |
✅ Zadig → WinUSB (one-time, reversible) | ❌ No — Workstation can't see the device until reverted | ✅ Yes |
| Print only, keep Workstation | WindowsSpoolerTransport |
❌ None | ✅ Yes | ❌ No |
| Both at once | — | — | not currently possible | — |
The "both at once" gap is an open reverse-engineering question — Brady Workstation itself manages bidi without a Zadig swap, but the exact mechanism hasn't been reconstructed. Contributions welcome; capture recipe is in the spec.
Option A — UsbTransport (full bidi; requires Zadig)
Windows binds its stock driver to the BMP51, which holds an exclusive handle and prevents pyusb from claiming it. You need to bind WinUSB to the device using Zadig — a small standalone utility, no install required.
The repository ships two config files in tools/ that turn Zadig's ~5-click flow into one click and eliminate the driver-selection confusion:
tools/zadig.ini— pre-sets advanced mode, list-all-devices, and WinUSB as the default driver.tools/brady-bmp51.cfg— a preset device config that identifies the BMP51 by VID0E2E/ PID000Bso you don't have to hunt for it in the dropdown.
Install steps:
-
Download
zadig-2.x.exefrom zadig.akeo.ie into any folder. -
Copy
tools/zadig.inifrom this repo next tozadig.exe(same folder). Zadig reads it automatically at launch. -
Plug in the BMP51 and run
zadig.exeas administrator. -
Device → Load Preset Device → select
tools/brady-bmp51.cfg. The BMP51 is now the active target and WinUSB is already pre-selected as the driver. -
Click Install Driver. Takes 10–20 seconds. Zadig auto-generates and auto-signs the catalog on the fly, so Windows accepts the unsigned driver without any Secure Boot gymnastics.
-
From a fresh shell:
pip install "pybrady[usb]" brady-diagnose # confirm the swap worked brady-print --text HELLO --dry-run # validate the bytes, print nothing brady-print --text HELLO # actually print
The
[usb]extra on Windows includeslibusb-package, which bundles thelibusb-1.0.dllthat pyusb needs as its backend. No system-level libusb install is required.
While WinUSB is bound, Brady Workstation can no longer talk to the printer. To revert, run tools\Revert-BradyWinUSB.ps1 from an elevated PowerShell:
# From the pybrady repo root, or wherever you saved the script:
.\tools\Revert-BradyWinUSB.ps1
The script finds the WinUSB driver package Windows provisioned, calls pnputil /delete-driver ... /uninstall /force, and waits for Windows to re-enumerate the BMP51 with the stock driver. Brady Workstation should be able to see the printer again immediately after.
If the script reports nothing to revert but you're still stuck, there's a manual fallback: Device Manager (devmgmt.msc) → find the BMP51 (check both Universal Serial Bus devices and Printers) → right-click → Uninstall device → tick Delete the driver software for this device → unplug + replug.
Option B — WindowsSpoolerTransport (print only; no Zadig)
If you don't need live status queries, WindowsSpoolerTransport routes print data through Brady's existing Windows print queue (e.g. BMP51(53)). The stock driver stays bound, Brady Workstation keeps working, and no Zadig step is needed:
pip install "pybrady[windows]"
from pybrady import BradyPrinter
from pybrady.transport import WindowsSpoolerTransport
async with WindowsSpoolerTransport.find_bmp51() as t:
printer = BradyPrinter(t, model="BMP51")
await printer.print(img)
Under the hood: OpenPrinter → StartDocPrinter(datatype="RAW") → WritePrinter → EndDocPrinter → ClosePrinter. Requires pywin32, installed by the [windows] extra.
Validation status. Write path validated on a real BMP51 (2026-04-13): labels print through the Brady-driver-bound queue, Brady Workstation stays functional. WindowsSpoolerTransport.read() for bidirectional status does not work — Brady's driver doesn't expose a bidirectional channel through the spooler's port monitor (ReadPrinter returns ERROR_INVALID_HANDLE). Use Option A if you need query_status().
macOS
No driver setup needed — macOS lets pyusb claim USB-printer-class devices directly. pip install 'pybrady[usb]' and you're done.
Contributing
Issues and MRs welcome at gitlab.com/ggiesen/pybrady. Particularly looking for help with:
Real-hardware validation on untested platforms. Anything in the ⚠ rows of the matrix above. The most useful reports follow this shape:
- Platform (
uname -aoutput orsysteminfo/system_profiler SPSoftwareDataType) - Printer model + firmware version (
brady-print --identifyover USB, or the LCD's info screen) - What you ran (full command line)
- What happened (output, stack trace, or a PNG saved via
--saveand the label abrady-print --text HELLOproduced)
Other Brady printer models. The protocol spec (docs/brady_specification.md) covers every model Brady ships in theory, but only BMP51 over USB is validated on real hardware today. If you have any of the other models on the supported-printers table above — BMP61, M610/M710, M611, i5300, S3700, etc. — we'd love a session of USBPcap / Wi-Fi packet capture + a quick round-trip test of the PRINTER_MODELS entry for that model.
Transports other than USB. TCP (Wi-Fi / Ethernet), Bluetooth Classic, and BLE all have byte-level specs written down but no tested Python code. Anyone with a network-capable Brady printer who wants to bring up one of those transports — the protocol work is done, it's just Python glue + asyncio + the existing AsyncTransport interface.
.bws / .bwt samples that exercise edge cases. The decoder is tested against what we have: one asset tag, one barcode, one QR template, one mixed-formatting label. If you have a Brady template that uses features ours don't exercise (multi-paragraph text, rotated elements, images other than icons, uncommon barcode symbologies), a copy of the file — which is your own content, not Brady's — would help harden the decoder.
Windows print-spooler transport and Linux /dev/usb/lp* transport. Both designed out in docs/ideas.md but neither implemented. Either would eliminate the current per-OS driver-swap friction (Zadig on Windows, usblp detach on Linux).
See docs/brady_specification.md for the full byte-level protocol reference if you're contributing at the protocol level, and docs/ideas.md for the speculatively-punted items that have enough design detail to pick up.
License
MPL 2.0. Modifications to pybrady files must be shared back; importing pybrady from proprietary code is fine.
Acknowledgements
Protocol specification based on reverse-engineering of the Brady Express Labels Android app. See docs/brady_specification.md for the full byte-level spec used to build this library.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pybrady-0.1.4.tar.gz.
File metadata
- Download URL: pybrady-0.1.4.tar.gz
- Upload date:
- Size: 374.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
967a979cae2a4f7c53ebbec95e7d6ca49227df6a1a031c8660e7dd446e855124
|
|
| MD5 |
64eb5f9fa5a885f8609d7797ca312813
|
|
| BLAKE2b-256 |
013baf399624f2d3310314dc88fb65a036e3b2e2967cd94af415e21e0c9e61ac
|
File details
Details for the file pybrady-0.1.4-py3-none-any.whl.
File metadata
- Download URL: pybrady-0.1.4-py3-none-any.whl
- Upload date:
- Size: 254.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2c9317cee94280421b33f552cfab56c5280d3ed900ca85d42359b5acf57523e1
|
|
| MD5 |
2f762bb4164f47c6d2d56a951f906929
|
|
| BLAKE2b-256 |
c1082ff22711aea4d321ac42ecd9686482bb975925362b2ef86d0f782626e178
|