From 3d48cb4a7b6db48e0fcef0094c3b8c97d2c89fb3 Mon Sep 17 00:00:00 2001 From: AndBondStyle Date: Mon, 30 Oct 2023 02:03:02 +0300 Subject: [PATCH] Update --- .pre-commit-config.yaml | 15 ++ README.md | 19 -- examples/B21_30x15mm_240x120px.png | Bin 0 -> 5132 bytes examples/B21_80x50mm_640x384px.png | Bin 0 -> 17676 bytes niimprint/__init__.py | 1 + niimprint/__main__.py | 106 +++++--- niimprint/{niimbotpacket.py => packet.py} | 10 +- niimprint/printencoder.py | 29 --- niimprint/printer.py | 289 ++++++++++++++++++++++ niimprint/printerclient.py | 195 --------------- poetry.lock | 112 +++++++++ pyproject.toml | 25 ++ readme.md | 52 ++++ requirements.txt | 4 +- 14 files changed, 572 insertions(+), 285 deletions(-) create mode 100644 .pre-commit-config.yaml delete mode 100644 README.md create mode 100644 examples/B21_30x15mm_240x120px.png create mode 100644 examples/B21_80x50mm_640x384px.png create mode 100644 niimprint/__init__.py rename niimprint/{niimbotpacket.py => packet.py} (71%) delete mode 100644 niimprint/printencoder.py create mode 100644 niimprint/printer.py delete mode 100644 niimprint/printerclient.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 readme.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e94cadb --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + - id: check-merge-conflict + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.3 + hooks: + - id: ruff-format + - id: ruff + args: [--fix, --exit-non-zero-on-fix] diff --git a/README.md b/README.md deleted file mode 100644 index 3105e6e..0000000 --- a/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# (WIP) Niimbot printer client - - usage: niimprint [-h] -a ADDRESS [--no-check] [-d DENSITY] [-t TYPE] [-n QUANTITY] image - - Niimbot printer client - - positional arguments: - image PIL supported image file - - options: - -h, --help show this help message and exit - -a ADDRESS, --address ADDRESS - MAC address of target device - --no-check Skips image check - -d DENSITY, --density DENSITY - Printer density (1~3) - -t TYPE, --type TYPE Label type (1~3) - -n QUANTITY, --quantity QUANTITY - Number of copies diff --git a/examples/B21_30x15mm_240x120px.png b/examples/B21_30x15mm_240x120px.png new file mode 100644 index 0000000000000000000000000000000000000000..73aee754457e7031e848cfde9eb6273ae7c6877d GIT binary patch literal 5132 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 a%Ou#%-t_DezXt^T(NNJ*F1l{@`2Pdrrj>sH literal 0 HcmV?d00001 diff --git a/niimprint/__init__.py b/niimprint/__init__.py new file mode 100644 index 0000000..6f0e992 --- /dev/null +++ b/niimprint/__init__.py @@ -0,0 +1 @@ +from .printer import BluetoothTransport, PrinterClient, SerialTransport diff --git a/niimprint/__main__.py b/niimprint/__main__.py index 28a5280..7abaa49 100644 --- a/niimprint/__main__.py +++ b/niimprint/__main__.py @@ -1,44 +1,76 @@ -import argparse -import printerclient -import printencoder +import re +import click from PIL import Image -import time -# import math -# mm_to_px = lambda x: math.ceil(x / 25.4 * 203) -# px_to_mm = lambda x: math.ceil(x / 25.4 * 203) +from niimprint import BluetoothTransport, PrinterClient, SerialTransport -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description="Niimbot printer client") - parser.add_argument('-a', '--address', required=True, help="MAC address of target device") - parser.add_argument('--no-check', action='store_true', help="Skips image check") - parser.add_argument('-d', '--density', type=int, default=2, help="Printer density (1~3)") - parser.add_argument('-t', '--type', type=int, default=1, help="Label type (1~3)") - parser.add_argument('-n', '--quantity', type=int, default=1, help="Number of copies") - parser.add_argument('image', help="PIL supported image file") - args = parser.parse_args() - img = Image.open(args.image) - if img.width / img.height > 1: - # rotate clockwise 90deg, upper line (left line) prints first. - img = img.transpose(Image.ROTATE_270) - assert args.no_check or (img.width == 96 and img.height < 600) +@click.command("print") +@click.option( + "-m", + "--model", + type=click.Choice(["b21", "d11"], False), + default="b21", + show_default=True, + help="Niimbot printer model", +) +@click.option( + "-c", + "--conn", + type=click.Choice(["usb", "bluetooth"]), + default="usb", + show_default=True, + help="Connection type", +) +@click.option( + "-a", + "--addr", + help="Bluetooth MAC address OR serial device path", +) +@click.option( + "-d", + "--density", + type=click.IntRange(1, 5), + default=5, + show_default=True, + help="Print density", +) +@click.option( + "-i", + "--image", + type=click.Path(exists=True), + required=True, + help="Image path", +) +def print_cmd(model, conn, addr, density, image): + assert model != "d11", "D11 support may be broken (test yourself)" + assert conn != "bluetooth", "Bluetooth support may be broken (test yourself)" - printer = printerclient.PrinterClient(args.address) - printer.set_label_type(args.type) - printer.set_label_density(args.density) + if conn == "bluetooth": + addr = addr.upper() + assert re.fullmatch(r"([0-9A-F]{2}:){5}([0-9A-F]{2})", addr), "Bad MAC address" + transport = BluetoothTransport(addr) + if conn == "usb": + port = addr if addr is not None else "auto" + transport = SerialTransport(port=port) - printer.start_print() - printer.allow_print_clear() - printer.start_page_print() - printer.set_dimension(img.height, img.width) - printer.set_quantity(args.quantity) - for pkt in printencoder.naive_encoder(img): - printer._send(pkt) - printer.end_page_print() - while (a := printer.get_print_status())['page'] != args.quantity: - # print(a) - time.sleep(0.1) - printer.end_print() + if model == "b21": + # This may be wrong, but B21 doesn't accept anything larger. It's just shy of + # 50mm * 8 px/mm = 400px (for vertical space it's always 8 px/mm), so lgtm. + max_height_px = 384 + if model == "d11": + max_height_px = 100 # I don't have D11 to test + + # Image is printed left-to-right. Generally, we expect image width to be larger + # than image height, because that's the usual sticker aspect ratio. + image = Image.open(image) + assert image.width > image.height, "Are you sure image rotation is right?" + assert image.height <= max_height_px, f"Image height too big for {model}" + + printer = PrinterClient(transport) + printer.print_image(image, density=density) + + +if __name__ == "__main__": + print_cmd() diff --git a/niimprint/niimbotpacket.py b/niimprint/packet.py similarity index 71% rename from niimprint/niimbotpacket.py rename to niimprint/packet.py index 951520d..30cf078 100644 --- a/niimprint/niimbotpacket.py +++ b/niimprint/packet.py @@ -5,11 +5,11 @@ class NiimbotPacket: @classmethod def from_bytes(cls, pkt): - assert pkt[:2] == b'\x55\x55' - assert pkt[-2:] == b'\xaa\xaa' + assert pkt[:2] == b"\x55\x55" + assert pkt[-2:] == b"\xaa\xaa" type_ = pkt[2] len_ = pkt[3] - data = pkt[4:4+len_] + data = pkt[4 : 4 + len_] checksum = type_ ^ len_ for i in data: @@ -22,7 +22,9 @@ class NiimbotPacket: checksum = self.type ^ len(self.data) for i in self.data: checksum ^= i - return bytes((0x55, 0x55, self.type, len(self.data), *self.data, checksum, 0xaa, 0xaa)) + return bytes( + (0x55, 0x55, self.type, len(self.data), *self.data, checksum, 0xAA, 0xAA) + ) def __repr__(self): return f"" diff --git a/niimprint/printencoder.py b/niimprint/printencoder.py deleted file mode 100644 index 09be883..0000000 --- a/niimprint/printencoder.py +++ /dev/null @@ -1,29 +0,0 @@ -import PIL.Image as Image -import PIL.ImageOps as ImageOps -import struct -import niimbotpacket -import sys - -if sys.version_info.minor >= 10: - def countbitsofbytes(data): - return int.from_bytes(data, 'big').bit_count() -else: - def countbitsofbytes(data): - n = int.from_bytes(data, 'big') - # https://stackoverflow.com/a/9830282 - n = (n & 0x55555555) + ((n & 0xAAAAAAAA) >> 1) - n = (n & 0x33333333) + ((n & 0xCCCCCCCC) >> 2) - n = (n & 0x0F0F0F0F) + ((n & 0xF0F0F0F0) >> 4) - n = (n & 0x00FF00FF) + ((n & 0xFF00FF00) >> 8) - n = (n & 0x0000FFFF) + ((n & 0xFFFF0000) >> 16) - return n - -def naive_encoder(img): - img_data = ImageOps.invert(img.convert("L")).convert("1").tobytes() - - for x in range(img.height): - line_data = img_data[x*12:(x+1)*12] - counts = ( countbitsofbytes(line_data[i*4:(i+1)*4]) for i in range(3) ) - header = struct.pack('>H3BB', x, *counts, 1) - pkt = niimbotpacket.NiimbotPacket(0x85, header+line_data) - yield pkt diff --git a/niimprint/printer.py b/niimprint/printer.py new file mode 100644 index 0000000..dcd2d07 --- /dev/null +++ b/niimprint/printer.py @@ -0,0 +1,289 @@ +import abc +import enum +import logging +import math +import socket +import struct +import time + +import serial +from PIL import Image, ImageOps +from serial.tools.list_ports import comports as list_comports + +from niimprint.packet import NiimbotPacket + + +class InfoEnum(enum.IntEnum): + DENSITY = 1 + PRINTSPEED = 2 + LABELTYPE = 3 + LANGUAGETYPE = 6 + AUTOSHUTDOWNTIME = 7 + DEVICETYPE = 8 + SOFTVERSION = 9 + BATTERY = 10 + DEVICESERIAL = 11 + HARDVERSION = 12 + + +class RequestCodeEnum(enum.IntEnum): + GET_INFO = 64 # 0x40 + GET_RFID = 26 # 0x1A + HEARTBEAT = 220 # 0xDC + SET_LABEL_TYPE = 35 # 0x23 + SET_LABEL_DENSITY = 33 # 0x21 + START_PRINT = 1 # 0x01 + END_PRINT = 243 # 0xF3 + START_PAGE_PRINT = 3 # 0x03 + END_PAGE_PRINT = 227 # 0xE3 + ALLOW_PRINT_CLEAR = 32 # 0x20 + SET_DIMENSION = 19 # 0x13 + SET_QUANTITY = 21 # 0x15 + GET_PRINT_STATUS = 163 # 0xA3 + + +def _packet_to_int(x): + return int.from_bytes(x.data, "big") + + +class BaseTransport(metaclass=abc.ABCMeta): + @abc.abstractmethod + def read(self, length: int) -> bytes: + raise NotImplementedError + + @abc.abstractmethod + def write(self, data: bytes): + raise NotImplementedError + + +class BluetoothTransport(BaseTransport): + def __init__(self, address: str): + self._sock = socket.socket( + socket.AF_BLUETOOTH, + socket.SOCK_STREAM, + socket.BTPROTO_RFCOMM, + ) + self._sock.connect((address, 1)) + + def read(self, length: int) -> bytes: + return self._sock.recv(length) + + def write(self, data: bytes): + return self._sock.send(data) + + +class SerialTransport(BaseTransport): + def __init__(self, port: str = "auto"): + port = port if port != "auto" else self._detect_port() + self._serial = serial.Serial(port=port, baudrate=115200, timeout=0.5) + + def _detect_port(self): + all_ports = list(list_comports()) + if len(all_ports) == 0: + raise RuntimeError("No serial ports detected") + if len(all_ports) > 1: + msg = "Too many serial ports, please select one via SERIAL_PORT env var:" + for port, desc, hwid in all_ports: + msg += f"\n- {port} : {desc} [{hwid}]" + raise RuntimeError(msg) + return all_ports[0][0] + + def read(self, length: int) -> bytes: + return self._serial.read(length) + + def write(self, data: bytes): + return self._serial.write(data) + + +class PrinterClient: + def __init__(self, transport): + self._transport = transport + self._packetbuf = bytearray() + + def print_image(self, image: Image, density: int = 3): + # Internally, we're slicing the image vertically, so it's convenient to rotate + image = image.transpose(Image.ROTATE_270) + self.set_label_density(density) + self.set_label_type(1) + self.start_print() + # self.allow_print_clear() # Something unsupported in protocol decoding (B21) + self.start_page_print() + self.set_dimension(image.height, image.width) + # self.set_quantity(1) # Same thing (B21) + for pkt in self._encode_image(image): + self._send(pkt) + self.end_page_print() + while not self.end_print(): + time.sleep(0.1) + + def _encode_image(self, image: Image): + img = ImageOps.invert(image.convert("L")).convert("1") + for y in range(img.height): + line_data = [img.getpixel((x, y)) for x in range(img.width)] + line_data = "".join("0" if pix == 0 else "1" for pix in line_data) + line_data = int(line_data, 2).to_bytes(math.ceil(img.width / 8), "big") + counts = (0, 0, 0) # It seems like you can always send zeros + header = struct.pack(">H3BB", y, *counts, 1) + pkt = NiimbotPacket(0x85, header + line_data) + yield pkt + + def _recv(self): + packets = [] + self._packetbuf.extend(self._transport.read(1024)) + while len(self._packetbuf) > 4: + pkt_len = self._packetbuf[3] + 7 + if len(self._packetbuf) >= pkt_len: + packet = NiimbotPacket.from_bytes(self._packetbuf[:pkt_len]) + self._log_buffer(" ", packet.to_bytes()) + packets.append(packet) + del self._packetbuf[:pkt_len] + return packets + + def _send(self, packet): + self._transport.write(packet.to_bytes()) + + def _log_buffer(self, prefix: str, buff: bytes): + msg = ":".join(f"{i:#04x}"[-2:] for i in buff) + logging.debug(msg) + + def _transceive(self, reqcode, data, respoffset=1): + respcode = respoffset + reqcode + packet = NiimbotPacket(reqcode, data) + self._log_buffer("-->", packet.to_bytes()) + self._send(packet) + resp = None + for _ in range(6): + for packet in self._recv(): + if packet.type == 219: + raise ValueError + elif packet.type == 0: + raise NotImplementedError + elif packet.type == respcode: + resp = packet + if resp: + return resp + time.sleep(0.1) + return resp + + def get_info(self, key): + if packet := self._transceive(RequestCodeEnum.GET_INFO, bytes((key,)), key): + match key: + case InfoEnum.DEVICESERIAL: + return packet.data.hex() + case InfoEnum.SOFTVERSION: + return _packet_to_int(packet) / 100 + case InfoEnum.HARDVERSION: + return _packet_to_int(packet) / 100 + case _: + return _packet_to_int(packet) + else: + return None + + def get_rfid(self): + packet = self._transceive(RequestCodeEnum.GET_RFID, b"\x01") + data = packet.data + + if data[0] == 0: + return None + uuid = data[0:8].hex() + idx = 8 + + barcode_len = data[idx] + idx += 1 + barcode = data[idx : idx + barcode_len].decode() + + idx += barcode_len + serial_len = data[idx] + idx += 1 + serial = data[idx : idx + serial_len].decode() + + idx += serial_len + total_len, used_len, type_ = struct.unpack(">HHB", data[idx:]) + return { + "uuid": uuid, + "barcode": barcode, + "serial": serial, + "used_len": used_len, + "total_len": total_len, + "type": type_, + } + + def heartbeat(self): + packet = self._transceive(RequestCodeEnum.HEARTBEAT, b"\x01") + closingstate = None + powerlevel = None + paperstate = None + rfidreadstate = None + + match len(packet.data): + case 20: + paperstate = packet.data[18] + rfidreadstate = packet.data[19] + case 13: + closingstate = packet.data[9] + powerlevel = packet.data[10] + paperstate = packet.data[11] + rfidreadstate = packet.data[12] + case 19: + closingstate = packet.data[15] + powerlevel = packet.data[16] + paperstate = packet.data[17] + rfidreadstate = packet.data[18] + case 10: + closingstate = packet.data[8] + powerlevel = packet.data[9] + rfidreadstate = packet.data[8] + case 9: + closingstate = packet.data[8] + + return { + "closingstate": closingstate, + "powerlevel": powerlevel, + "paperstate": paperstate, + "rfidreadstate": rfidreadstate, + } + + def set_label_type(self, n): + assert 1 <= n <= 3 + packet = self._transceive(RequestCodeEnum.SET_LABEL_TYPE, bytes((n,)), 16) + return bool(packet.data[0]) + + def set_label_density(self, n): + assert 1 <= n <= 5 # B21 has 5 levels, not sure for D11 + packet = self._transceive(RequestCodeEnum.SET_LABEL_DENSITY, bytes((n,)), 16) + return bool(packet.data[0]) + + def start_print(self): + packet = self._transceive(RequestCodeEnum.START_PRINT, b"\x01") + return bool(packet.data[0]) + + def end_print(self): + packet = self._transceive(RequestCodeEnum.END_PRINT, b"\x01") + return bool(packet.data[0]) + + def start_page_print(self): + packet = self._transceive(RequestCodeEnum.START_PAGE_PRINT, b"\x01") + return bool(packet.data[0]) + + def end_page_print(self): + packet = self._transceive(RequestCodeEnum.END_PAGE_PRINT, b"\x01") + return bool(packet.data[0]) + + def allow_print_clear(self): + packet = self._transceive(RequestCodeEnum.ALLOW_PRINT_CLEAR, b"\x01", 16) + return bool(packet.data[0]) + + def set_dimension(self, w, h): + packet = self._transceive( + RequestCodeEnum.SET_DIMENSION, struct.pack(">HH", w, h) + ) + return bool(packet.data[0]) + + def set_quantity(self, n): + packet = self._transceive(RequestCodeEnum.SET_QUANTITY, struct.pack(">H", n)) + return bool(packet.data[0]) + + def get_print_status(self): + packet = self._transceive(RequestCodeEnum.GET_PRINT_STATUS, b"\x01", 16) + page, progress1, progress2 = struct.unpack(">HBB", packet.data) + return {"page": page, "progress1": progress1, "progress2": progress2} diff --git a/niimprint/printerclient.py b/niimprint/printerclient.py deleted file mode 100644 index 35189ee..0000000 --- a/niimprint/printerclient.py +++ /dev/null @@ -1,195 +0,0 @@ -import niimbotpacket -import socket -import struct -import time -import enum - -class InfoEnum(enum.IntEnum): - DENSITY = 1 - PRINTSPEED = 2 - LABELTYPE = 3 - LANGUAGETYPE = 6 - AUTOSHUTDOWNTIME = 7 - DEVICETYPE = 8 - SOFTVERSION = 9 - BATTERY = 10 - DEVICESERIAL = 11 - HARDVERSION = 12 - -class RequestCodeEnum(enum.IntEnum): - GET_INFO = 64 - GET_RFID = 26 - HEARTBEAT = 220 - SET_LABEL_TYPE = 35 - SET_LABEL_DENSITY = 33 - START_PRINT = 1 - END_PRINT = 243 - START_PAGE_PRINT = 3 - END_PAGE_PRINT = 227 - ALLOW_PRINT_CLEAR = 32 - SET_DIMENSION = 19 - SET_QUANTITY = 21 - GET_PRINT_STATUS = 163 - -_packet_to_int = lambda x: int.from_bytes(x.data, 'big') - -# TODO REMOVE MAGIC NUMBER -class PrinterClient: - def __init__(self, address): - self._sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM) - self._sock.connect((address, 1)) - self._packetbuf = bytearray() - - def _recv(self): - packets = [] - self._packetbuf.extend(self._sock.recv(1024)) - while len(self._packetbuf) > 4: - pkt_len = self._packetbuf[3] + 7 - if len(self._packetbuf) >= pkt_len: - packet = niimbotpacket.NiimbotPacket.from_bytes(self._packetbuf[:pkt_len]) - # print('recv:',packet) - packets.append(packet) - del self._packetbuf[:pkt_len] - return packets - - def _send(self, packet): - # print('send:',packet) - self._sock.send(packet.to_bytes()) - - def _transceive(self, reqcode, data, respoffset=1): - respcode = respoffset + reqcode - self._send(niimbotpacket.NiimbotPacket(reqcode, data)) - resp = None - for _ in range(6): - for packet in self._recv(): - if packet.type == 219: - raise ValueError - elif packet.type == 0: - raise NotImplementedError - elif packet.type == respcode: - resp = packet - if resp: - return resp - time.sleep(0.1) - return resp - - def get_info(self, key): - if packet := self._transceive(RequestCodeEnum.GET_INFO, bytes((key,)), key): - match key: - case InfoEnum.DEVICESERIAL: - return packet.data.hex() - case InfoEnum.SOFTVERSION: - return _packet_to_int(packet) / 100 - case InfoEnum.HARDVERSION: - return _packet_to_int(packet) / 100 - case _: - return _packet_to_int(packet) - else: - return None - - def get_rfid(self): - packet = self._transceive(RequestCodeEnum.GET_RFID, b'\x01') - data = packet.data - - if data[0] == 0: - return None - uuid = data[0:8].hex() - idx = 8 - - barcode_len = data[idx] - idx += 1 - barcode = data[idx:idx+barcode_len].decode() - - idx += barcode_len - serial_len = data[idx] - idx += 1 - serial = data[idx:idx+serial_len].decode() - - idx += serial_len - total_len, used_len, type_ = struct.unpack('>HHB', data[idx:]) - return { - 'uuid': uuid, - 'barcode': barcode, - 'serial': serial, - 'used_len': used_len, - 'total_len': total_len, - 'type': type_ - } - - def heartbeat(self): - packet = self._transceive(RequestCodeEnum.HEARTBEAT, b'\x01') - closingstate = None - powerlevel = None - paperstate = None - rfidreadstate = None - - match len(packet.data): - case 20: - paperstate = packet.data[18] - rfidreadstate = packet.data[19] - case 13: - closingstate = packet.data[9] - powerlevel = packet.data[10] - paperstate = packet.data[11] - rfidreadstate = packet.data[12] - case 19: - closingstate = packet.data[15] - powerlevel = packet.data[16] - paperstate = packet.data[17] - rfidreadstate = packet.data[18] - case 10: - closingstate = packet.data[8] - powerlevel = packet.data[9] - rfidreadstate = packet.data[8] - case 9: - closingstate = packet.data[8] - - return { - 'closingstate': closingstate, - 'powerlevel': powerlevel, - 'paperstate': paperstate, - 'rfidreadstate': rfidreadstate - } - - def set_label_type(self, n): - assert 1 <= n <= 3 - packet = self._transceive(RequestCodeEnum.SET_LABEL_TYPE, bytes((n,)), 16) - return bool(packet.data[0]) - - def set_label_density(self, n): - assert 1 <= n <= 3 - packet = self._transceive(RequestCodeEnum.SET_LABEL_DENSITY, bytes((n,)), 16) - return bool(packet.data[0]) - - def start_print(self): - packet = self._transceive(RequestCodeEnum.START_PRINT, b'\x01') - return bool(packet.data[0]) - - def end_print(self): - packet = self._transceive(RequestCodeEnum.END_PRINT, b'\x01') - return bool(packet.data[0]) - - def start_page_print(self): - packet = self._transceive(RequestCodeEnum.START_PAGE_PRINT, b'\x01') - return bool(packet.data[0]) - - def end_page_print(self): - packet = self._transceive(RequestCodeEnum.END_PAGE_PRINT, b'\x01') - return bool(packet.data[0]) - - def allow_print_clear(self): - packet = self._transceive(RequestCodeEnum.ALLOW_PRINT_CLEAR, b'\x01', 16) - return bool(packet.data[0]) - - def set_dimension(self, w, h): - packet = self._transceive(RequestCodeEnum.SET_DIMENSION, struct.pack('>HH', w, h)) - return bool(packet.data[0]) - - def set_quantity(self, n): - packet = self._transceive(RequestCodeEnum.SET_QUANTITY, struct.pack('>H', n)) - return bool(packet.data[0]) - - def get_print_status(self): - packet = self._transceive(RequestCodeEnum.GET_PRINT_STATUS, b'\x01', 16) - page, progress1, progress2 = struct.unpack('>HBB', packet.data) - return {'page': page, 'progress1': progress1, 'progress2': progress2} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..c700401 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,112 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "pillow" +version = "10.1.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Pillow-10.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106"}, + {file = "Pillow-10.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273"}, + {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666"}, + {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2"}, + {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593"}, + {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db"}, + {file = "Pillow-10.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f"}, + {file = "Pillow-10.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818"}, + {file = "Pillow-10.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57"}, + {file = "Pillow-10.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7"}, + {file = "Pillow-10.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7"}, + {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610"}, + {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839"}, + {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172"}, + {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061"}, + {file = "Pillow-10.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262"}, + {file = "Pillow-10.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992"}, + {file = "Pillow-10.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a"}, + {file = "Pillow-10.1.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b"}, + {file = "Pillow-10.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d"}, + {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27"}, + {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312"}, + {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de"}, + {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651"}, + {file = "Pillow-10.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b"}, + {file = "Pillow-10.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f"}, + {file = "Pillow-10.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996"}, + {file = "Pillow-10.1.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793"}, + {file = "Pillow-10.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e"}, + {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2"}, + {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a"}, + {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01"}, + {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d"}, + {file = "Pillow-10.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80"}, + {file = "Pillow-10.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212"}, + {file = "Pillow-10.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14"}, + {file = "Pillow-10.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099"}, + {file = "Pillow-10.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616"}, + {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb"}, + {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219"}, + {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34"}, + {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd"}, + {file = "Pillow-10.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28"}, + {file = "Pillow-10.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2"}, + {file = "Pillow-10.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256"}, + {file = "Pillow-10.1.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7"}, + {file = "Pillow-10.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba"}, + {file = "Pillow-10.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4"}, + {file = "Pillow-10.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9"}, + {file = "Pillow-10.1.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e"}, + {file = "Pillow-10.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412"}, + {file = "Pillow-10.1.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b"}, + {file = "Pillow-10.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f"}, + {file = "Pillow-10.1.0.tar.gz", hash = "sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "pyserial" +version = "3.5" +description = "Python Serial Port Extension" +optional = false +python-versions = "*" +files = [ + {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, + {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, +] + +[package.extras] +cp2110 = ["hidapi"] + +[metadata] +lock-version = "2.0" +python-versions = "~3.11" +content-hash = "cac430413d223c6dc9dc0c27f33322b70a134eb2ff5709934235a261451e419c" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fbebee9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "niimprint" +version = "0.1.0" +description = "" +authors = [] + +[tool.poetry.dependencies] +python = "~3.11" +pyserial = "^3.5" +pillow = "^10.1.0" +click = "^8.1.7" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +line-length = 88 +target-version = "py311" + +[tool.ruff.lint] +select = ["I", "F", "E"] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..f2e6fae --- /dev/null +++ b/readme.md @@ -0,0 +1,52 @@ +# `niimprint` — Experimental Niimbot Printer Client + +**Fork changelog & differences from original version:** + +- Tested on Niimbot B21 and Python 3.11. Niimbot D11 support may be broken! +- Added transport abstraction: switch between bluetooth and USB (serial) +- Switched to [click](https://click.palletsprojects.com/) CLI library instead of argparse +- Integrated [pyproject.toml](https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/) and [poetry](https://python-poetry.org) +- Integrated [pre-commit](https://pre-commit.com/) and [ruff](https://docs.astral.sh/ruff/), re-formatted all files +- Miscellaneous refactoring / file renaming / etc. + +## Installation + +Recommended method is to use [poetry](https://python-poetry.org) and install with `poetry install`. However `requirements.txt` is also provided for convenience. Project is tested on Python 3.11, but should work on other versions. + +## Usage + +``` +$ python niimprint --help + +Usage: niimprint [OPTIONS] + +Options: + -m, --model [b21|d11] Niimbot printer model [default: b21] + -c, --conn [usb|bluetooth] Connection type [default: usb] + -a, --addr TEXT Bluetooth MAC address OR serial device path + -d, --density INTEGER RANGE Print density [default: 5; 1<=x<=5] + -i, --image PATH Image path [required] + --help Show this message and exit. +``` + +## Examples + +**B21, USB connection, 30x15 mm (240x120 px) label.** On linux, you can try to omit `--addr` option and let the script to auto-detect the serial port (it will fail if there're multiple available ports). On windows, serial ports will be named like `COM1`, `COM2` etc. (check in device manager). + +``` +python niimprint --addr /dev/ttyACM0 --image examples/B21_30x15mm_240x120px.png +``` + +**B21, bluetooth connection:** untested (todo). + +**D11:** completly untested, however original fork supports it. If you have D11 at hand and willing to test, please open an issue! + +## Image vs Label size + +### Niimbot B21 + +According to my observations, B21 has **8 pixels per mm** resolution. But there's one catch: printer specs say it supports up to 50mm-wide labels (which sould translate to `50 * 8 = 400` pixels). However trying to print anything larger than `384` pixels resulted in error. My guess it's the actual hardware limit, which is "almost equal" to stated 50 mm. + +### Niimbot D11 + +??? diff --git a/requirements.txt b/requirements.txt index 0799398..0964750 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ -Pillow >= 8.0 +click==8.1.7 +pillow==10.1.0 +pyserial==3.5