code for new guide
This commit is contained in:
parent
dcd5f0d4de
commit
b5e610e9b2
7 changed files with 354 additions and 0 deletions
BIN
CircuitPython_BLEThermalPrinter/adabot.bmp
Normal file
BIN
CircuitPython_BLEThermalPrinter/adabot.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
CircuitPython_BLEThermalPrinter/blinka.bmp
Normal file
BIN
CircuitPython_BLEThermalPrinter/blinka.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
142
CircuitPython_BLEThermalPrinter/code.py
Normal file
142
CircuitPython_BLEThermalPrinter/code.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import seekablebitmap
|
||||
|
||||
import gc
|
||||
import os
|
||||
import struct
|
||||
import time
|
||||
|
||||
import board
|
||||
import digitalio
|
||||
import keypad
|
||||
import ulab.numpy as np
|
||||
|
||||
import adafruit_ble
|
||||
from adafruit_ble import BLERadio
|
||||
from adafruit_ble.advertising import Advertisement
|
||||
|
||||
from thermalprinter import CatPrinter
|
||||
from seekablebitmap import imageopen
|
||||
|
||||
ble = BLERadio() # pylint: disable=no-member
|
||||
|
||||
buttons = keypad.Keys([board.BUTTON_A, board.BUTTON_B], value_when_pressed=False)
|
||||
|
||||
def pnmopen(filename):
|
||||
"""
|
||||
Scan for netpbm format info, skip over comments, and read header data.
|
||||
|
||||
Return the format, header, and the opened file positioned at the start of
|
||||
the bitmap data.
|
||||
"""
|
||||
# pylint: disable=too-many-branches
|
||||
image_file = open(filename, "rb")
|
||||
magic_number = image_file.read(2)
|
||||
image_file.seek(2)
|
||||
pnm_header = []
|
||||
next_value = bytearray()
|
||||
while True:
|
||||
# We have all we need at length 3 for formats P2, P3, P5, P6
|
||||
if len(pnm_header) == 3:
|
||||
return image_file, magic_number, pnm_header
|
||||
|
||||
if len(pnm_header) == 2 and magic_number in [b"P1", b"P4"]:
|
||||
return image_file, magic_number, pnm_header
|
||||
|
||||
next_byte = image_file.read(1)
|
||||
if next_byte == b"":
|
||||
raise RuntimeError("Unsupported image format {}".format(magic_number))
|
||||
if next_byte == b"#": # comment found, seek until a newline or EOF is found
|
||||
while image_file.read(1) not in [b"", b"\n"]: # EOF or NL
|
||||
pass
|
||||
elif not next_byte.isdigit(): # boundary found in header data
|
||||
if next_value:
|
||||
# pull values until space is found
|
||||
pnm_header.append(int("".join(["%c" % char for char in next_value])))
|
||||
next_value = bytearray() # reset the byte array
|
||||
else:
|
||||
next_value += next_byte # push the digit into the byte array
|
||||
|
||||
def wait_for_press(kbd):
|
||||
"""
|
||||
Wait for a keypress and return the event
|
||||
"""
|
||||
while True:
|
||||
event = kbd.events.get()
|
||||
if event and event.pressed:
|
||||
return event
|
||||
|
||||
def show(s):
|
||||
"""
|
||||
Display a message on the screen
|
||||
"""
|
||||
board.DISPLAY.auto_refresh = False
|
||||
print("\n" * 24)
|
||||
print(s)
|
||||
board.DISPLAY.auto_refresh = True
|
||||
|
||||
def show_error(s):
|
||||
"""
|
||||
Display a message on the screen and wait for a button press
|
||||
"""
|
||||
show(s + "\nPress a button to continue")
|
||||
wait_for_press(buttons)
|
||||
|
||||
def find_cat_printer(radio):
|
||||
"""
|
||||
Connect to the cat printer device using BLE
|
||||
"""
|
||||
while True:
|
||||
show("Scanning for GB02 device...")
|
||||
for adv in radio.start_scan(Advertisement):
|
||||
complete_name = getattr(adv, 'complete_name')
|
||||
if complete_name is not None:
|
||||
print(f"Saw {complete_name}")
|
||||
if complete_name == 'GB02':
|
||||
radio.stop_scan()
|
||||
return radio.connect(adv, timeout=10)[CatPrinter]
|
||||
|
||||
image_files = [i for i in os.listdir('/') if i.lower().endswith(".pbm") or i.lower().endswith(".bmp")]
|
||||
image_files.sort(key=lambda filename: filename.lower())
|
||||
|
||||
def select_image():
|
||||
i = 0
|
||||
while True:
|
||||
show(f"Select image file\nA: next image\nB: print this image\n\n{image_files[i]}")
|
||||
event = wait_for_press(buttons)
|
||||
if event.key_number == 0: # button "A"
|
||||
i = (i + 1) % len(image_files)
|
||||
if event.key_number == 1: # button "B"
|
||||
return image_files[i]
|
||||
|
||||
printer = find_cat_printer(ble)
|
||||
|
||||
while True:
|
||||
try:
|
||||
filename = select_image()
|
||||
|
||||
show(f"Loading {filename}")
|
||||
|
||||
image = imageopen(filename)
|
||||
if image.width != 384:
|
||||
raise ValueError("Invalid image. Must be 384 pixels wide")
|
||||
if image.bits_per_pixel != 1:
|
||||
raise ValueError("Invalid image. Must be 1 bit per pixel (black & white)")
|
||||
|
||||
invert_image = image.palette and image.palette[0] == 0
|
||||
|
||||
show(f"Printing {filename}")
|
||||
|
||||
for i in range(image.height):
|
||||
row_data = image.get_row(i)
|
||||
if invert_image:
|
||||
row_data = ~np.frombuffer(row_data, dtype=np.uint8)
|
||||
printer.print_bitmap_row(row_data)
|
||||
|
||||
# Print blank lines until the paper can be torn off
|
||||
for i in range(80):
|
||||
printer.print_bitmap_row(b'\0' * 48)
|
||||
|
||||
except Exception as e:
|
||||
show_error(str(e))
|
||||
image_files.remove(filename)
|
||||
continue
|
||||
BIN
CircuitPython_BLEThermalPrinter/cricket.bmp
Normal file
BIN
CircuitPython_BLEThermalPrinter/cricket.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
CircuitPython_BLEThermalPrinter/minerva.bmp
Normal file
BIN
CircuitPython_BLEThermalPrinter/minerva.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
90
CircuitPython_BLEThermalPrinter/seekablebitmap.py
Normal file
90
CircuitPython_BLEThermalPrinter/seekablebitmap.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import struct
|
||||
import ulab
|
||||
|
||||
class SeekableBitmap:
|
||||
def __init__(self, image_file, width, height, bits_per_pixel, *, bytes_per_row=None, data_start=None, stride=None, palette=None):
|
||||
self.image_file = image_file
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.bits_per_pixel = bits_per_pixel
|
||||
self.bytes_per_row = bytes_per_row if bytes_per_row else (bits_per_pixel * width + 7) // 8
|
||||
self.stride = stride if stride else self.bytes_per_row
|
||||
self.palette = palette
|
||||
self.data_start = data_start if data_start else image_file.tell()
|
||||
|
||||
def get_row(self, row):
|
||||
self.image_file.seek(self.data_start + row * self.stride)
|
||||
return self.image_file.read(self.bytes_per_row)
|
||||
|
||||
def _pnmopen(filename):
|
||||
"""
|
||||
Scan for netpbm format info, skip over comments, and read header data.
|
||||
|
||||
Return the format, header, and the opened file positioned at the start of
|
||||
the bitmap data.
|
||||
"""
|
||||
# pylint: disable=too-many-branches
|
||||
image_file = open(filename, "rb")
|
||||
magic_number = image_file.read(2)
|
||||
image_file.seek(2)
|
||||
pnm_header = []
|
||||
next_value = bytearray()
|
||||
while True:
|
||||
# We have all we need at length 3 for formats P2, P3, P5, P6
|
||||
if len(pnm_header) == 3:
|
||||
return image_file, magic_number, pnm_header
|
||||
|
||||
if len(pnm_header) == 2 and magic_number in [b"P1", b"P4"]:
|
||||
return image_file, magic_number, pnm_header
|
||||
|
||||
next_byte = image_file.read(1)
|
||||
if next_byte == b"":
|
||||
raise RuntimeError("Unsupported image format {}".format(magic_number))
|
||||
if next_byte == b"#": # comment found, seek until a newline or EOF is found
|
||||
while image_file.read(1) not in [b"", b"\n"]: # EOF or NL
|
||||
pass
|
||||
elif not next_byte.isdigit(): # boundary found in header data
|
||||
if next_value:
|
||||
# pull values until space is found
|
||||
pnm_header.append(int("".join(["%c" % char for char in next_value])))
|
||||
next_value = bytearray() # reset the byte array
|
||||
else:
|
||||
next_value += next_byte # push the digit into the byte array
|
||||
|
||||
def pnmopen(filename):
|
||||
image_file, magic_number, pnm_header = _pnmopen(filename)
|
||||
if magic_number == b'P4':
|
||||
return SeekableBitmap(image_file, pnm_header[0], pnm_header[1], 1, palette=b'\xff\xff\xff\x00\x00\x00\x00\x00')
|
||||
if magic_number == b'P5':
|
||||
return SeekableBitmap(image_file, pnm_header[0], pnm_header[1], pnm_header[2].bit_length())
|
||||
if magic_number == b'P6':
|
||||
return SeekableBitmap(image_file, pnm_header[0], pnm_header[1], 3*pnm_header[2].bit_length())
|
||||
raise ValueError(f"Unknown or unsupported magic number {magic_number}")
|
||||
|
||||
def bmpopen(filename):
|
||||
image_file = open(filename, "rb")
|
||||
|
||||
header = image_file.read(34)
|
||||
|
||||
data_start, header_size, width, height, planes, bits_per_pixel, method = struct.unpack("<10x4l2hl", header)
|
||||
|
||||
bits_per_pixel = bits_per_pixel if bits_per_pixel != 0 else 1
|
||||
|
||||
mask_size = (12 if header_size < 56 else 16) if method == 3 else 0
|
||||
palette_start = header_size + 14
|
||||
image_file.seek(palette_start)
|
||||
palette = image_file.read(4 << bits_per_pixel)
|
||||
|
||||
stride=(bits_per_pixel*width+31)//32*4
|
||||
if height < 0:
|
||||
height = -height
|
||||
else:
|
||||
data_start = data_start + stride * (height - 1)
|
||||
stride = -stride
|
||||
|
||||
return SeekableBitmap(image_file, width, height, bits_per_pixel, data_start=data_start, stride=stride, palette=palette)
|
||||
|
||||
def imageopen(filename):
|
||||
if filename.lower().endswith(".bmp"):
|
||||
return bmpopen(filename)
|
||||
return pnmopen(filename)
|
||||
122
CircuitPython_BLEThermalPrinter/thermalprinter.py
Normal file
122
CircuitPython_BLEThermalPrinter/thermalprinter.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# Protocol information from Thermal_Printer Arduino library
|
||||
# https://github.com/bitbank2/Thermal_Printer/
|
||||
import collections
|
||||
import time
|
||||
from adafruit_ble.uuid import StandardUUID
|
||||
from adafruit_ble.services import Service
|
||||
from adafruit_ble.characteristics.stream import StreamIn
|
||||
|
||||
PrinterConfig = collections.namedtuple(
|
||||
"PrinterConfig", ("width", "service", "characteristic")
|
||||
)
|
||||
|
||||
# Switch the printing mode to bitmap
|
||||
printimage = b"Qx\xbe\x00\x01\x00\x00\x00\xff"
|
||||
# Switch the printing mode to text
|
||||
printtext = b"Qx\xbe\x00\x01\x00\x01\x07\xff"
|
||||
# Command to feed paper
|
||||
paperfeed = b"Qx\xa1\x00\x02\x00\x1eZ\xff\xff"
|
||||
|
||||
# this table helps compute the checksum of transmitted data
|
||||
# it is crc-8-ccitt
|
||||
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"
|
||||
|
||||
# mirrortable[i] is the bit reversed version of the byte i
|
||||
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"
|
||||
|
||||
|
||||
def checksum(data, start, count):
|
||||
cs = 0
|
||||
for i in range(start, start + count):
|
||||
cs = checksumtable[cs ^ data[i]]
|
||||
return cs
|
||||
|
||||
|
||||
PRINTER_CAT = PrinterConfig(width=384, service=0xA3E0, characteristic=0xAE01)
|
||||
|
||||
MODE_TEXT = "MODE_TEXT"
|
||||
MODE_BITMAP = "MODE_BITMAP"
|
||||
|
||||
printers = {"GB02": PRINTER_CAT, "GB01": PRINTER_CAT}
|
||||
|
||||
class CatPrinter(Service):
|
||||
|
||||
uuid = StandardUUID(0xAE30)
|
||||
|
||||
_tx = StreamIn(uuid=StandardUUID(0xAE01),
|
||||
timeout=1.0,
|
||||
buffer_size=256,
|
||||
)
|
||||
|
||||
def _write_data(self, buf):
|
||||
result = self._tx.write(buf)
|
||||
|
||||
@property
|
||||
def bitmap_width(self):
|
||||
return 384
|
||||
|
||||
def __init__(self, service=None):
|
||||
super().__init__(service=service)
|
||||
self._mode = None
|
||||
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
return self._mode
|
||||
|
||||
@mode.setter
|
||||
def mode(self, value):
|
||||
if value == self.mode:
|
||||
return
|
||||
|
||||
if value == MODE_TEXT:
|
||||
self._write_data(printtext)
|
||||
elif value == MODE_BITMAP:
|
||||
self._write_data(printimage)
|
||||
else:
|
||||
raise ValueError("Invalid mode %r" % value)
|
||||
|
||||
self._mode = value
|
||||
|
||||
def feed_lines(self, lines):
|
||||
buf = bytearray(paperfeed)
|
||||
buf[6] = lines & 0xff
|
||||
buf[7] = lines >> 8
|
||||
buf[8] = checksum(buf, 6, 2)
|
||||
self._write_data(buf)
|
||||
|
||||
def _print_common(self, text, reverse_bits=True):
|
||||
|
||||
offset = 0
|
||||
data = memoryview(text)
|
||||
while data:
|
||||
sz = min(112, len(data))
|
||||
sub_data = data[:sz]
|
||||
data = data[sz:]
|
||||
buf = bytearray(sz + 8)
|
||||
buf[0] = 0x51
|
||||
buf[1] = 0x78
|
||||
buf[2] = 0xA2
|
||||
buf[3] = 0x0
|
||||
buf[4] = sz
|
||||
buf[5] = 0
|
||||
if reverse_bits:
|
||||
buf[6 : 6 + sz] = bytes(mirrortable[c] for c in sub_data)
|
||||
else:
|
||||
buf[6 : 6 + sz] = sub_data
|
||||
buf[6 + sz] = checksum(buf, 6, len(sub_data))
|
||||
buf[6 + sz + 1] = 0xFF
|
||||
|
||||
self._write_data(buf)
|
||||
|
||||
def print_text(self, text):
|
||||
self.mode = MODE_TEXT
|
||||
self._print_common(text.encode("utf-8"))
|
||||
|
||||
def print_line(self, text):
|
||||
self.print_text(text)
|
||||
self._print_common(b"\n")
|
||||
|
||||
def print_bitmap_row(self, data, reverse_bits=True):
|
||||
self.mode = MODE_BITMAP
|
||||
self._print_common(data, reverse_bits)
|
||||
Loading…
Reference in a new issue