Skip to content

Instantly share code, notes, and snippets.

@tonosaman
Forked from tai/casio.py
Created September 12, 2024 02:57
Show Gist options
  • Save tonosaman/39e0bed0ad1d645f0404bba30e1b0649 to your computer and use it in GitHub Desktop.
Save tonosaman/39e0bed0ad1d645f0404bba30e1b0649 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
# -*- coding: utf-8-unix -*-
"""
Label printing tool for Casio "NameLand" USB label printer.
This module provides control interface to series of label printers
from Casio, namely KLD-300. It probably works with other older
models, but I have only tested this with KLD-300.
KLD-300 is an USB device with vendor-specific class, and uses
EP1_OUT to send command/data and EP2_IN for receiving response/status.
There are 2 types of command: 1-byte command and multi-byte command.
1-byte command is used for basic operation, like feeding and cutting
a tape. Multi-byte command is used for more complex control, like
configuring printer or sending label data. It seems to use
first 4 bytes to express command type, and rest for payload to send data.
Most command only expects 1-byte response code from a printer.
Response code 0x06 seems to be ACK, and 0x15 seems to be NAK.
Other codes are also seen, and generally seem to indicate other
non-ACK states like "busy".
Structure of command/control bytes are reverse engineered to
some extent, but it is not complete. In this script, many bytes are
simply replayed as captured. Comment "ALWAYSSAME" means captured
byte never changed regardless of tape or print configuration.
USB packet size is configured as 64bytes, so maximum payload is 60bytes.
This means label data (bitmap data) is split and sent in 60-byte block.
Label image format is a simple 1-bit (black and white) bitmap with
bits packed into a byte. Bytes are in column-oriented format,
meaning bytes in column 1 is sent before any bytes in column 2 is sent.
Also, bit order in each byte is in little endian order.
Label image is expected to be either in 128xN or 64xN bitmap.
Larger tape requires 128xN bitmap, and smaller tape requires 64xN bitmap.
To ease printing of usual "WIDTHx128" or "WIDTHx64" bitmap,
img2casio() and casio2img() functions may be used to convert
standard PIL image into Casio-styled transposed/bitflipped PIL image.
"""
import usb
import PIL
import PIL.ImageDraw
import PIL.ImageFont
import logging
log = logging.getLogger(__name__)
######################################################################
VID = 0x07cf
PID = 0x4106
EP_OUT = 0x01 | usb.ENDPOINT_OUT
EP_IN = 0x02 | usb.ENDPOINT_IN
ACK = b"\x06"
NAK = b"\x15"
CUT_FULL = 0x00
CUT_AUTO = 0x01
CUT_NONE = 0xFF
TTFONT = '/usr/share/fonts/truetype/fonts-japanese-gothic.ttf'
#TTFONT = '/usr/share/fonts/truetype/mikachan/mikachan.ttf'
######################################################################
def find(vid=VID, pid=PID):
"""Find Casio label printer"""
for bus in usb.busses():
for dev in bus.devices:
if dev.idVendor != vid: continue
yield dev
def dec(capdata):
"""Decode wireshark-decoded hex dump into bytearray"""
return bytearray.fromhex(capdata.replace(":", " "))
def cmd(buf, size=0):
"""Return command bytes in 16B buffer"""
return buf if size <= len(buf) else buf.ljust(size, b"\x00")
def ww(dev, data):
"""Write data to device"""
return dev.bulkWrite(EP_OUT, data, timeout=10000)
def rr(dev, size=1):
"""Read data from device"""
return bytes(dev.bulkRead(EP_IN, size, timeout=10000))
def send(dev, data, size=1):
"""Write to and then read from device"""
ww(dev, data)
return rr(dev, size=size)
######################################################################
def bitrev(u8):
"""Reverse bit-order of a byte"""
ret = 0
for i in range(8):
if u8 & (1 << i): ret |= (0x80 >> i)
return ret
def maketbl(fn, src=bytes(range(256))):
"""Create byte translation table"""
dst = bytes([fn(i) for i in src])
return bytearray.maketrans(src, dst)
# bit-reversing translation table
RTBL = maketbl(bitrev)
def img2casio(img):
"""Convert PIL image to Casio-style PIL bitmap"""
tmp = img.convert('1').transpose(PIL.Image.TRANSPOSE)
buf = tmp.tobytes().translate(RTBL)
cbm = PIL.Image.frombytes(mode='1', data=buf, size=tmp.size)
return cbm
def casio2img(cbm):
"""Convert Casio-style PIL bitmap into standard PIL bitmap"""
buf = cbm.tobytes().translate(RTBL)
img = PIL.Image.frombytes(mode='1', data=buf, size=cbm.size)
return img.transpose(PIL.Image.TRANSPOSE)
def loadfont(fontpath=TTFONT, size=128):
"""Return PIL ImageFont loaded from given fontfile"""
return PIL.ImageFont.truetype(fontpath, size)
def label(msg, size=(512, 128), font=None):
"""Create a label in standard PIL bitmap (not in Casio-style)"""
img = PIL.Image.new('1', size)
draw = PIL.ImageDraw.Draw(img)
draw.font = font if font else PIL.ImageFont.load_default()
msgsize = draw.font.getsize(msg)
ydelta = img.size[1] - msgsize[1]
draw.text((0, ydelta >> 1), msg, 1)
return img
######################################################################
def ping(dev):
"""Send first command in the capture. Should return ACK"""
data = dec("02:02:04:00:00:04:03:01:00:00:00:00:00:00:00:00")
return send(dev, data)
def probe(dev):
"""Send second command in the capture. Returns 6B of data"""
data = dec("02:1d:00:00:00:00:00:00:00:00:00:00:00:00:00:00")
return send(dev, data, 6)
def get_tape_size(dev):
"""Check tape size"""
data = dec("02:1a:00:00:00:00:00:00:00:00:00:00:00:00:00:00")
return send(dev, data, 6)
def set_label_size(dev, label_size):
"""Check if label and tape size matches"""
data = dec("02:17:02:00:81:00:00:00:00:00:00:00:00:00:00:00")
if label_size == 6.0:
data[4:5] = b"\x81\x00"
elif label_size == 9.0:
data[4:5] = b"\x85\x00"
elif label_size == 18.0:
data[4:5] = b"\x87\x03"
else:
return NAK
return send(dev, data)
def set_config1(dev, tape_size, gray_level=3):
"""Configure tape size (3.5-24.0) and gray level (1-5)"""
data = dec("02:09:06:00:01:00:ff:ff:00:00:00:00:00:00:00:00")
if tape_size in (6.0, 9.0):
data[4] = 0x01
else:
data[4] = 0x00
# gray level: 1:FE, 2:FF, 3:00, 4:01, 5:02
data[8] = 0xFF & (gray_level - 3)
return send(dev, data)
def set_config2(dev, cut_mode=CUT_AUTO):
"""Configure tape cut mode"""
data = dec("02:19:01:00:01:00:00:00:00:00:00:00:00:00:00:00")
data[4] = cut_mode
return send(dev, data)
def send_label(dev, img, nr=1):
"""Send label image in PIL bitmap with 64xN or 128xN size"""
# data must be split into payload size
USB_PAYLOAD_SIZE = 60
# prepare data to send. use dummy data if not given
if img is None:
buf = bytearray(USB_PAYLOAD_SIZE)
else:
buf = bytearray(img.tobytes())
# align by USB payload size
padlen = USB_PAYLOAD_SIZE - len(buf) % USB_PAYLOAD_SIZE
buf.extend([0] * padlen)
buflen = len(buf)
for i in range(nr):
# send each block
for i in range(0, buflen - USB_PAYLOAD_SIZE, USB_PAYLOAD_SIZE):
ret = send(dev, b"\x02\xfe\x3c\x00" + buf[i:i+USB_PAYLOAD_SIZE])
# send last block?
i = buflen - USB_PAYLOAD_SIZE
#ret = send(dev, b"\x02\xfe\x30\x00" + buf[i:i+USB_PAYLOAD_SIZE])
ret = send(dev, b"\x02\xfe\x0c\x00" + buf[i:i+USB_PAYLOAD_SIZE])
# mark end-of-label?
data = dec("02:04:00:00:00:00:00:00:00:00:00:00:00:00:00:00")
ret = send(dev, data)
# move tape a little?
ret = send(dev, b"\x0c")
return ret
def feed(dev):
"""Feed some tape. Should return ACK."""
data = dec("02:1b:01:00:15:00:00:00:00:00:00:00:00:00:00:00")
return send(dev, data)
def cut(dev):
"""Cut tape. Should return ACK. Can be called anytime"""
return send(dev, b"\x07")
def reset(dev):
"""Reset device. No return value. May feed some tape"""
ww(dev, b"\x18")
######################################################################
def print_label(dev, img, size=6.0, gray=3, cut_mode=CUT_AUTO, nr=1):
"""Print label by replaying what was captured"""
ret = ping(dev)
ret = probe(dev)
# ALWAYSSAME
data = dec("02:82:00:00:00:00:00:00:00:00:00:00:00:00:00:00")
ret = send(dev, data, 5)
# size check
ret = get_tape_size(dev)
ret = set_label_size(dev, size)
# ALWAYSSAME
ret = send(dev, dec("02:01:00:00:00:00:00:00:00:00:00:00:00:00:00:00"))
ret = send(dev, dec("02:1c:01:00:01:00:00:00:00:00:00:00:00:00:00:00"))
ret = send(dev, dec("02:0d:01:00:40:00:00:00:00:00:00:00:00:00:00:00"))
# configure
ret = set_config1(dev, tape_size=size, gray_level=gray)
ret = set_config2(dev, cut_mode=cut_mode)
# print
ret = send_label(dev, img, nr)
ret = feed(dev)
ret = cut(dev)
reset(dev)
if __name__ == '__main__' and '__file__' in globals():
dev = None
try:
dev = next(find()).open()
dev.claimInterface(0)
ttfont = loadfont()
img = label("ABC", size=(512, 128), font=ttfont)
cbm = img2casio(img)
print_label(dev, cbm.resize((64, 256)), size=6.0)
finally:
if dev: dev.releaseInterface()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment