Pythonからサーマルプリンタ「PAPERANG」の印刷できました（Windows10、Python3.6）

お待たせしました！
Pythonからサーマルプリンタ「PAPERANG」の印刷できました。

# サンプルコード
[ここ](https://github.com/SatoshiRobatoFujimoto/miaomiaoji-tool.git)からダウンロードしてください

# 環境構築
~~~
$ conda create -n paperang python=3.6
$ conda activate paperang
$ cd miaomiaoji-tool
$ pip install PyBluez-win10
$ pip install opencv-python
~~~

# 実行
~~~
$ python message_process.py
~~~

# 解説
[ここ](https://github.com/ihciah/miaomiaoji-tool)からフォークしたものを変更してます。

message_process.pyを実行してみるとfilter関数でエラーが出ます。47行目と64行目のfilter関数をlistでくくります。[Python3におけるmap/filterの使い方](http://hiroto1979.hatenablog.jp/entry/2016/02/24/020534)

~~~python 
valid_devices = list(filter(lambda d: len(d) == 2 and d[1] in valid_names, nearby_devices))
・・・
valid_service = list(filter(
    lambda s: 'protocol' in s and 'name' in s and s['protocol'] == 'RFCOMM' and s['name'] == 'SerialPort',
    service_matches
))
~~~ 

pybluezだと63行目のfind_serviceのところでOSErrorが出るため、PyBluez-win10を用いています。[※OSError when program try to use functions from bluetooth._msbt #279](https://github.com/pybluez/pybluez/issues/279)

~~~python
service_matches = find_service(uuid=self.uuid, address=self.address)
~~~

サービス名が見つからないエラーが出るので、service_matchesをprintして表示してみましょう。

~~~
[{'host': '00:15:83:B7:11:AF', 'name': b'SerialPort', 'description': '', 'port': 1, 'protocol': 'RFCOMM', 'rawrecord': b'5J\t\x00\x00\n\x00\x01\x00\x0f\t\x00\x015\x03\x19\x11\x01\t\x00\x045\x0c5\x03\x19\x01\x005\x05\x19\x00\x03\x08\x01\t\x00\x055\x03\x19\x10\x02\t\x00\x08\x08\xff\t\x00\t5\x085\x06\x19\x11\x01\t\x01\x02\t\x01\x00%\nSerialPort', 'service-classes': [b'1101'], 'profiles': [(b'1101', 258)], 'provider': None, 'service-id': None, 'handle': 65551}, {'host': '00:15:83:B7:11:AF', 'name': b'WeChat', 'description': '', 'port': 8, 'protocol': 'RFCOMM', 'rawrecord': b'5T\t\x00\x00\n\x00\x01\x00\x0e\t\x00\x015\x11\x1c\xe5\xb1R\xedkF\t\xe9Fxf^\x9a\x97,\xbc\t\x00\x045\x0c5\x03\x19\x01\x005\x05\x19\x00\x03\x08\x08\t\x00\x055\x03\x19\x10\x02\t\x00\x08\x08\xff\t\x00\t5\x085\x06\x19\x11\x01\t\x01\x02\t\x01\x00%\x06WeChat', 'service-classes': ['E5B152ED-6B46-09E9-4678-665E9A972CBC'], 'profiles': [(b'1101', 258)], 'provider': None, 'service-id': None, 'handle': 65550}]
~~~

'name': b'SerialPort'になっているため、65行目の'SerialPort'にbを付けます。

~~~python
lambda s: 'protocol' in s and 'name' in s and s['protocol'] == 'RFCOMM' and s['name'] == b'SerialPort',
~~~

あとservice_matchesをprintしたときに、下記のstruct.packのエラーが出ます。

~~~
 File "message_process.py", line 88, in packPerBytes
    result += struct.pack('<i', self.crc32(bytes))
struct.error: argument out of range
~~~

88行目の <i を <I に変更しましょう。[struct — Interpret bytes as packed binary data](https://docs.python.org/3/library/struct.html)

~~~python
result += struct.pack('<I', self.crc32(bytes))
~~~

次にencodeのエラーが出るため、

~~~
File "message_process.py", line 110, in recv
    logging.info("Recv: " + raw_msg.encode('hex'))
AttributeError: 'bytes' object has no attribute 'encode'
~~~

110行目と122行目の.encode('hex')を.hex()に変えます。[How do I fix AttributeError: 'bytes' object has no attribute 'encode'?](https://stackoverflow.com/questions/55701623/how-do-i-fix-attributeerror-bytes-object-has-no-attribute-encode)

~~~python
logging.info("Recv: " + raw_msg.hex())
・・・
, self.payload_length, self.payload.hex()
~~~

image_process.pyでmap関数のエラーが出るため、32行目のmap関数をlistでくくります。

~~~
File "\miaomiaoji-tool\image_process.py", line 20, in frombits
    for b in range(len(bits) / 8):
TypeError: object of type 'map' has no len()
~~~

また、20行目のfor文のrangeの範囲がfloatになっているので、intにキャストします。

~~~python
for b in range(int(len(bits) / 8)):
~~~

153行目のstruct.packでエラーが出るので、コメントアウトしてOKです。

~~~
# msg = struct.pack("<%dc" % len(binary_img, *binary_img)
~~~

300行のラインを印刷するには、バイナリにする必要があります。sendImageToBtの中身はmsgにそのままバイナリの文字列を入れています。

~~~python 
def sendImageToBt(self, binary_img):
    self.sendPaperTypeToBt()
    # msg = struct.pack("<%dc" % len(binary_img, *binary_img)
    msg = binary_img
    self.sendToBt(msg, BtCommandByte.PRT_PRINT_DATA, need_reply=False)
    self.sendFeedLineToBt(self.padding_line)

# Print a pure black image with 300 lines
img = b'\xff' * 48 * 300
mmj.sendImageToBt(img)
~~~

印刷できました。
![86242649_182835276314024_4717528697094012928_n.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/70a2ecaf-1625-4fd3-ef42-cdf02f69eba4.jpeg)

実は、self.sendToBtには各lineごとに送信する必要があります。

~~~python 
def sendImageToBt(self, binary_img):
    self.sendPaperTypeToBt()
    # msg = struct.pack("<%dc" % len(binary_img, *binary_img)
    msgs = [binary_img[x: x+192] for x in range(0, len(binary_img), 192)] # 4*48
    for msg in msgs:
        self.sendToBt(msg, BtCommandByte.PRT_PRINT_DATA, need_reply=False)
    self.sendFeedLineToBt(self.padding_line)
~~~

これが本当の300行のラインです。sendBinaryToBtという関数名に変えました。

![86189532_333920134236910_8458868322023243776_n.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/afc0495a-6ed0-42c4-1808-084519c8eb5f.jpeg)

\xffが黒で、\x00が白になります。\xffを二進数にするとわかりやすいのですが、\xffは0b11111111となり8本の黒い縦のラインが引かれることになります。なので、8ビットで画像の8ピクセル分を表現することになります。横のラインの幅は'\xff'が48個分までです。つまり、画像幅は8×48=384ピクセルになります。

画像をリサイズ、二値化し、[0,1]の値に変換、8ピクセルずつ16進数に変換し、各行ごとにプリントします。

~~~python 
def sendImageToBt(self, binary_img):
    self.sendPaperTypeToBt()
    height, width = binary_img.shape[:]
    for line in range(height):
        bits = [0 if x > 0 else 1 for x in binary_img[line]]
        bits = [bits[x:x+8] for x in range(0, len(bits), 8)]
        msg = ''
        for bit in bits:
            bin = '0b'+''.join(str(x) for x in bit)
            msg += '{:02x}'.format(int(bin, 0))
        msg = bytes.fromhex(msg)
        self.sendToBt(msg, BtCommandByte.PRT_PRINT_DATA, need_reply=False)
    self.sendFeedLineToBt(self.padding_line)

・・・
# Print an existing image(need opencv):
img = cv2.imread('kumamcn.png', 0)
ret, binary_img = cv2.threshold(img,127,255,cv2.THRESH_BINARY)
height, width = binary_img.shape[:]
binary_img = cv2.resize(binary_img, (384, int(height*384.0/width)), cv2.INTER_AREA)
mmj.sendImageToBt(binary_img)
~~~

完成です。これで任意の画像を印刷できます！
![86259996_614941075716724_5170025822672650240_n.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63863/4fbd771c-74ac-92a4-3811-3e5d57eb403b.jpeg)

[この記事が役に立ったらガチ本に生牡蠣をおごってください。](https://www.paypal.com/paypalme2/gachimoto)