Metadata-Version: 2.4
Name: pybrady
Version: 0.1.7
Summary: Python library for Brady label printers (USB, TCP, Bluetooth Classic, BLE)
Project-URL: Homepage, https://gitlab.com/ggiesen/pybrady
Project-URL: Repository, https://gitlab.com/ggiesen/pybrady.git
Project-URL: Issues, https://gitlab.com/ggiesen/pybrady/-/issues
Author: ggiesen
License-Expression: MPL-2.0
License-File: LICENSE
Keywords: bmp51,bmp61,brady,label,m211,m611,printer
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
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 :: Printing
Classifier: Topic :: System :: Hardware
Requires-Python: >=3.10
Requires-Dist: pillow>=10.0
Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
Provides-Extra: all
Requires-Dist: bleak>=0.22; extra == 'all'
Requires-Dist: libusb-package>=1.0; (sys_platform == 'win32') and extra == 'all'
Requires-Dist: lz4>=4.0; extra == 'all'
Requires-Dist: pdf417gen>=0.8; extra == 'all'
Requires-Dist: ppf-datamatrix>=0.2; extra == 'all'
Requires-Dist: python-barcode>=0.15; extra == 'all'
Requires-Dist: pyusb>=1.2; extra == 'all'
Requires-Dist: pywin32>=305; (sys_platform == 'win32') and extra == 'all'
Requires-Dist: segno>=1.6; extra == 'all'
Provides-Extra: barcode
Requires-Dist: python-barcode>=0.15; extra == 'barcode'
Provides-Extra: ble
Requires-Dist: bleak>=0.22; extra == 'ble'
Provides-Extra: datamatrix
Requires-Dist: ppf-datamatrix>=0.2; extra == 'datamatrix'
Provides-Extra: lz4
Requires-Dist: lz4>=4.0; extra == 'lz4'
Provides-Extra: pdf417
Requires-Dist: pdf417gen>=0.8; extra == 'pdf417'
Provides-Extra: qr
Requires-Dist: segno>=1.6; extra == 'qr'
Provides-Extra: render
Requires-Dist: pdf417gen>=0.8; extra == 'render'
Requires-Dist: ppf-datamatrix>=0.2; extra == 'render'
Requires-Dist: python-barcode>=0.15; extra == 'render'
Requires-Dist: segno>=1.6; extra == 'render'
Provides-Extra: usb
Requires-Dist: libusb-package>=1.0; (sys_platform == 'win32') and extra == 'usb'
Requires-Dist: pyusb>=1.2; extra == 'usb'
Provides-Extra: windows
Requires-Dist: pywin32>=305; (sys_platform == 'win32') and extra == 'windows'
Description-Content-Type: text/markdown

# 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

```bash
pip install 'pybrady[usb]'            # USB support (pyusb)
pip install 'pybrady[ble]'            # BLE support (bleak)
pip install 'pybrady[all]'            # everything
```

## Development

pybrady uses [uv](https://docs.astral.sh/uv/) for environment management, with ruff for lint/format and pytest for tests.

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

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

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

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

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

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

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

```bash
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](docs/brady_specification.md#25-windows-driver-architecture-open-question-transports-evaluated) — 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](https://zadig.akeo.ie/) — 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](https://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:

   ```powershell
   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`](https://pypi.org/project/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:

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

```powershell
pip install "pybrady[windows]"
```

```python
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](https://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](LICENSE). 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`](docs/brady_specification.md) for the full byte-level spec used to build this library.
