kjy00302 / niimprint

(WIP) Niimbot printer client. Tested on D11.
MIT License
96 stars 52 forks source link

Add USB connection support #8

Open kjy00302 opened 1 year ago

kjy00302 commented 1 year ago

D11 (and other devices maybe?) actually supports USB connection (as CDC ACM serial device, and uses same packet). But currently niimprint is hard-coded to use Bluetooth serial.

AndBondStyle commented 1 year ago

Succesfully tested printing via USB on B21 (ref #4). Below is the summary of what I changed, tell me if you want a PR.

Created separate transport classes for bluetooth and serial (based on pyserial library). You must create instance of either BluetoothTransport or SerialTransport and pass it to PrinterClient constructor.

Transport classes ```python ... from serial.tools.list_ports import comports import serial 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.1) def _detect_port(self): all_ports = list(comports()) if len(all_ports) > 1: raise RuntimeError("Too many serial ports") 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): ... ```

Changed printencoder.naive_encoder function to be less hard-coded for D11. Honestly, I'm not sure how image encoding works on D11, because you were using the raw img.convert("1").tobytes() which (according to docs) uses 1-byte per pixel encoding. On B21 however, it's 1-bit per pixel. Also didn't get the idea of 3 magic numbers with "bit count", tried just always sending 3 zeros and it works fine.

Print encoder ```python def naive_encoder(img: Image.Image): img = ImageOps.invert(img.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.NiimbotPacket(0x85, header + line_data) yield pkt ```

P.S. I also think PrinterClient should have print_image method, wich should contain both native_encoder logic inside it and the print sequence (which is in __main__.py right now)

iROOT commented 11 months ago

@AndBondStyle Can you fork the repository and post code changes to support USB?

AndBondStyle commented 11 months ago

@iROOT here you go. However I only tested it with Niimbot B21. Do you have B21 or D11? https://github.com/AndBondStyle/niimprint

iROOT commented 11 months ago

@AndBondStyle Thank you. I have B1. This is a complete analogue of the B21, only with a different body design. While it has not been possible to print, at startup the tape unwinds a centimeter inward and returns back. This does not cause any error. I'll try to debug it later.

AndBondStyle commented 11 months ago

@iROOT hmm... I've got the same problem when trying to print an image with wrong resolution. What resolution are you using? Also, I disabled some checksum-related code in image encoding function (originally it was here), but for my B21 it works fine.

Would you care to take same wireshark / usbpcap captures of USB traffic? You need a windows machine and niimbot official app for that, and I can guide you through the process.

iROOT commented 11 months ago

@AndBondStyle I now used 8 pixels per millimeter and everything started to work out. Here is 30x20s with third party tape. There is some downward shift in the pattern, I think due to calibration. At the beginning it printed with only the top part of the image and rotated it, removed the 270 degree rotation and the image became full width. Then I found that if you add a delay of 0.3 seconds in the print_image function after self.end_page_print(), then the picture is completely printed. Accordingly, I think the delay should be proportional to the amount of data, it needs to be calculated somehow. B1_30x20mm_240x120px Print_30x20_with_delays_0 0_0 1_0 2_0 3

I got a dump via Wireshark. Here it captures before the Niimbot program starts and stops after it finishes. B1_dump_print_30x20_240x160.zip

By the way, I noticed that the printer does not care for which original tape the NTAG is inserted. It still prints what is sent to it.

AndBondStyle commented 11 months ago

@iROOT thank you for the capture. The problem with alignment was caused by me naively assuming that label length (aligned with printing direction) is always bigger than its height. This is the case for 30x15 and 80x50 labels, but clearly not for 30x20 labels. That means if you rotated your image originally by 90 degrees it would've probably printed fine. I'll try to point it out more clearly in the readme and add some extra args to CLI.

Regarding the delay after end_page_print, yes, it's a bit dirty right now, I will look into that.

P.S. Already updated the repo with more detailed readme