Print text and images on a Phomemo M110 label printer over Bluetooth LE — from the CLI, a small FastAPI web server with a job queue, or as a Python library.
Uses uv with your activated virtualenv:
uv syncSupply the printer's BLE MAC with --addr or the PHOMEMO_ADDR environment
variable. If you set neither, the command auto-discovers the printer by
scanning (and prints a tip to set PHOMEMO_ADDR so future prints skip the ~8 s
scan).
phomemo scan # lists nearby devices, flags the M110 printer
export PHOMEMO_ADDR=AA:BB:CC:DD:EE:FF
phomemo print-text "Hi" # works with no address set — discovers, prints, hints# Text (defaults to a 40x30 mm label)
phomemo print-text "Hello world" --label 40x30 --font-size 40 --align center
phomemo print-text "Box A-12" --density 12
# Image — scaled to fit the whole label (Floyd–Steinberg dithering by default)
phomemo print-image logo.png --label 40x30
phomemo print-image photo.jpg --label 50x30 --threshold 128
phomemo print-image banner.png --label 40x0 --no-fit # continuous: width only, any height
# Dry run — render to a PNG instead of printing (no hardware needed)
phomemo print-text "Preview me" --out preview.png
phomemo print-image logo.png --out preview.pngLabel size. --label/-l WxH is in millimetres (default 40x30). The M110
prints at 8 dots/mm, so 40x30 → 320×240 dots. Width is the dimension across the
print head (max 48 mm / 384 dots). Use height 0 (e.g. 40x0) for continuous
media. For images, --fit (default) scales the whole image into the WxH label;
--no-fit scales to the width only and lets the height follow the aspect ratio.
Common options: --addr/-a, --label/-l (mm), --width (px override, multiple
of 8), --density (1–15), --speed (1–5), --media (0x0a label / 0x0b
continuous), --debug (log BLE services + send sequence).
phomemo serve --host 0.0.0.0 --port 8000 # uses PHOMEMO_ADDR or --addrOpen http://localhost:8000/ for the status page (submit text/image jobs and watch the queue update live). API:
| Method | Path | Body |
|---|---|---|
| POST | /api/print/text |
JSON {text, label, font_size, align, density, speed, media} |
| POST | /api/print/image |
multipart file (+ label, fit, threshold, density, speed, media) |
| GET | /api/jobs |
list all jobs |
| GET | /api/jobs/{id} |
single job status |
| GET | /api/status |
printer address, queue depth, active job |
Jobs run one at a time through an in-memory async queue (cleared on restart).
pyphomemo exposes a clean async API:
import asyncio
from pyphomemo import print_text, print_image, scan, PhomemoPrinter
# One-shot helpers (connect, print, disconnect)
asyncio.run(print_text("12:CB:A3:08:0F:34", "Box A-12", label="40x30", align="center"))
asyncio.run(print_image("12:CB:A3:08:0F:34", "label.png", label="40x30"))
# Discover printers (returns ScanResult: .address .name .rssi .is_phomemo)
for d in asyncio.run(scan(timeout=8)):
print(d.address, d.name, "← printer" if d.is_phomemo else "")
# Reuse one connection for several labels
async def batch(addr, texts):
async with PhomemoPrinter(addr) as p:
from pyphomemo import text_to_raster
for t in texts:
raster, height, _ = text_to_raster(t, width=320, align="center")
await p.print_raster(raster, height, width_bytes=320 // 8)
asyncio.run(batch("12:CB:A3:08:0F:34", ["A-1", "A-2", "A-3"]))Exported names: print_text, print_image, scan, discover_printer,
ScanResult, PhomemoPrinter, print_raster, resolve_address,
PrinterError, ENV_ADDR; the model API (PrinterModel, MODELS,
DEFAULT_MODEL, get_model, identify_model, is_phomemo_name); the rendering
helpers (text_to_raster, image_to_raster, text_to_image, label_to_px,
parse_label_size, mm_to_px, load_image, load_font); constants
(PRINTER_WIDTH_PX, BYTES_PER_LINE, PX_PER_MM); and the protocol /
imaging / models submodules. Rendering and detection helpers need no
hardware — handy for previews and tests.
Build a single self-contained pyphomemo executable (no Python install needed
on the target machine) with PyInstaller:
uv sync --group build # install PyInstaller
./build.sh # -> dist/pyphomemo (~25 MB, CLI + web server)The binary bundles everything (CLI, web server, Pillow, bleak) — scp dist/pyphomemo to another x86-64 Linux box and run it directly. (Build on the
OS/arch you want to target; PyInstaller does not cross-compile.) Installing
upx on your PATH before building shrinks it further. Under the hood it's
driven by pyphomemo.spec.
The M110 print head is 384 dots (48 mm) wide. Text/images are rendered to a 1-bit
raster with Pillow, then wrapped in the M110's ESC/POS command framing
(protocol.py) and streamed in 128-byte GATT chunks over BLE characteristic
0xff02 (printer.py). Protocol details were reverse-engineered from the
projects credited below.
This project stands entirely on the reverse-engineering work of two excellent open-source projects — huge thanks to their authors:
- phomemo-tools by Laurent Vivier (GPL-3.0) — a Linux/CUPS driver for Phomemo printers. Its
rastertopm110filter is the source of the M110 ESC/POS command bytes (speed1b 4e 0d, density1b 4e 04, media1f 11, raster1d 76 30 00, footer1f f0 …) and the 203 dpi / 8 dots-per-mm geometry. - phomymo by transcriptionstream (ISC) — a browser-based Web Bluetooth label designer (https://phomymo.affordablemagic.net). Its
ble.js/printer.jsgave the BLE GATT details (service0xff00, write0xff02, notify0xff03), the 128-byte chunked write flow, the delay-separatedprintM110send sequence, and the dithering/raster-packing approach.
Both arrived at their knowledge by sniffing the Bluetooth traffic of the official
Phomemo Android app. pyphomemo simply reimplements the M110 path in Python with
a CLI, web server, and library API.
MIT © Manuel Kuhlmann. pyphomemo is a clean-room reimplementation
that uses only the documented protocol (non-copyrightable byte sequences and BLE
characteristics) from the projects above — no source code was copied from them.
This project was developed largely with the help of AI: the code, tests, and documentation were written by Claude (Anthropic's Claude Code, Opus 4.x) under human direction and review. The M110 protocol itself was not invented by the model — it was derived from the reverse-engineered reference projects credited in Acknowledgements. Reasonable care has been taken to review and test the output, but please use it at your own discretion.