Skip to content

Instantly share code, notes, and snippets.

@aleksas
Last active March 13, 2024 01:25
Show Gist options
  • Save aleksas/5fbead40311b1cc8b355f1ec86e57346 to your computer and use it in GitHub Desktop.
Save aleksas/5fbead40311b1cc8b355f1ec86e57346 to your computer and use it in GitHub Desktop.
Command `DYMO LabelMaster 280` from python on Ubuntu

Script copied from original dymoprint page and modified to include LM280 usb device id 0x1005 and ttf paths changed. Also usb device class changed from 0x3 to 0x7.

  • Install drivers from this repo by following these instructions.
  • Check if Ubuntu detects the LM 280 when connected to computer and powered on.
  • Run the script.
#!/usr/bin/env python3
# === LICENSE STATEMENT ===
# Copyright (c) 2011 Sebastian J. Bronner <waschtl@sbronner.com>
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice and
# this notice are preserved.
# === END LICENSE STATEMENT ===
# The following module libraries are not included with python and need to be
# installed separately:
# * Pillow (python-pillow or python3-pillow)
# * PyUSB (python-pyusb, python3-pyusb, python-usb, or python3-usb)
from PIL import Image, ImageDraw, ImageFont
import argparse
import array
import fcntl
import os
import re
import struct
import subprocess
import sys
import termios
import textwrap
import usb
#FONT_FILENAME = '/usr/share/fonts/TTF/DejaVuSans.ttf'
#FONT_FILENAME = '/usr/share/fonts/TTF/DejaVuSans-Bold.ttf'
#FONT_FILENAME = '/usr/share/fonts/TTF/DejaVuSans-Oblique.ttf'
#FONT_FILENAME = '/usr/share/fonts/TTF/DejaVuSans-BoldOblique.ttf'
#FONT_FILENAME = '/usr/share/fonts/TTF/DejaVuSans-ExtraLight.ttf'
FONT_FILENAME = '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'
#FONT_FILENAME = '/usr/share/fonts/TTF/DejaVuSansCondensed-Bold.ttf'
#FONT_FILENAME = '/usr/share/fonts/TTF/DejaVuSansCondensed-Oblique.ttf'
#FONT_FILENAME = '/usr/share/fonts/TTF/DejaVuSansCondensed-BoldOblique.ttf'
#FONT_FILENAME = '/usr/share/fonts/TTF/ezra/SILEOT.ttf'
#FONT_FILENAME = '/usr/share/fonts/TTF/gentium/Gentium-R.ttf' # IPA
FONT_SIZERATIO = 7./8
VERSION = "0.2.0 (2016-11-07)"
class DymoLabeler:
"""Create and work with a Dymo LabelManager PnP object.
This class contains both mid-level and high-level functions. In general,
the high-level functions should be used. However, special purpose usage
may require the mid-level functions. That is why they are provided.
However, they should be well understood before use. Look at the
high-level functions for help. Each function is marked in its docstring
with 'HLF' or 'MLF' in parentheses.
"""
_ESC = 0x1b
_SYN = 0x16
_MAX_BYTES_PER_LINE = 8 # 64 pixels on a 12mm tape
_USB_VENDOR = 0x0922
_USB_PRODUCT = (0x1001, 0x1002, 0x1005)
# Number of commands to send before waiting for a response. This helps
# to avoid timeouts due to differences between data transfer and
# printer speeds. I added this because I kept getting "IOError: [Errno
# 110] Connection timed out" with long labels. Using dev.default_timeout
# (1000) and the transfer speeds available in the descriptors somewhere, a
# sensible timeout can also be calculated dynamically.
_SYNWAIT = 64
def __init__(self):
"""Initialize the LabelManager object. (HLF)"""
self.cmd = []
self.response = False
self.bytes_per_line_ = None
self.dot_tab_ = 0
# Find and prepare device communication endpoints.
dev = usb.core.find(custom_match = lambda d: (d.idVendor ==
self._USB_VENDOR and d.idProduct in self._USB_PRODUCT))
if dev is None:
raise RuntimeError("No USB device matching the following criteria "
"was found:\n idVendor: 0x%04x\n idProduct: %s" %
(self._USB_VENDOR, ', '.join('0x%04x' % id_ for id_ in
self._USB_PRODUCT)))
try:
dev.set_configuration()
except usb.core.USBError as e:
if e.errno == 13:
# Handle error number 13 (Access denied) by printing
# instructions for gaining access.
lines = []
lines.append("You do not have sufficient access to the "
"device. You probably want to add some udev rules in "
"/etc/udev/rules.d/dymoprint.rules along the following "
"lines:")
lines.append("")
for id_ in self._USB_PRODUCT:
lines.append('ACTION=="add", SUBSYSTEMS=="usb", '
'ATTRS{idVendor}=="%04X", ATTRS{idProduct}=="%04X", '
'MODE="0660", GROUP="plugdev"' % (self._USB_VENDOR, id_))
lines.append("")
lines.append("Following that, turn your device off and back "
"on again to activate the new permissions.")
raise RuntimeError('\n'.join(lines))
if e.errno == 16:
# Handle error number 16 (Resource busy) by ignoring it. It
# just means that the configuration has already been set.
pass
else:
# On all other errors, the original exception is simple
# re-raised.
raise
intf = usb.util.find_descriptor(dev.get_active_configuration(),
bInterfaceClass=0x7)
if dev.is_kernel_driver_active(intf.bInterfaceNumber):
dev.detach_kernel_driver(intf.bInterfaceNumber)
self.data = usb.util.find_descriptor(intf, custom_match = (lambda e:
usb.util.endpoint_direction(e.bEndpointAddress) ==
usb.util.ENDPOINT_OUT))
self.status = usb.util.find_descriptor(intf, custom_match = (lambda e:
usb.util.endpoint_direction(e.bEndpointAddress) ==
usb.util.ENDPOINT_IN))
def send_command(self):
"""Send the already built command to the LabelManager. (MLF)"""
if len(self.cmd) == 0:
return
while len(self.cmd) > 0:
synCount = 0
pos = -1
while synCount < self._SYNWAIT:
try:
pos += self.cmd[pos+1:].index(self._SYN) + 1
except ValueError:
pos = len(self.cmd)
break
synCount += 1
cmdBin = array.array('B', [self._ESC, ord('A')])
cmdBin.tofile(self.data)
rspBin = self.status.read(8)
rsp = array.array('B', rspBin).tolist()
print(rsp, pos, len(self.cmd))
cmdBin = array.array('B', self.cmd[:pos])
cmdBin.tofile(self.data)
self.cmd = self.cmd[pos:]
self.cmd = []
if not self.response:
return
self.response = False
responseBin = self.status.read(8)
response = array.array('B', responseBin).tolist()
return response
def reset_command(self):
"""Remove a partially built command. (MLF)"""
self.cmd = []
self.response = False
def build_command(self, cmd):
"""Add the next instruction to the command. (MLF)"""
self.cmd += cmd
def status_request(self):
"""Set instruction to get the device's status. (MLF)"""
cmd = [self._ESC, ord('A')]
self.build_command(cmd)
self.response = True
def dot_tab(self, value):
"""Set the bias text height, in bytes. (MLF)"""
if value < 0 or value > self._MAX_BYTES_PER_LINE:
raise ValueError
cmd = [self._ESC, ord('B'), value]
self.build_command(cmd)
self.dot_tab_ = value
self.bytes_per_line_ = None
def tape_color(self, value):
"""Set the tape color. (MLF)"""
if value < 0: raise ValueError
cmd = [self._ESC, ord('C'), value]
self.build_command(cmd)
def bytes_per_line(self, value):
"""Set the number of bytes sent in the following lines. (MLF)"""
if value < 0 or value + self.dot_tab_ > self._MAX_BYTES_PER_LINE:
raise ValueError
if value == self.bytes_per_line_:
return
cmd = [self._ESC, ord('D'), value]
self.build_command(cmd)
self.bytes_per_line_ = value
def cut(self):
"""Set instruction to trigger cutting of the tape. (MLF)"""
cmd = [self._ESC, ord('E')]
self.build_command(cmd)
def line(self, value):
"""Set next printed line. (MLF)"""
self.bytes_per_line(len(value))
cmd = [self._SYN] + value
self.build_command(cmd)
def chain_mark(self):
"""Set Chain Mark. (MLF)"""
self.dot_tab(0)
self.bytes_per_line(self._MAX_BYTES_PER_LINE)
self.line([0x99] * self._MAX_BYTES_PER_LINE)
def skip_lines(self, value):
"""Set number of lines of white to print. (MLF)"""
if value <= 0:
raise ValueError
self.bytes_per_line(0)
cmd = [self._SYN] * value
self.build_command(cmd)
def init_label(self):
"""Set the label initialization sequence. (MLF)"""
cmd = [0x00] * 8
self.build_command(cmd)
def get_status(self):
"""Ask for and return the device's status. (HLF)"""
self.status_request()
response = self.send_command()
print(response)
def print_label(self, lines, dot_tab):
"""Print the label described by lines. (HLF)"""
self.init_label
self.tape_color(0)
self.dot_tab(dot_tab)
for line in lines:
self.line(line)
self.skip_lines(56) # advance printed matter past cutter
self.skip_lines(56) # add symmetric margin
self.status_request()
response = self.send_command()
print(response)
def die(message=None):
if message: print(message, file=sys.stderr)
sys.exit(1)
def pprint(par, fd=sys.stdout):
rows, columns = struct.unpack('HH', fcntl.ioctl(sys.stderr,
termios.TIOCGWINSZ, struct.pack('HH', 0, 0)))
print(textwrap.fill(par, columns), file=fd)
def main():
# set up argument parsing with usage and help output
description = ("This script will print labels on a Dymo LabelManager PnP "
"connected to your computer via USB.")
parser = argparse.ArgumentParser(
description=description)
parser.add_argument(
'-v', '--version',
action='version',
version='%(prog)s ' + VERSION
)
parser.add_argument(
'lines',
metavar='line',
nargs='+',
help=("A single line will be printed at the maximum available size on "
"the label. If multiple lines are specified, they will be reduced "
"in size to fit the vertical space and will be placed in a "
"column."))
args = parser.parse_args()
# create dymo labeler object
lm = DymoLabeler()
# create an empty label image
labelheight = lm._MAX_BYTES_PER_LINE * 8
lineheight = float(labelheight) / len(args.lines)
fontsize = int(round(lineheight * FONT_SIZERATIO))
font = ImageFont.truetype(FONT_FILENAME, fontsize)
labelwidth = max(font.getsize(line)[0] for line in args.lines)
labelbitmap = Image.new('1', (labelwidth, labelheight))
# write the text into the empty image
labeldraw = ImageDraw.Draw(labelbitmap)
for i, line in enumerate(args.lines):
lineposition = int(round(i * lineheight))
labeldraw.text((0, lineposition), line, font=font, fill=255)
del labeldraw
# convert the image to the proper matrix for the dymo labeler object
labelrotated = labelbitmap.transpose(Image.ROTATE_270)
labelstream = labelrotated.tobytes()
labelstreamrowlength = labelheight//8 + (1 if labelheight%8 != 0 else 0)
if len(labelstream)/labelstreamrowlength != labelwidth:
die('An internal problem was encountered while processing the label '
'bitmap!')
labelrows = [labelstream[i:i+labelstreamrowlength] for i in
range(0, len(labelstream), labelstreamrowlength)]
labelmatrix = [array.array('B', labelrow).tolist() for labelrow in
labelrows]
# optimize the matrix for the dymo label printer
dottab = 0
while max(line[0] for line in labelmatrix) == 0:
labelmatrix = [line[1:] for line in labelmatrix]
dottab += 1
for line in labelmatrix:
while len(line) > 0 and line[-1] == 0:
del line[-1]
# print the label
lm.print_label(labelmatrix, dottab)
if __name__ == '__main__':
main()
# TODO
# * support multiple ProductIDs (1001, 1002)
# * put everything in classes that would need to be used by a GUI
# * allow selection of font with command line options
# * allow font size specification with command line option (points, pixels?)
# * provide an option to show a preview of what the label will look like
# * read and write a .dymoprint file containing user preferences
# * implement errors using exceptions
# * implement regular output using standard methods
# * get rid of die()
# * get rid of pprint()
# * get rid of access_error()
# * provide a version that includes its dependent libraries, and a version that
# uses system libraries
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment