Skip to main content

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

Hand-built label:

import asyncio
from PIL import Image, ImageDraw
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())

Continuous-tape text with automatic shrink-to-fit (0.1.5+):

from pybrady.labels import continuous_text
from pybrady import BradyPrinter
from pybrady.transport import UsbTransport
import asyncio

async def main():
    result = continuous_text(
        "Rack 7 • Port 24",
        tape_width_in=0.67,
        dpi=300,
        size_pt=36,        # shrunk automatically if it doesn't fit
    )
    if result.shrunk:
        print(f"note: shrunk from 36pt to {result.actual_size_pt}pt to fit tape")

    async with UsbTransport.find_bmp51() as t:
        printer = BradyPrinter(t, model="BMP51")
        await printer.print(result.image)

asyncio.run(main())

The composer never silently clips — if even the minimum legible size (8pt default) doesn't fit, it raises LabelOverflowError. This replaces the pre-0.1.5 silent-truncation behaviour that could waste media on unreadable labels.

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 VID 0E2E / PID 000B so you don't have to hunt for it in the dropdown.

Install steps:

  1. Download zadig-2.x.exe from zadig.akeo.ie into any folder.

  2. Copy tools/zadig.ini from this repo next to zadig.exe (same folder). Zadig reads it automatically at launch.

  3. Plug in the BMP51 and run zadig.exe as administrator.

  4. 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.

  5. 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.

  6. 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 includes libusb-package, which bundles the libusb-1.0.dll that 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: OpenPrinterStartDocPrinter(datatype="RAW")WritePrinterEndDocPrinterClosePrinter. 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:

  1. Platform (uname -a output or systeminfo / system_profiler SPSoftwareDataType)
  2. Printer model + firmware version (brady-print --identify over USB, or the LCD's info screen)
  3. What you ran (full command line)
  4. What happened (output, stack trace, or a PNG saved via --save and the label a brady-print --text HELLO produced)

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

pybrady-0.1.6.tar.gz (383.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pybrady-0.1.6-py3-none-any.whl (262.3 kB view details)

Uploaded Python 3

File details

Details for the file pybrady-0.1.6.tar.gz.

File metadata

  • Download URL: pybrady-0.1.6.tar.gz
  • Upload date:
  • Size: 383.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

Hashes for pybrady-0.1.6.tar.gz
Algorithm Hash digest
SHA256 33e550117ca336b03cb885faea6aa3baf017d4e6cea856349f1fc3b90e88777d
MD5 dc05f31948dad3bfb82c112bdf6865e8
BLAKE2b-256 c91916f04b5ea567b30f2b2e285edf8c6ba466780076a5d4fc6da9ec4e6edc25

See more details on using hashes here.

File details

Details for the file pybrady-0.1.6-py3-none-any.whl.

File metadata

  • Download URL: pybrady-0.1.6-py3-none-any.whl
  • Upload date:
  • Size: 262.3 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

Hashes for pybrady-0.1.6-py3-none-any.whl
Algorithm Hash digest
SHA256 de21aec2c5fd2586ad7af8e55730450f020d46579f617cbb7ea7edb7b0af6cdc
MD5 fba5a454ab5564b4525377b411bbd8bb
BLAKE2b-256 343ce9996fe5efa84c44bcfc5b8587f685f35de831bd2c417f7286b4b7de42e6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page