Bluetoothミニプリンター「Mini Printer Sugar」をPCから操作する

Bluetooth感熱ミニプリンター「Mini Printer Sugar」(株式会社エール)。ビックカメラドットコムだと5000円ちょいするが、あきばおーで1000円ちょいで売ってたので遊び用に買ってきた。専用のスマホアプリからしか印刷できない想定の製品だけど、PCから直接操作できるようにしたい。
スペック
| 項目 | 仕様 |
|---|---|
| 印刷方式 | 感熱式(インク不要) |
| 解像度 | 200dpi |
| 接続 | Bluetooth(BLE) |
| 電源 | USB充電式 |
| サイズ | W81 x D85 x H45mm / 146g |
BLEスキャンとサービス確認
Python + bleakでBLEスキャンする。
pip install bleak
import asyncio
from bleak import BleakScanner
async def scan():
devices = await BleakScanner.discover(timeout=5.0)
for d in sorted(devices, key=lambda x: x.rssi, reverse=True):
print(f"{d.rssi:4d} dBm | {d.name or '(unknown)':20s} | {d.address}")
asyncio.run(scan())
デバイス名 YMP-01 で検出された。
接続してサービスを確認すると、以下の構成だった。
| 項目 | UUID |
|---|---|
| Service | 49535343-fe7d-4ae5-8fa9-9fafd205e455 |
| TX(Write) | 49535343-8841-43f4-a8d4-ecbe34729bb3 |
| RX(Notify) | 49535343-1e4d-4bd9-ba61-23c647249616 |
これはISSC(Microchip)のTransparent UARTサービスで、BLE経由でシリアル通信するための汎用プロファイル。安価なBLE感熱プリンターでよく見る「Cat Printerプロトコル」(Service UUID 0000AE30)とは異なるため、Cat Printer系のライブラリはそのまま使えない。
プロトコルの特定
Cat Printer式のコマンド(マジックバイト 0x51 0x78)を送ってみたが反応なし。ESC/POSコマンドを試したところ、紙送りに成功した。
import asyncio
from bleak import BleakScanner, BleakClient
TX_UUID = "49535343-8841-43f4-a8d4-ecbe34729bb3"
async def feed():
devices = await BleakScanner.discover(timeout=5.0)
target = next((d for d in devices if d.name and "YMP" in d.name), None)
if not target:
print("YMP not found")
return
async with BleakClient(target.address) as client:
# ESC @ = プリンター初期化
await client.write_gatt_char(TX_UUID, bytes([0x1B, 0x40]))
await asyncio.sleep(1)
# ESC J n = n dot分の紙送り
await client.write_gatt_char(TX_UUID, bytes([0x1B, 0x4A, 0x30]))
await asyncio.sleep(1)
# LF x3 = 改行3回
await client.write_gatt_char(TX_UUID, bytes([0x0A, 0x0A, 0x0A]))
asyncio.run(feed())
つまりこのプリンターはBLE上でESC/POSコマンドを受け付ける。ESC/POSは感熱レシートプリンターの業界標準プロトコルで、コマンド体系が広く文書化されている。
画像を印刷する
ESC/POSのビットマップ印刷コマンド GS v 0(0x1D 0x76 0x30)で画像を送信する。画像をグレースケール化 → 閾値128で白黒2値化 → 1ビット/ピクセルのビットマップに変換して送る。
import asyncio
from bleak import BleakScanner, BleakClient
from PIL import Image
TX_UUID = "49535343-8841-43f4-a8d4-ecbe34729bb3"
def image_to_escpos(img_path, max_width=384):
img = Image.open(img_path).convert("RGBA")
# 透過部分を白背景に合成
bg = Image.new("RGBA", img.size, (255, 255, 255, 255))
bg.paste(img, mask=img.split()[3])
img = bg.convert("L")
# プリンター幅に合わせてリサイズ
w, h = img.size
if w > max_width:
ratio = max_width / w
img = img.resize((max_width, int(h * ratio)))
w, h = img.size
# 幅を8の倍数にパディング
pad_w = (w + 7) // 8 * 8
if pad_w != w:
padded = Image.new("L", (pad_w, h), 255)
padded.paste(img, (0, 0))
img = padded
w = pad_w
# 白黒2値化
img = img.point(lambda x: 0 if x < 128 else 255, "1")
bytes_per_line = w // 8
# GS v 0: 1D 76 30 m xL xH yL yH [data]
header = bytes([
0x1D, 0x76, 0x30, 0x00,
bytes_per_line & 0xFF, (bytes_per_line >> 8) & 0xFF,
h & 0xFF, (h >> 8) & 0xFF,
])
# PILの1-modeは0=黒、ESC/POSは1=黒なので反転
raw = img.tobytes()
inverted = bytes([~b & 0xFF for b in raw])
return header + inverted
async def print_image():
devices = await BleakScanner.discover(timeout=5.0)
target = next((d for d in devices if d.name and "YMP" in d.name), None)
if not target:
print("YMP not found")
return
async with BleakClient(target.address) as client:
await client.write_gatt_char(TX_UUID, bytes([0x1B, 0x40]))
await asyncio.sleep(0.5)
data = image_to_escpos("image.png")
# BLEのMTU制限があるのでチャンクに分けて送信
chunk_size = 100
for i in range(0, len(data), chunk_size):
await client.write_gatt_char(TX_UUID, data[i:i+chunk_size])
await asyncio.sleep(0.05)
await asyncio.sleep(1)
await client.write_gatt_char(TX_UUID, bytes([0x0A, 0x0A, 0x0A]))
asyncio.run(print_image())
いくつかのパターンで印刷を試した。なお、このプリンターは紙が下から排出されるためビットマップを上から送ると上下逆に印刷される。180度回転してから送る必要がある。
1. 256x256そのまま + 閾値2値化
元画像の256pxをリサイズせず送信。回転補正なし。
- 送信サイズ: 256x256
- 2値化: 閾値128
- 回転: なし(逆さで出力)


