kjy00302/niimprint

Add USB connection support

kjy00302 opened this issue · 7 comments

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.

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
...
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
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

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

@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

@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.

@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

@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.

@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