Local-first web application for controlling a Bluetooth mini thermal printer from a desktop browser. No cloud services, no database — a FastAPI backend that talks to the printer, and a vanilla-JS frontend for designing, previewing, and printing content.
Primary target hardware: SC03h "iPrint" thermal pocket printer (58 mm, 384 dots) and its clones (FC02, D1, GB01, GB02, WalkPrint, FunPrint).
Protocol reference: iPrint_SC03h_Protocol_Documentation.md — a full implementation guide for the proprietary BLE binary protocol.
- Block-based editor: text, images, QR codes, barcodes, separators, spacers
- Receipt designer: structured
tableandtotalsblocks (item rows, dotted-leader subtotal/TOTAL lines) - Thermal image pipeline: auto-level, brightness/contrast/sharpen, two dithering modes — Photo (Bayer + Threshold hybrid: even dot texture, hard contrast) and Text (sharp cutoff)
- Live thermal preview (384 px, 1-bit), PNG/PDF export
- Print queue with queued → preparing → printing → completed/failed/cancelled states
- Multi-copy printing; per-copy wake+init (protocol §6.1); cancel between copies
- CSV → barcode label batch printing
- Batch basket (localStorage) → queue with live status
- Hardware diagnostic test page
- Reprint from history
- BLE scan/connect with protocol auto-detection from device name (iPrint vs ESC/POS)
- Interactive connection popup (steps + waiting/error/retry states), 20 s connect cap
- Device info: sends
0xA8on connect and shows model/firmware on the dashboard - Keep-alive watchdog: pings
0xA3every 30 s; a failing ping tears down the stale BLE link (protocol §13.2) instead of failing mysteriously mid-print - Battery/paper status from notify payloads (best-effort)
- Auto-reconnect to the last used printer on startup and after mid-print failures
- Settings: paper width, print resolution, margin, density, tear-bar feed dots
- True 16-level grayscale printing — the official app's photo mode (
0xBE [0,1]+ LZO-compressed0xCFchunks) with its exact tone curve and gray-level Floyd–Steinberg diffusion; per-dot heat energy (no dither dots) - 180-byte BLE chunking, 10 ms pacing (25 ms for > 20 KB jobs)
- Packet-aligned 4 KB bursts with 600 ms drain pauses for long jobs
- CRC-8 (poly 0x07) validated per packet; wrong-width rows center-cropped
- Mandatory notify subscription enforced at connect time
- Packet Inspector (
/api/debug/*): parsed last-job packets with CRC pass/fail, opcode names, raw hex, plus a live wire trace (TX/RX)
- SSE (
/api/events): job state transitions and printer connect/disconnect pushed to the UI in real time (polling fallback) - History modal with stats: totals by status, content-type breakdown, paper usage, prints today
┌────────────────────────────────────────────┐
│ WEB INTERFACE (vanilla JS) │
│ Editor • Preview • Templates • Settings │
│ Batch • History • Packet Inspector │
└───────────────────┬────────────────────────┘
│ HTTP/JSON + SSE (EventSource)
▼
┌────────────────────────────────────────────┐
│ FASTAPI BACKEND │
│ │
│ api/ REST routers + events + debug │
│ services/ printer_manager (queue + SSE)│
│ print_engine (blocks → image) │
│ image_processor (thermal pipe)│
│ template_manager, documents │
│ adapters/ ble (Bleak), bluetooth_classic│
│ mock │
│ protocols/ iprint, escpos, tspl │
└───────────────────┬────────────────────────┘
│ BLE (180 B chunks, paced)
▼
Bluetooth Thermal Printer
Everything runs on 127.0.0.1; no user content leaves the machine.
| Requirement | Version / Notes |
|---|---|
| Python | 3.10+ (tested with 3.10–3.12) |
| OS | Windows 10/11 (primary; BLE via Bleak + system Bluetooth, fonts from C:/Windows/Fonts) |
| Linux | Works for the web app + ESC/POS; BLE needs bluez and python3-dev (see below) |
| macOS | Web app + ESC/POS work; BLE support in Bleak is best-effort |
| Printer | Any BLE thermal printer speaking the iPrint protocol (SC03h, FC02, D1, GB01/02, WalkPrint, FunPrint…) or a standard ESC/POS-over-BLE printer |
Windows (PowerShell):
# 1. Clone / download the project, then:
cd mini-print-studio
# 2. Create and activate a virtual environment
python -m venv venv
.\venv\Scripts\Activate.ps1
# 3. Install dependencies
pip install -r requirements.txtmacOS / Linux (bash):
cd mini-print-studio
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# Linux only — BLE requires BlueZ + dev headers:
sudo apt install bluez libbluetooth-dev python3-dev # Debian/Ubuntu
sudo systemctl enable --now bluetoothpython -m backend.main- The backend serves both the API and the web UI at http://127.0.0.1:8000
- It binds to localhost only — nothing is exposed to the network
- Optional:
MPS_DATA_DIRrelocatessettings.json,printers.json,history.json(tests use this to isolate data)
- Power the printer on and put it near the computer (charge it first if it is low).
- Make sure the computer's Bluetooth radio is on (Settings → Bluetooth & devices).
- Open the app → Devices → Scan BLE Printers.
- Click your printer (name should contain
SC03h,iprint,gb01,walkprint, etc.) → the connection popup walks through BLE link → service discovery → notify subscription → done. - Run Test Page to verify density, alignment, and paper feed.
No OS-level pairing is required — the app connects directly over GATT. If Windows has previously paired the printer, remove it first (Settings → Bluetooth → remove device) to avoid interference.
python -m unittest discover -s tests -qAll 81 tests must pass (protocol packet/CRC, image pipeline, block rendering, templates, REST API).
| Symptom | Fix |
|---|---|
pip install fails on bleak / Pillow |
Upgrade pip (python -m pip install --upgrade pip), then retry |
| Server won't start — "port 8000 in use" | Close the other server, or start on a different port: python -m uvicorn backend.main:app --port 8001 |
| Scan finds nothing | Bluetooth radio off → enable it; printer off/low battery → charge and power on; click Scan again |
| Connect times out after 20 s | Printer too far away; radio interference; reboot the printer, then retry |
Linux: Could not initialize Bluetooth |
Install BlueZ (libbluetooth-dev), restart the bluetooth service |
| Fonts render as boxes | Windows fonts are used by default (C:/Windows/Fonts); on Linux install ttf-mscorefonts or fonts-dejavu |
| Prints look faint / too dark | Settings → Print Density (energy scales from the 17520 baseline); Tear-bar Feed adjusts the trailing feed |
| Endpoint | Description |
|---|---|
GET /api/printers |
Scan for BLE printers |
POST /api/printers/connect |
Connect (body: printer_id, connection_type, address, protocol) |
POST /api/printers/disconnect |
Disconnect |
GET /api/printers/status |
Status incl. battery, paper, device_info |
POST /api/printers/test |
Print diagnostic page |
POST /api/print |
Submit a job (blocks, copies, feed) |
GET /api/print/jobs/{job_id} |
Job status |
GET /api/print/queue |
Active + queued jobs |
POST /api/print/preview |
Render blocks → base64 PNG |
POST /api/print/export?fmt=png|pdf |
Download rendered output |
POST /api/print/cancel |
Cancel job(s) |
POST /api/print/csv |
CSV → barcode labels |
POST /api/images/process |
Image → thermal 1-bit |
GET/POST /api/settings |
App settings (printer.tear_bar_feed_dots, density, …) |
GET /api/templates |
Built-in + saved templates |
GET /api/history, /api/history/stats |
Print history + statistics |
GET /api/documents |
Saved documents |
GET /api/events |
SSE — job + printer events |
GET /api/debug/last-payload |
Parsed last job packets with CRC status |
GET /api/debug/trace |
Recent wire activity (TX/RX hex) |
- Packet:
51 78 <op> 00 <len16le> <payload> <crc8(payload)> FF - CRC-8/ATM (poly
0x07), computed over the payload only - Job sequence:
0xA3 [0x00]wake →0xA4 [0x33]quality →0xAFenergy →0xBE [0x00]mode →0xBD [0x23]speed → rows → feed - Rows: exactly 48 bytes (384 dots, MSB-first,
1= black) - Writes: 180-byte chunks, ≥ 10 ms apart (25 ms for > 20 KB), 600 ms drain between 4 KB bursts
- Never send
0xA6(crashes SC03h), disconnect immediately, or skip the notify subscription
See iPrint_SC03h_Protocol_Documentation.md for the complete reference (opcodes, CRC table, test vectors, pitfalls).
{
"printer": {
"paper_width_mm": 58,
"printable_width_px": 384,
"margin_px": 8,
"density": 8,
"tear_bar_feed_dots": 130
},
"image": { "default_dither": "atkinson" },
"app": { "theme": "dark", "debug_mode": false }
}tear_bar_feed_dots controls the trailing feed of iPrint jobs (sent as 0xA1 chunks of ≤ 100 dots); 0 disables it.
python -m unittest discover -s tests -q81 tests cover the image pipeline, block rendering (incl. table/totals), protocol packet parsing + CRC validation, feed chunking, device-info parsing, SSE event flow, templates, and the REST API (via a persistent-loop ASGI client with an isolated MPS_DATA_DIR).
| Symptom | Fix |
|---|---|
| Nothing prints, no response | Reconnect; the notify subscription is mandatory (§13.3) |
| Prints worked, now writes fail | Stale BLE socket after system restart — reconnect fresh; the watchdog now detects this proactively |
| Page tail missing | Long job: pacing is automatic (> 20 KB → 25 ms + bursts) |
| Garbled rows | 48-byte rows, MSB-first — use the Packet Inspector to check CRC |
| Faint/dark output | Adjust density in Settings (energy scales from 17520) |
| Job aborts on disconnect | Hold the connection ≥ 3 s after the last write (server keeps it open) |
See Bluetooth_Incident_Report.md for a real-world stale-socket incident and the fix that became the auto-reconnect + watchdog behavior.