thermark 0.33.0

Local sticker printing for pocket thermal printers over BLE/USB — QR links, inventory tags, badges, photos; no vendor app
Documentation
# thermark

**Print labels without the vendor app.** Local, scriptable QR codes, Wi-Fi
stickers, inventory tags, and text for pocket thermal printers. No cloud or
account needed to print.

[Download for macOS or Linux](https://github.com/kahwee/thermark/releases/latest) ·
[Watch the offline demo](https://kahwee.github.io/thermark/) ·
[Report your printer](https://github.com/kahwee/thermark/issues/new?template=hardware-report.yml)

**Hardware-tested: B1 over Bluetooth LE.** USB and other printer profiles are
experimental. thermark prints monochrome labels; it does not support colour
printing.

## Install and print your first label

On macOS (Apple Silicon or Intel), install the prebuilt BLE binary:

```sh
brew install kahwee/thermark/thermark
```

For Linux, or macOS without Homebrew, use the
[prebuilt downloads](https://github.com/kahwee/thermark/releases/latest).
Choose `ble` for Bluetooth or `full` for experimental USB serial support.
No Rust compiler is needed for a prebuilt download.

Turn on your B1, load 50×30 mm labels, and quit the vendor app. Then:

```sh
thermark scan --save
thermark doctor --use-config
thermark qr --url "https://example.com" --text "Hello, label!" --label 50x30
```

If several printers appear, follow the scan selection hint. On macOS, allow
Bluetooth access for your terminal when requested. See
[connection troubleshooting](#macos-bluetooth-ownership) if discovery fails.

### Try it without a printer

```sh
thermark qr --url "https://example.com" --text "Hello, label!" \
  --model b1 --label 50x30 --save hello-label.png --no-print
```

This saves a real PNG locally without connecting to hardware. Scan its QR or
inspect the layout before spending a label.

| Guest Wi-Fi | Inventory | URL + text |
| --- | --- | --- |
| ![Demo Wi-Fi label with fake credentials]fixtures/sticker_wifi.png | ![Demo inventory label]fixtures/sticker_inventory.png | ![Demo URL label]fixtures/sticker_link.png |

These are rendered previews using public demo data, not photographs of prints.

## What it does

- Guest Wi-Fi stickers: network name plus a QR code to join.
- URL stickers: a scannable link with human-readable text.
- Plain text, inventory, badge, and line-art labels.
- Exact physical sizing from a CLI or Rust program.
- Local previews before using paper.

## Support

| Model family / path | Status |
|---|---|
| B1 over BLE | Hardware-tested |
| B1 over USB serial | Experimental; implemented and mock-tested |
| B1 Pro, B21 Pro, D11, D11_H, D110 | Experimental monochrome profiles |
| B18 | Geometry known; print task unresolved |

Printing with any profile/task/connection combination other than B1+B1 over
BLE requires `--allow-experimental`; offline previews and saved renders do not.
Multi-colour printheads and colour raster protocols are out of scope.

Building from source requires Rust 1.98 or newer. [`rust-toolchain.toml`](rust-toolchain.toml) tracks
the current stable toolchain for rustup users.

## Build and set up

Build from GitHub with Cargo:

```sh
cargo install --git https://github.com/kahwee/thermark --locked
# BLE only:
cargo install --git https://github.com/kahwee/thermark --locked --no-default-features --features ble
```

The first crates.io release is prepared but not yet published. Until then,
use the commands above or a prebuilt binary.

For a prebuilt binary, [download the latest release](https://github.com/kahwee/thermark/releases/latest).
Choose `ble` for the hardware-tested B1-over-Bluetooth path, or `full` to also
include experimental USB serial support. Builds are available for Linux
x86_64/ARM64 and macOS Apple Silicon/Intel; verification and unpacking commands
are below. Building from source requires Rust:

```bash
scripts/build.sh

# Quit the vendor app first; only one BLE client can hold the printer.
./target/release/thermark scan --save
./target/release/thermark identify
./target/release/thermark doctor --use-config
```

When asking for setup help, create a support report that is safe to attach to
a public issue:

```bash
./target/release/thermark doctor --use-config --json > thermark-report.json
```

The JSON includes the operating system, architecture, enabled transports, and
check results. Printer names and addresses, serial numbers, RFID barcodes, and
local filesystem paths are omitted.

For the smallest primary-path binary, omit USB serial support and build BLE
only:

```bash
scripts/build.sh --no-default-features --features ble
```

The build script and local `scripts/check.sh` reset `target/` on the first run
after 14 days, then regenerate the artifacts needed by that command. Between
resets, builds reuse the cache normally. The first use starts the retention
clock for existing artifacts. Cleanup removes generated binaries, debug/release
caches, and diagnostics under `target/`; source files, `local/`, and printer
settings are unaffected. Plain `cargo build` does not run this cleanup. CI
retains its own cache policy.

Releases include SHA-256 checksums. Verify the checksum before unpacking, for
example (replace the version and platform with your download):

```bash
# Linux
sha256sum --check thermark-0.33.0-Linux-X64-ble.tar.gz.sha256

# macOS
shasum -a 256 --check thermark-0.33.0-macOS-ARM64-ble.tar.gz.sha256

tar -xzf thermark-0.33.0-macOS-ARM64-ble.tar.gz
cd thermark-0.33.0-macOS-ARM64-ble
./thermark --version
./thermark tasks
```

Contributors can run the same validation entry point locally and in CI:

```bash
scripts/check.sh           # formatting, Clippy, default-feature tests and docs
scripts/check.sh all       # also lint/test/document each transport feature set
scripts/check.sh render    # focused golden / fixture / placement checks
```

Use `scripts/check.sh --help` for prerequisites and modes. Checks require no
printer; failed golden renders appear in `target/golden-actual/`.

To capture the exact model, firmware, geometry, and task for a hardware report:

```bash
./target/release/thermark identify --json \
  > local/printer-identity.json
```

Use the full advertising name shown by `scan`. Matching is exact by default;
`--fuzzy` enables intentional substring matching. On macOS, Bluetooth device IDs
are UUIDs rather than classic MAC addresses.

### macOS Bluetooth ownership

macOS can show a printer as **Connected** while thermark reports that it is not
discoverable. One likely cause is that another Bluetooth client—often a vendor
app or a system-managed session—still owns this printer's BLE GATT connection.
The same symptom can also mean the printer is asleep, out of range, or not
advertising, so treat ownership as a diagnosis to verify rather than a fact.

Before retrying, disconnect the printer in macOS Bluetooth settings, quit any
vendor label app, wake or power-cycle the printer, and then run:

```bash
./target/release/thermark scan
./target/release/thermark doctor --use-config
```

If `doctor` reports a matching `/dev/cu.…` endpoint, that supports the ownership
diagnosis, but proves neither ownership nor serial-protocol compatibility. Use
the endpoint with `--conn usb` only when the printer responds to the serial
protocol; otherwise release any competing Bluetooth session and use BLE
normally.

## Print stickers

Always pass the physical label size. After `scan --save`, the printer address is
read from config.

Guest Wi-Fi:

```bash
THERMARK_WIFI_PASSWORD='your-password' \
  ./target/release/thermark wifi \
  --ssid "YourNetwork" \
  --label 50x30
```

Open networks do not need a password:

```bash
./target/release/thermark wifi \
  --ssid "Cafe-Guest" \
  --security nopass \
  --label 50x30
```

URL with readable text:

```bash
./target/release/thermark qr \
  --url "https://example.com/o/1042" \
  --text $'ORDER #1042\nPriority' \
  --font-name helvetica \
  --label 50x30
```

Plain text:

```bash
./target/release/thermark text \
  --text $'FRAGILE\nthis way up' \
  --label 50x30
```

Existing artwork:

```bash
./target/release/thermark print \
  -i local/prints/art.png \
  --label 50x30 \
  --no-fill \
  --margin 0 \
  -d 4
```

Personal artwork and real credentials belong under `local/`, which is ignored
by git. Committed files under [`fixtures/`](fixtures/) contain public demo data
only.

### Which printer profile controls rendering

For an online `text`, `qr`, `wifi`, or `calibrate` command, thermark connects
and identifies the printer before converting the physical label size to pixels.
The detected profile's DPI and printhead width therefore determine the rendered
canvas. If a generated-label command also uses `--save`, the PNG is written
from that same detected-profile render before it is printed.

Hardware printing fails closed if the identity probe fails or reports an
unrecognized model. thermark will not render or send a job using provisional
profile geometry in that case.

`--no-print` remains connection-free. In that mode, generated labels use the
profile selected by `--model`, then the saved configuration, then the B1
default. Combine `--save <path> --no-print` when you want a wholly offline
render.

Online raw-image printing also lays out the image against the detected profile.
Its default 1 mm registration margin is converted at the detected DPI; an
explicitly saved pixel inset and `--full-bleed` remain exact.

## Preview and calibrate

Preview the exact bitmap for the selected profile without printing:

```bash
./target/release/thermark print \
  -i local/prints/art.png \
  --label 50x30 \
  --preview local/preview.png
```

`print --preview` applies the selected threshold and dithering and writes the
final monochrome page for the configured or `--model` profile. If that profile
does not match hardware later detected by an online print, its geometry can
differ. Generated sticker commands use
`--save <path> --no-print` to inspect their composed artwork; those saved PNGs
can retain antialiasing that the printer later thresholds. Without
`--no-print`, `--save` waits until printer identification so the saved bitmap
and printed page share the same profile-sized render.

Check label placement on hardware:

```bash
./target/release/thermark calibrate --label 50x30
./target/release/thermark calibrate --boundary --label 50x30
```

### Label size and RFID

The printer's RFID response can describe the consumable type and remaining
label count, but it does not report dimensions in millimetres. Vendor software
may resolve an RFID barcode through its own catalogue; thermark has no cloud
catalogue. Pass `--label WxH` when media changes, or save the size with
`thermark config set --label WxH`.

## If a print looks wrong

1. Run `thermark info` and charge the printer if the battery is low.
2. Preview the bitmap to separate rendering problems from hardware problems.
3. Print the same bitmap twice. Inconsistent truncation points indicate power,
   not geometry.
4. Use `calibrate --boundary` only after the printer is charged.

A charged B1 reaches the full feed canvas. Its default 1 mm top/bottom inset is
registration margin, not a known unprintable band.

For connection failures on macOS, follow the
[Bluetooth ownership checks](#macos-bluetooth-ownership) above.

## Development

```bash
scripts/check.sh all       # formatting, Clippy, tests, and docs across feature sets
scripts/check.sh render    # golden, fixture, and placement checks
cargo bench --locked --bench image_pipeline
cargo bench --locked --no-default-features --bench packet_decoder
PROPTEST_CASES=4096 cargo test --locked --no-default-features --test packet_stream
```

Linux BLE builds require `libdbus-1-dev` and `pkg-config`. The shared check
script uses locked dependencies, rejects accidental golden-image updates, and
matches the validation run by CI on Linux and macOS. Packet-stream property
tests cover arbitrary fragmentation, full-size payloads, corruption recovery,
and bounded retained input. Commit any generated `proptest-regressions/` seeds
when fixing a failing property so the same input stays covered.

Dependency security runs separately on dependency changes and weekly against
the current RustSec advisory database, failing on vulnerabilities, unsoundness
advisories, and yanked crates.
Unmaintained notices remain visible: `ab_glyph` currently pulls in
`ttf-parser`, covered by [RUSTSEC-2026-0192](https://rustsec.org/advisories/RUSTSEC-2026-0192).
There is no patched upstream version; replacing the font engine needs dedicated
rendering verification. To reproduce it locally:

```bash
cargo install cargo-audit --version 0.22.2 --locked
cargo audit --deny unsound --deny yanked
```

The benchmarks report CPU-only medians. Compare runs on the same host, and
measure peak RSS in separate processes when evaluating memory changes. See the
[packet decoder measurements](docs/packet-decoder-benchmark.md) for the bulk-read
optimization and its small-read tradeoff.

The architecture keeps four concerns separate:

- `profile.rs`: detected printer identity, physical capabilities, default task,
  and the single support-status registry exposed as `PROFILES`.
- `transport/`: BLE and USB communication.
- `printer/`: validated job lifecycle and protocol queries.
- `label.rs` / `image_encode.rs`: layout and monochrome raster generation.

For library API migrations, see the explicit old-to-new mappings under
[0.32.0 → Removed](CHANGELOG.md#removed).

See [`AGENTS.md`](AGENTS.md) for protocol details, hardware measurements, and
contributor invariants.

## License

MIT

## Help support more printers

Own a B1 Pro, B21 Pro, D11, D11_H, or D110? A physical test with public demo
content helps more than a successful compilation. Share your model, firmware,
OS, connection, label size, privacy-safe diagnostics, and a photo of the result
through the [hardware report form](https://github.com/kahwee/thermark/issues/new?template=hardware-report.yml).

Read [CONTRIBUTING.md](CONTRIBUTING.md) for a short testing checklist. Support
status changes only when the evidence supports it.