{"id":51004207,"url":"https://github.com/mkuhlmann/pyphomemo","last_synced_at":"2026-06-20T18:30:33.339Z","repository":{"id":361710196,"uuid":"1255433698","full_name":"mkuhlmann/pyphomemo","owner":"mkuhlmann","description":"Print text \u0026 images on Phomemo M110 label printers over Bluetooth LE — CLI, FastAPI web server, and Python library.","archived":false,"fork":false,"pushed_at":"2026-05-31T22:25:29.000Z","size":127,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-01T00:13:57.387Z","etag":null,"topics":["ble","bleak","bluetooth-le","cli","escpos","fastapi","label-prionter","m110","phomemo","python","thermal-printer"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mkuhlmann.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-31T20:27:59.000Z","updated_at":"2026-05-31T22:25:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mkuhlmann/pyphomemo","commit_stats":null,"previous_names":["mkuhlmann/pyphomemo"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/mkuhlmann/pyphomemo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mkuhlmann%2Fpyphomemo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mkuhlmann%2Fpyphomemo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mkuhlmann%2Fpyphomemo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mkuhlmann%2Fpyphomemo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mkuhlmann","download_url":"https://codeload.github.com/mkuhlmann/pyphomemo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mkuhlmann%2Fpyphomemo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34581933,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ble","bleak","bluetooth-le","cli","escpos","fastapi","label-prionter","m110","phomemo","python","thermal-printer"],"created_at":"2026-06-20T18:30:30.756Z","updated_at":"2026-06-20T18:30:33.324Z","avatar_url":"https://github.com/mkuhlmann.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pyphomemo\n\nPrint **text** and **images** on a **Phomemo M110** label printer over **Bluetooth LE**\n— from the CLI, a small FastAPI web server with a job queue, or as a Python library.\n\n## Install\n\nUses [`uv`](https://docs.astral.sh/uv/) with your activated virtualenv:\n\n```bash\nuv sync\n```\n\n## Printer address\n\nSupply the printer's BLE MAC with `--addr` or the `PHOMEMO_ADDR` environment\nvariable. If you set **neither**, the command **auto-discovers** the printer by\nscanning (and prints a tip to set `PHOMEMO_ADDR` so future prints skip the ~8 s\nscan).\n\n```bash\nphomemo scan                     # lists nearby devices, flags the M110 printer\nexport PHOMEMO_ADDR=AA:BB:CC:DD:EE:FF\n\nphomemo print-text \"Hi\"          # works with no address set — discovers, prints, hints\n```\n\n## CLI\n\n```bash\n# Text (defaults to a 40x30 mm label)\nphomemo print-text \"Hello world\" --label 40x30 --font-size 40 --align center\nphomemo print-text \"Box A-12\" --density 12\n\n# Image — scaled to fit the whole label (Floyd–Steinberg dithering by default)\nphomemo print-image logo.png --label 40x30\nphomemo print-image photo.jpg --label 50x30 --threshold 128\nphomemo print-image banner.png --label 40x0 --no-fit   # continuous: width only, any height\n\n# Dry run — render to a PNG instead of printing (no hardware needed)\nphomemo print-text \"Preview me\" --out preview.png\nphomemo print-image logo.png --out preview.png\n```\n\n**Label size.** `--label/-l WxH` is in **millimetres** (default `40x30`). The M110\nprints at 8 dots/mm, so `40x30` → 320×240 dots. Width is the dimension across the\nprint head (max 48 mm / 384 dots). Use height `0` (e.g. `40x0`) for continuous\nmedia. For images, `--fit` (default) scales the whole image into the `WxH` label;\n`--no-fit` scales to the width only and lets the height follow the aspect ratio.\n\nCommon options: `--addr/-a`, `--label/-l` (mm), `--width` (px override, multiple\nof 8), `--density` (1–15), `--speed` (1–5), `--media` (0x0a label / 0x0b\ncontinuous), `--debug` (log BLE services + send sequence).\n\n## Web server\n\n```bash\nphomemo serve --host 0.0.0.0 --port 8000   # uses PHOMEMO_ADDR or --addr\n```\n\nOpen \u003chttp://localhost:8000/\u003e for the status page (submit text/image jobs and watch\nthe queue update live). API:\n\n| Method | Path               | Body                                                                          |\n| ------ | ------------------ | ----------------------------------------------------------------------------- |\n| POST   | `/api/print/text`  | JSON `{text, label, font_size, align, density, speed, media}`                 |\n| POST   | `/api/print/image` | multipart `file` (+ `label`, `fit`, `threshold`, `density`, `speed`, `media`) |\n| GET    | `/api/jobs`        | list all jobs                                                                 |\n| GET    | `/api/jobs/{id}`   | single job status                                                             |\n| GET    | `/api/status`      | printer address, queue depth, active job                                      |\n\nJobs run one at a time through an in-memory async queue (cleared on restart).\n\n## Library use\n\n`pyphomemo` exposes a clean async API:\n\n```python\nimport asyncio\nfrom pyphomemo import print_text, print_image, scan, PhomemoPrinter\n\n# One-shot helpers (connect, print, disconnect)\nasyncio.run(print_text(\"12:CB:A3:08:0F:34\", \"Box A-12\", label=\"40x30\", align=\"center\"))\nasyncio.run(print_image(\"12:CB:A3:08:0F:34\", \"label.png\", label=\"40x30\"))\n\n# Discover printers (returns ScanResult: .address .name .rssi .is_phomemo)\nfor d in asyncio.run(scan(timeout=8)):\n    print(d.address, d.name, \"← printer\" if d.is_phomemo else \"\")\n\n# Reuse one connection for several labels\nasync def batch(addr, texts):\n    async with PhomemoPrinter(addr) as p:\n        from pyphomemo import text_to_raster\n        for t in texts:\n            raster, height, _ = text_to_raster(t, width=320, align=\"center\")\n            await p.print_raster(raster, height, width_bytes=320 // 8)\n\nasyncio.run(batch(\"12:CB:A3:08:0F:34\", [\"A-1\", \"A-2\", \"A-3\"]))\n```\n\nExported names: `print_text`, `print_image`, `scan`, `discover_printer`,\n`ScanResult`, `PhomemoPrinter`, `print_raster`, `resolve_address`,\n`PrinterError`, `ENV_ADDR`; the model API (`PrinterModel`, `MODELS`,\n`DEFAULT_MODEL`, `get_model`, `identify_model`, `is_phomemo_name`); the rendering\nhelpers (`text_to_raster`, `image_to_raster`, `text_to_image`, `label_to_px`,\n`parse_label_size`, `mm_to_px`, `load_image`, `load_font`); constants\n(`PRINTER_WIDTH_PX`, `BYTES_PER_LINE`, `PX_PER_MM`); and the `protocol` /\n`imaging` / `models` submodules. Rendering and detection helpers need no\nhardware — handy for previews and tests.\n\n## Standalone binary\n\nBuild a single self-contained `pyphomemo` executable (no Python install needed\non the target machine) with PyInstaller:\n\n```bash\nuv sync --group build      # install PyInstaller\n./build.sh                 # -\u003e dist/pyphomemo  (~25 MB, CLI + web server)\n```\n\nThe binary bundles everything (CLI, web server, Pillow, bleak) — `scp\ndist/pyphomemo` to another x86-64 Linux box and run it directly. (Build on the\nOS/arch you want to target; PyInstaller does not cross-compile.) Installing\n`upx` on your `PATH` before building shrinks it further. Under the hood it's\ndriven by [`pyphomemo.spec`](pyphomemo.spec).\n\n## How it works\n\nThe M110 print head is 384 dots (48 mm) wide. Text/images are rendered to a 1-bit\nraster with Pillow, then wrapped in the M110's ESC/POS command framing\n(`protocol.py`) and streamed in 128-byte GATT chunks over BLE characteristic\n`0xff02` (`printer.py`). Protocol details were reverse-engineered from the\nprojects credited below.\n\n## Acknowledgements\n\nThis project stands entirely on the reverse-engineering work of two excellent\nopen-source projects — huge thanks to their authors:\n\n- **[phomemo-tools](https://github.com/vivier/phomemo-tools)** by **Laurent Vivier** (GPL-3.0) — a Linux/CUPS driver for Phomemo printers. Its `rastertopm110` filter is the source of the M110 ESC/POS command bytes (speed `1b 4e 0d`, density `1b 4e 04`, media `1f 11`, raster `1d 76 30 00`, footer `1f f0 …`) and the 203 dpi / 8 dots-per-mm geometry.\n- **[phomymo](https://github.com/transcriptionstream/phomymo)** by **transcriptionstream** (ISC) — a browser-based Web Bluetooth label designer (\u003chttps://phomymo.affordablemagic.net\u003e). Its `ble.js`/`printer.js` gave the BLE GATT details (service `0xff00`, write `0xff02`, notify `0xff03`), the 128-byte chunked write flow, the delay-separated `printM110` send sequence, and the dithering/raster-packing approach.\n\nBoth arrived at their knowledge by sniffing the Bluetooth traffic of the official\nPhomemo Android app. `pyphomemo` simply reimplements the M110 path in Python with\na CLI, web server, and library API.\n\n## License\n\n[MIT](LICENSE) © Manuel Kuhlmann. `pyphomemo` is a clean-room reimplementation\nthat uses only the documented protocol (non-copyrightable byte sequences and BLE\ncharacteristics) from the projects above — no source code was copied from them.\n\n## AI Notice\n\nThis project was developed largely with the help of AI: the code, tests, and\ndocumentation were written by [Claude](https://claude.com/claude-code)\n(Anthropic's Claude Code, Opus 4.x) under human direction and review. The M110\nprotocol itself was not invented by the model — it was derived from the\nreverse-engineered reference projects credited in [Acknowledgements](#acknowledgements).\nReasonable care has been taken to review and test the output, but please use it\nat your own discretion.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmkuhlmann%2Fpyphomemo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmkuhlmann%2Fpyphomemo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmkuhlmann%2Fpyphomemo/lists"}