Skip to content

Commit b5e610e

Browse files
committed
code for new guide
1 parent dcd5f0d commit b5e610e

7 files changed

Lines changed: 354 additions & 0 deletions

File tree

30.6 KB
Binary file not shown.
18.4 KB
Binary file not shown.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import seekablebitmap
2+
3+
import gc
4+
import os
5+
import struct
6+
import time
7+
8+
import board
9+
import digitalio
10+
import keypad
11+
import ulab.numpy as np
12+
13+
import adafruit_ble
14+
from adafruit_ble import BLERadio
15+
from adafruit_ble.advertising import Advertisement
16+
17+
from thermalprinter import CatPrinter
18+
from seekablebitmap import imageopen
19+
20+
ble = BLERadio() # pylint: disable=no-member
21+
22+
buttons = keypad.Keys([board.BUTTON_A, board.BUTTON_B], value_when_pressed=False)
23+
24+
def pnmopen(filename):
25+
"""
26+
Scan for netpbm format info, skip over comments, and read header data.
27+
28+
Return the format, header, and the opened file positioned at the start of
29+
the bitmap data.
30+
"""
31+
# pylint: disable=too-many-branches
32+
image_file = open(filename, "rb")
33+
magic_number = image_file.read(2)
34+
image_file.seek(2)
35+
pnm_header = []
36+
next_value = bytearray()
37+
while True:
38+
# We have all we need at length 3 for formats P2, P3, P5, P6
39+
if len(pnm_header) == 3:
40+
return image_file, magic_number, pnm_header
41+
42+
if len(pnm_header) == 2 and magic_number in [b"P1", b"P4"]:
43+
return image_file, magic_number, pnm_header
44+
45+
next_byte = image_file.read(1)
46+
if next_byte == b"":
47+
raise RuntimeError("Unsupported image format {}".format(magic_number))
48+
if next_byte == b"#": # comment found, seek until a newline or EOF is found
49+
while image_file.read(1) not in [b"", b"\n"]: # EOF or NL
50+
pass
51+
elif not next_byte.isdigit(): # boundary found in header data
52+
if next_value:
53+
# pull values until space is found
54+
pnm_header.append(int("".join(["%c" % char for char in next_value])))
55+
next_value = bytearray() # reset the byte array
56+
else:
57+
next_value += next_byte # push the digit into the byte array
58+
59+
def wait_for_press(kbd):
60+
"""
61+
Wait for a keypress and return the event
62+
"""
63+
while True:
64+
event = kbd.events.get()
65+
if event and event.pressed:
66+
return event
67+
68+
def show(s):
69+
"""
70+
Display a message on the screen
71+
"""
72+
board.DISPLAY.auto_refresh = False
73+
print("\n" * 24)
74+
print(s)
75+
board.DISPLAY.auto_refresh = True
76+
77+
def show_error(s):
78+
"""
79+
Display a message on the screen and wait for a button press
80+
"""
81+
show(s + "\nPress a button to continue")
82+
wait_for_press(buttons)
83+
84+
def find_cat_printer(radio):
85+
"""
86+
Connect to the cat printer device using BLE
87+
"""
88+
while True:
89+
show("Scanning for GB02 device...")
90+
for adv in radio.start_scan(Advertisement):
91+
complete_name = getattr(adv, 'complete_name')
92+
if complete_name is not None:
93+
print(f"Saw {complete_name}")
94+
if complete_name == 'GB02':
95+
radio.stop_scan()
96+
return radio.connect(adv, timeout=10)[CatPrinter]
97+
98+
image_files = [i for i in os.listdir('/') if i.lower().endswith(".pbm") or i.lower().endswith(".bmp")]
99+
image_files.sort(key=lambda filename: filename.lower())
100+
101+
def select_image():
102+
i = 0
103+
while True:
104+
show(f"Select image file\nA: next image\nB: print this image\n\n{image_files[i]}")
105+
event = wait_for_press(buttons)
106+
if event.key_number == 0: # button "A"
107+
i = (i + 1) % len(image_files)
108+
if event.key_number == 1: # button "B"
109+
return image_files[i]
110+
111+
printer = find_cat_printer(ble)
112+
113+
while True:
114+
try:
115+
filename = select_image()
116+
117+
show(f"Loading {filename}")
118+
119+
image = imageopen(filename)
120+
if image.width != 384:
121+
raise ValueError("Invalid image. Must be 384 pixels wide")
122+
if image.bits_per_pixel != 1:
123+
raise ValueError("Invalid image. Must be 1 bit per pixel (black & white)")
124+
125+
invert_image = image.palette and image.palette[0] == 0
126+
127+
show(f"Printing {filename}")
128+
129+
for i in range(image.height):
130+
row_data = image.get_row(i)
131+
if invert_image:
132+
row_data = ~np.frombuffer(row_data, dtype=np.uint8)
133+
printer.print_bitmap_row(row_data)
134+
135+
# Print blank lines until the paper can be torn off
136+
for i in range(80):
137+
printer.print_bitmap_row(b'\0' * 48)
138+
139+
except Exception as e:
140+
show_error(str(e))
141+
image_files.remove(filename)
142+
continue
26.3 KB
Binary file not shown.
30.1 KB
Binary file not shown.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import struct
2+
import ulab
3+
4+
class SeekableBitmap:
5+
def __init__(self, image_file, width, height, bits_per_pixel, *, bytes_per_row=None, data_start=None, stride=None, palette=None):
6+
self.image_file = image_file
7+
self.width = width
8+
self.height = height
9+
self.bits_per_pixel = bits_per_pixel
10+
self.bytes_per_row = bytes_per_row if bytes_per_row else (bits_per_pixel * width + 7) // 8
11+
self.stride = stride if stride else self.bytes_per_row
12+
self.palette = palette
13+
self.data_start = data_start if data_start else image_file.tell()
14+
15+
def get_row(self, row):
16+
self.image_file.seek(self.data_start + row * self.stride)
17+
return self.image_file.read(self.bytes_per_row)
18+
19+
def _pnmopen(filename):
20+
"""
21+
Scan for netpbm format info, skip over comments, and read header data.
22+
23+
Return the format, header, and the opened file positioned at the start of
24+
the bitmap data.
25+
"""
26+
# pylint: disable=too-many-branches
27+
image_file = open(filename, "rb")
28+
magic_number = image_file.read(2)
29+
image_file.seek(2)
30+
pnm_header = []
31+
next_value = bytearray()
32+
while True:
33+
# We have all we need at length 3 for formats P2, P3, P5, P6
34+
if len(pnm_header) == 3:
35+
return image_file, magic_number, pnm_header
36+
37+
if len(pnm_header) == 2 and magic_number in [b"P1", b"P4"]:
38+
return image_file, magic_number, pnm_header
39+
40+
next_byte = image_file.read(1)
41+
if next_byte == b"":
42+
raise RuntimeError("Unsupported image format {}".format(magic_number))
43+
if next_byte == b"#": # comment found, seek until a newline or EOF is found
44+
while image_file.read(1) not in [b"", b"\n"]: # EOF or NL
45+
pass
46+
elif not next_byte.isdigit(): # boundary found in header data
47+
if next_value:
48+
# pull values until space is found
49+
pnm_header.append(int("".join(["%c" % char for char in next_value])))
50+
next_value = bytearray() # reset the byte array
51+
else:
52+
next_value += next_byte # push the digit into the byte array
53+
54+
def pnmopen(filename):
55+
image_file, magic_number, pnm_header = _pnmopen(filename)
56+
if magic_number == b'P4':
57+
return SeekableBitmap(image_file, pnm_header[0], pnm_header[1], 1, palette=b'\xff\xff\xff\x00\x00\x00\x00\x00')
58+
if magic_number == b'P5':
59+
return SeekableBitmap(image_file, pnm_header[0], pnm_header[1], pnm_header[2].bit_length())
60+
if magic_number == b'P6':
61+
return SeekableBitmap(image_file, pnm_header[0], pnm_header[1], 3*pnm_header[2].bit_length())
62+
raise ValueError(f"Unknown or unsupported magic number {magic_number}")
63+
64+
def bmpopen(filename):
65+
image_file = open(filename, "rb")
66+
67+
header = image_file.read(34)
68+
69+
data_start, header_size, width, height, planes, bits_per_pixel, method = struct.unpack("<10x4l2hl", header)
70+
71+
bits_per_pixel = bits_per_pixel if bits_per_pixel != 0 else 1
72+
73+
mask_size = (12 if header_size < 56 else 16) if method == 3 else 0
74+
palette_start = header_size + 14
75+
image_file.seek(palette_start)
76+
palette = image_file.read(4 << bits_per_pixel)
77+
78+
stride=(bits_per_pixel*width+31)//32*4
79+
if height < 0:
80+
height = -height
81+
else:
82+
data_start = data_start + stride * (height - 1)
83+
stride = -stride
84+
85+
return SeekableBitmap(image_file, width, height, bits_per_pixel, data_start=data_start, stride=stride, palette=palette)
86+
87+
def imageopen(filename):
88+
if filename.lower().endswith(".bmp"):
89+
return bmpopen(filename)
90+
return pnmopen(filename)
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Protocol information from Thermal_Printer Arduino library
2+
# https://github.com/bitbank2/Thermal_Printer/
3+
import collections
4+
import time
5+
from adafruit_ble.uuid import StandardUUID
6+
from adafruit_ble.services import Service
7+
from adafruit_ble.characteristics.stream import StreamIn
8+
9+
PrinterConfig = collections.namedtuple(
10+
"PrinterConfig", ("width", "service", "characteristic")
11+
)
12+
13+
# Switch the printing mode to bitmap
14+
printimage = b"Qx\xbe\x00\x01\x00\x00\x00\xff"
15+
# Switch the printing mode to text
16+
printtext = b"Qx\xbe\x00\x01\x00\x01\x07\xff"
17+
# Command to feed paper
18+
paperfeed = b"Qx\xa1\x00\x02\x00\x1eZ\xff\xff"
19+
20+
# this table helps compute the checksum of transmitted data
21+
# it is crc-8-ccitt
22+
checksumtable = b"\x00\x07\x0e\t\x1c\x1b\x12\x158?61$#*-pw~ylkbeHOFATSZ]\xe0\xe7\xee\xe9\xfc\xfb\xf2\xf5\xd8\xdf\xd6\xd1\xc4\xc3\xca\xcd\x90\x97\x9e\x99\x8c\x8b\x82\x85\xa8\xaf\xa6\xa1\xb4\xb3\xba\xbd\xc7\xc0\xc9\xce\xdb\xdc\xd5\xd2\xff\xf8\xf1\xf6\xe3\xe4\xed\xea\xb7\xb0\xb9\xbe\xab\xac\xa5\xa2\x8f\x88\x81\x86\x93\x94\x9d\x9a' ).;<52\x1f\x18\x11\x16\x03\x04\r\nWPY^KLEBohafst}z\x89\x8e\x87\x80\x95\x92\x9b\x9c\xb1\xb6\xbf\xb8\xad\xaa\xa3\xa4\xf9\xfe\xf7\xf0\xe5\xe2\xeb\xec\xc1\xc6\xcf\xc8\xdd\xda\xd3\xd4ing`ur{|QV_XMJCD\x19\x1e\x17\x10\x05\x02\x0b\x0c!&/(=:34NI@GRU\\[vqx\x7fjmdc>907\"%,+\x06\x01\x08\x0f\x1a\x1d\x14\x13\xae\xa9\xa0\xa7\xb2\xb5\xbc\xbb\x96\x91\x98\x9f\x8a\x8d\x84\x83\xde\xd9\xd0\xd7\xc2\xc5\xcc\xcb\xe6\xe1\xe8\xef\xfa\xfd\xf4\xf3"
23+
24+
# mirrortable[i] is the bit reversed version of the byte i
25+
mirrortable = b"\x00\x80@\xc0 \xa0`\xe0\x10\x90P\xd00\xb0p\xf0\x08\x88H\xc8(\xa8h\xe8\x18\x98X\xd88\xb8x\xf8\x04\x84D\xc4$\xa4d\xe4\x14\x94T\xd44\xb4t\xf4\x0c\x8cL\xcc,\xacl\xec\x1c\x9c\\\xdc<\xbc|\xfc\x02\x82B\xc2\"\xa2b\xe2\x12\x92R\xd22\xb2r\xf2\n\x8aJ\xca*\xaaj\xea\x1a\x9aZ\xda:\xbaz\xfa\x06\x86F\xc6&\xa6f\xe6\x16\x96V\xd66\xb6v\xf6\x0e\x8eN\xce.\xaen\xee\x1e\x9e^\xde>\xbe~\xfe\x01\x81A\xc1!\xa1a\xe1\x11\x91Q\xd11\xb1q\xf1\t\x89I\xc9)\xa9i\xe9\x19\x99Y\xd99\xb9y\xf9\x05\x85E\xc5%\xa5e\xe5\x15\x95U\xd55\xb5u\xf5\r\x8dM\xcd-\xadm\xed\x1d\x9d]\xdd=\xbd}\xfd\x03\x83C\xc3#\xa3c\xe3\x13\x93S\xd33\xb3s\xf3\x0b\x8bK\xcb+\xabk\xeb\x1b\x9b[\xdb;\xbb{\xfb\x07\x87G\xc7'\xa7g\xe7\x17\x97W\xd77\xb7w\xf7\x0f\x8fO\xcf/\xafo\xef\x1f\x9f_\xdf?\xbf\x7f\xff"
26+
27+
28+
def checksum(data, start, count):
29+
cs = 0
30+
for i in range(start, start + count):
31+
cs = checksumtable[cs ^ data[i]]
32+
return cs
33+
34+
35+
PRINTER_CAT = PrinterConfig(width=384, service=0xA3E0, characteristic=0xAE01)
36+
37+
MODE_TEXT = "MODE_TEXT"
38+
MODE_BITMAP = "MODE_BITMAP"
39+
40+
printers = {"GB02": PRINTER_CAT, "GB01": PRINTER_CAT}
41+
42+
class CatPrinter(Service):
43+
44+
uuid = StandardUUID(0xAE30)
45+
46+
_tx = StreamIn(uuid=StandardUUID(0xAE01),
47+
timeout=1.0,
48+
buffer_size=256,
49+
)
50+
51+
def _write_data(self, buf):
52+
result = self._tx.write(buf)
53+
54+
@property
55+
def bitmap_width(self):
56+
return 384
57+
58+
def __init__(self, service=None):
59+
super().__init__(service=service)
60+
self._mode = None
61+
62+
63+
@property
64+
def mode(self):
65+
return self._mode
66+
67+
@mode.setter
68+
def mode(self, value):
69+
if value == self.mode:
70+
return
71+
72+
if value == MODE_TEXT:
73+
self._write_data(printtext)
74+
elif value == MODE_BITMAP:
75+
self._write_data(printimage)
76+
else:
77+
raise ValueError("Invalid mode %r" % value)
78+
79+
self._mode = value
80+
81+
def feed_lines(self, lines):
82+
buf = bytearray(paperfeed)
83+
buf[6] = lines & 0xff
84+
buf[7] = lines >> 8
85+
buf[8] = checksum(buf, 6, 2)
86+
self._write_data(buf)
87+
88+
def _print_common(self, text, reverse_bits=True):
89+
90+
offset = 0
91+
data = memoryview(text)
92+
while data:
93+
sz = min(112, len(data))
94+
sub_data = data[:sz]
95+
data = data[sz:]
96+
buf = bytearray(sz + 8)
97+
buf[0] = 0x51
98+
buf[1] = 0x78
99+
buf[2] = 0xA2
100+
buf[3] = 0x0
101+
buf[4] = sz
102+
buf[5] = 0
103+
if reverse_bits:
104+
buf[6 : 6 + sz] = bytes(mirrortable[c] for c in sub_data)
105+
else:
106+
buf[6 : 6 + sz] = sub_data
107+
buf[6 + sz] = checksum(buf, 6, len(sub_data))
108+
buf[6 + sz + 1] = 0xFF
109+
110+
self._write_data(buf)
111+
112+
def print_text(self, text):
113+
self.mode = MODE_TEXT
114+
self._print_common(text.encode("utf-8"))
115+
116+
def print_line(self, text):
117+
self.print_text(text)
118+
self._print_common(b"\n")
119+
120+
def print_bitmap_row(self, data, reverse_bits=True):
121+
self.mode = MODE_BITMAP
122+
self._print_common(data, reverse_bits)

0 commit comments

Comments
 (0)