サイズは小さいがドット絵のくっきり感は一番出ている。
2. 384px拡大 + Floyd-Steinbergディザリング
プリンター幅いっぱいに拡大し、PILのFloyd-Steinbergディザリングで2値化。
- 送信サイズ: 384x384(NEAREST拡大)
- 2値化: Floyd-Steinbergディザリング
- 回転: なし(逆さで出力)

ディザリングのドットパターンがピクセルアートのドットと干渉して、逆にぼやけた。ピクセルアートにはディザリングは不向き。
3. 384px拡大 + 閾値2値化
ディザリングなしで384pxに拡大。
- 送信サイズ: 384x384(NEAREST拡大)
- 2値化: 閾値128
- 回転: なし(逆さで出力)

256→384は1.5倍で割り切れないため、ピクセルの拡大率にムラが出てジャギーが目立つ。
4. 全身イラスト + 180度回転
別のピクセルアート(全身)で180度回転を適用。
- 送信サイズ: 384x384(NEAREST拡大)
- 2値化: 閾値128
- 回転: 180度(正位置で出力)


向きは正しく出た。髪の中間色が飛んでしまうのは閾値2値化の限界で、200dpiの感熱プリンターだとこのあたりが実力。
5. トーン処理済み線画 + 閾値128
漫画用のスクリーントーンが貼ってある線画で試した。
- 送信サイズ: 384x530
- 2値化: 閾値128
- 回転: 180度


線は出ているが、髪や肌のトーン(薄いグレーのドットパターン)が閾値128では白に飛んでしまう。
6. トーン処理済み線画 + 閾値200
同じ線画で閾値を200に上げて再印刷。
- 送信サイズ: 384x530
- 2値化: 閾値200
- 回転: 180度

制服や目の周りのトーンは拾えるようになった。ただし髪のトーンは薄すぎて200でも飛ぶ。1000円の200dpi感熱プリンターだとこのあたりが限界。
テキスト(レシート)を印刷する
ESC/POSのテキスト印刷コマンド(ESC aでセンタリング、文字列 + LF)を直接送ってみたが、何も印刷されなかった。テキストコマンドには対応していないようだ。
代わりにPILでレシート画像をレンダリングし、ビットマップとして送信する方式で対応できた。
from PIL import Image, ImageDraw, ImageFont
def render_receipt(print_width=384):
font = ImageFont.truetype("/System/Library/Fonts/Menlo.ttc", 20)
font_large = ImageFont.truetype("/System/Library/Fonts/Menlo.ttc", 24)
lines = [
("center", font_large, "Kanachan Shop"),
("center", font, "========================"),
("left", font, "2026/03/15 00:30"),
("left", font, "------------------------"),
("left", font, "Chibi Sticker x1 500"),
("left", font, "Acrylic Key x2 1200"),
("left", font, "Bunny Headband x1 800"),
("left", font, "------------------------"),
("left", font, "Subtotal 2500"),
("left", font, "Tax(10%) 250"),
("center", font, "========================"),
("left", font_large, "Total 2750"),
("left", font, "Cash 3000"),
("left", font, "Change 250"),
("left", font, ""),
("center", font, "Thank you!"),
]
# 各行を描画して画像化
img = Image.new("L", (print_width, 600), 255)
draw = ImageDraw.Draw(img)
y = 20
for align, f, text in lines:
if align == "center":
bbox = draw.textbbox((0, 0), text, font=f)
x = (print_width - (bbox[2] - bbox[0])) // 2
else:
x = 10
draw.text((x, y), text, fill=0, font=f)
y += 30
# 180度回転してESC/POSビットマップに変換
img = img.rotate(180)
# ... 以降はimage_to_escposと同じ手順

テキストコマンドは通らないが、画像レンダリング経由ならレシート風の出力もできる。日本語フォントを指定すれば日本語のレシートも印刷可能。
1000円ちょいのおもちゃでもBLE接続 → ESC/POSで画像もレシートも出せることが分かった。まだ全然元が取れてないので、次はこれを使ってなんちゃってレジスターを作れるか試してみたい。